mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
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
99 lines
4.2 KiB
Go
99 lines
4.2 KiB
Go
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))
|
||
}
|