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:
prosolis
2026-07-16 07:44:10 -07:00
parent 85e5ba5fce
commit fc9e055083
7 changed files with 214 additions and 10 deletions

View File

@@ -109,7 +109,7 @@ func TestBuildFightSeats_SoloSeatsExactlyThePlayer(t *testing.T) {
fightTestChar(t, solo, 30)
seats, enemy, skip, refusal := (&AdventurePlugin{}).buildFightSeats(
solo, []id.UserID{solo}, dndBestiary["goblin"], 1, 0)
solo, []id.UserID{solo}, dndBestiary["goblin"], 1, 0, nil)
if refusal != "" || skip != "" {
t.Fatalf("solo fight refused: %s / %s", refusal, skip)
}
@@ -140,7 +140,7 @@ func TestBuildFightSeats_DownedMemberSitsOut(t *testing.T) {
roster := []id.UserID{leader, downed, standing}
seats, _, skip, refusal := (&AdventurePlugin{}).buildFightSeats(
leader, roster, dndBestiary["goblin"], 1, 0)
leader, roster, dndBestiary["goblin"], 1, 0, nil)
if refusal != "" {
t.Fatalf("party refused over a downed member: %s", refusal)
}
@@ -156,7 +156,7 @@ func TestBuildFightSeats_DownedMemberSitsOut(t *testing.T) {
// The one who was left behind typed `!fight` too, and silence is not an answer.
_, _, skip, refusal = (&AdventurePlugin{}).buildFightSeats(
downed, roster, dndBestiary["goblin"], 1, 0)
downed, roster, dndBestiary["goblin"], 1, 0, nil)
if refusal != "" {
t.Fatalf("a downed member must not refuse the party's fight: %s", refusal)
}
@@ -176,7 +176,7 @@ func TestBuildFightSeats_DownedLeaderRefusesTheFightForEveryone(t *testing.T) {
roster := []id.UserID{leader, member}
p := &AdventurePlugin{}
seats, _, _, refusal := p.buildFightSeats(leader, roster, dndBestiary["goblin"], 1, 0)
seats, _, _, refusal := p.buildFightSeats(leader, roster, dndBestiary["goblin"], 1, 0, nil)
if len(seats) != 0 || refusal == "" {
t.Fatalf("downed leader seated %d players, refusal %q", len(seats), refusal)
}
@@ -184,7 +184,7 @@ func TestBuildFightSeats_DownedLeaderRefusesTheFightForEveryone(t *testing.T) {
t.Errorf("the leader should be told to rest, got %q", refusal)
}
_, _, _, refusal = p.buildFightSeats(member, roster, dndBestiary["goblin"], 1, 0)
_, _, _, refusal = p.buildFightSeats(member, roster, dndBestiary["goblin"], 1, 0, nil)
if !strings.Contains(refusal, "leader") {
t.Errorf("the member should be told it is the leader holding things up, got %q", refusal)
}