mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
adventure: T6 postgame Layer-2 seam + Aurvandryx's Greed Tax (P8)
Add the pre-combat boss-hook seam for Tier-6 postgame bosses and the first bespoke mechanic on it. Seam: applyBossRunModifiers(bossID, enemy, run) in postgame_boss_hooks.go — a pure, idempotent, bestiary-ID-dispatched hook that folds run-state-derived adjustments into the freshly-built boss Combatant. No-op for every non-hooked enemy or nil run. The turn engine (which resolves both prod bosses and the sim) rebuilds the enemy every round via partyCombatantsForSession, so the hook must be a pure function of run state — fine at a terminal boss room, where the route is frozen. Wired at both enemy-finalization points: the per-round rebuild and buildFightSeats' initial HP persist (threaded a run param; the caller already had it). Greed Tax (boss_aurvandryx / first_hoard): her Attack rises with the richness of the route walked to reach her — the summed excess LootBias (>1.0) over the run's visited nodes. Note run.LootCollected is the wrong signal (BossOnly signature manifest, empty at the boss); the gilded veins on the graph are. Attack += min(2.0*richness, 12). Same-binary A/B sweep (n=150, L20 party + Pete + pets): taxless 66.7% -> taxed 48.0%, landing first_hoard mid-band. Claude-Session: https://claude.ai/code/session_0156WqjgsbmSY2U8eQ3Kkb1s
This commit is contained in:
98
internal/plugin/postgame_boss_hooks.go
Normal file
98
internal/plugin/postgame_boss_hooks.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package plugin
|
||||
|
||||
// Tier-6 postgame "Layer-2" boss mechanics (Phase P8).
|
||||
//
|
||||
// Layer 1 (shipped) is everything the stock engine expresses: a single
|
||||
// MonsterAbility rider plus HP/AC/Attack/PhaseTwoAt on the stat block. Layer 2
|
||||
// is the bespoke, per-boss stuff the plan promised — mechanics that read the
|
||||
// *run* the player took to reach the boss, not just the fight in front of them.
|
||||
//
|
||||
// This file holds the PRE-COMBAT half of that seam: adjustments derived once
|
||||
// from run state and folded into the boss's live Combatant before the fight
|
||||
// resolves. It must be a PURE, IDEMPOTENT function of run state, because the
|
||||
// turn engine rebuilds the enemy from the bestiary every single round
|
||||
// (partyCombatantsForSession) — the boss's numbers are re-derived on every
|
||||
// !attack. That is fine as long as the inputs are frozen for the duration of
|
||||
// the fight, which they are: the boss room is terminal, so no more nodes are
|
||||
// walked once the fight begins.
|
||||
//
|
||||
// In-combat Layer-2 hooks (Inversion Stitch, Amendment, Two Hearts) are a
|
||||
// separate seam — a new MonsterAbility.Effect case in applyAbility, shared by
|
||||
// both engines — and are not in this file.
|
||||
|
||||
// applyBossRunModifiers folds any pre-combat Layer-2 mechanic for bossID into
|
||||
// the freshly-built enemy Combatant, reading the run the player took to get
|
||||
// here. It is dispatched by bestiary ID and is a no-op for every enemy that
|
||||
// isn't a hooked T6 boss, so the two build seams can call it unconditionally on
|
||||
// whatever they just built. A nil enemy or nil run is a no-op.
|
||||
func applyBossRunModifiers(bossID string, enemy *Combatant, run *DungeonRun) {
|
||||
if enemy == nil || run == nil {
|
||||
return
|
||||
}
|
||||
switch bossID {
|
||||
case "boss_aurvandryx":
|
||||
applyGreedTax(enemy, run)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Greed Tax — Aurvandryx, the Ember Before Fire (First Hoard) ─────────────
|
||||
//
|
||||
// The First Hoard carries the game's richest LootBias nodes on purpose (the
|
||||
// gilded veins at 2.5–3.0; nowhere else in the game exceeds 1.0). Aurvandryx —
|
||||
// the hoard's true owner, not its guard dog — takes the interest out of your
|
||||
// hide: her Attack rises with how much of the gilded route you walked to reach
|
||||
// her cradle. Strip the veins and meet a furious wyrm; take the vow-of-poverty
|
||||
// line through the zone and fight her lean.
|
||||
//
|
||||
// Signal: the summed excess LootBias (above the 1.0 baseline) of every node the
|
||||
// run walked — NOT run.LootCollected, which only tracks the boss-only signature
|
||||
// manifest and is empty at the boss. A full-explore route through the First
|
||||
// Hoard accrues ~3.5 richness (+7 Attack); the cap covers a maximal line.
|
||||
//
|
||||
// Calibration (P8 sim sweep, L20 party+Pete+pets): the full-explore route lands
|
||||
// first_hoard at ~47% clear (mid-band 40–55), down from the taxless 56%. A lean
|
||||
// prod route pays no tax and fights her at the taxless rate; a maximal route
|
||||
// hits the cap. No base-stat retune was needed — the tax corrects the prior
|
||||
// slight overshoot into the band. NOTE: the sim's autopilot explores the whole
|
||||
// graph, so the sweep only exercises the full-route point; the lean/greedy
|
||||
// spread is a prod-only player-agency lever the headless sim cannot walk.
|
||||
const (
|
||||
// greedTaxRichnessMult converts summed route richness into Attack points.
|
||||
greedTaxRichnessMult = 2.0
|
||||
|
||||
// greedTaxMaxAttack caps the tax so a maximal hoard-run meets a very hard
|
||||
// wyrm, never a literally-unbeatable one.
|
||||
greedTaxMaxAttack = 12
|
||||
)
|
||||
|
||||
// greedRouteRichness sums the excess LootBias (above the 1.0 baseline) of every
|
||||
// node the run has walked. Extracted for a deterministic unit test.
|
||||
func greedRouteRichness(run *DungeonRun) float64 {
|
||||
g, ok := loadZoneGraph(run.ZoneID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
var rich float64
|
||||
for _, id := range run.VisitedNodes {
|
||||
if n, exists := g.Nodes[id]; exists && n.Content.LootBias > 1.0 {
|
||||
rich += n.Content.LootBias - 1.0
|
||||
}
|
||||
}
|
||||
return rich
|
||||
}
|
||||
|
||||
// greedTaxAttack is the Attack surcharge for a route of the given richness.
|
||||
func greedTaxAttack(richness float64) int {
|
||||
tax := int(richness * greedTaxRichnessMult)
|
||||
if tax > greedTaxMaxAttack {
|
||||
tax = greedTaxMaxAttack
|
||||
}
|
||||
if tax < 0 {
|
||||
tax = 0
|
||||
}
|
||||
return tax
|
||||
}
|
||||
|
||||
func applyGreedTax(enemy *Combatant, run *DungeonRun) {
|
||||
enemy.Stats.Attack += greedTaxAttack(greedRouteRichness(run))
|
||||
}
|
||||
Reference in New Issue
Block a user