diff --git a/internal/plugin/adventure_arena_bossflow_test.go b/internal/plugin/adventure_arena_bossflow_test.go index dc757d4..bb94b15 100644 --- a/internal/plugin/adventure_arena_bossflow_test.go +++ b/internal/plugin/adventure_arena_bossflow_test.go @@ -135,6 +135,67 @@ func TestBossFlowPhaseMessages(t *testing.T) { } } +// Phase L2 step 8 — assert that an arena win surfaces both the staged +// combat log (RenderCombatLog phases prepended by the arena intro) and a +// TwinBee BossDeath flavor line in the outcome block. Drives +// renderBossOutcome directly with a forced-win CombatResult so the +// assertion is RNG-free; the resolveArenaBoss smoke test above already +// exercises the live combat path end-to-end. +func TestArenaBossOutcome_WinSurfacesStagedLogAndBossDeath(t *testing.T) { + monster := arenaBosses[arenaBossID(1, 1)] + result := CombatResult{ + PlayerWon: true, + PlayerStartHP: 60, PlayerEndHP: 42, + EnemyStartHP: monster.HP, EnemyEndHP: 0, + Events: []CombatEvent{ + {Round: 1, Phase: "attack", Actor: "player", Action: "hit", Roll: 14, RollAgainst: monster.AC, Damage: 10, EnemyHP: monster.HP - 10, PlayerHP: 60}, + {Round: 2, Phase: "attack", Actor: "player", Action: "crit", Roll: 20, RollAgainst: monster.AC, Damage: monster.HP - 10, EnemyHP: 0, PlayerHP: 42}, + }, + TotalRounds: 2, + } + + // Staged combat log — same call resolveArenaBoss makes for `phases`. + phases := RenderCombatLog(result, "Champion", monster.Name) + if len(phases) == 0 { + t.Fatal("RenderCombatLog returned no phases for a winning result") + } + + victoryHeadline := "🏆 **" + monster.Name + "** falls (HP " + "60" + "→" + "42" + " / 60)." + outcome := renderBossOutcome(BossOutcomeInputs{ + ZoneID: ZoneArena, + RunID: "arena-stagedlog-test", + RoomIdx: 11, + Monster: monster, + Result: result, + PreHP: 60, PostHP: 42, MaxHP: 60, + PhaseTwoAt: arenaBossPhaseTwoAt(1), + Nat20s: 1, + Nat1s: 0, + DefeatHeadline: "unused", + VictoryHeadline: victoryHeadline, + }) + + if !strings.Contains(outcome, "🎭 **TwinBee:**") { + t.Errorf("outcome missing TwinBee narration line: %q", outcome) + } + if !strings.Contains(outcome, victoryHeadline) { + t.Errorf("outcome missing victory headline: %q", outcome) + } + if !strings.Contains(outcome, "🎲 d20 —") { + t.Errorf("outcome missing dice-roll summary: %q", outcome) + } + // BossDeath line must be present — the TwinBee line preceding the + // victory headline is sourced from flavor.BossDeath via twinBeeLine. + headlineIdx := strings.Index(outcome, victoryHeadline) + if headlineIdx <= 0 { + t.Fatalf("victory headline not after a TwinBee line: %q", outcome) + } + prefix := outcome[:headlineIdx] + if !strings.Contains(prefix, "🎭 **TwinBee:**") { + t.Errorf("BossDeath TwinBee line should precede victory headline; prefix=%q", prefix) + } +} + func TestArenaBosses_AllTiersPopulated(t *testing.T) { for tier := 1; tier <= 5; tier++ { for round := 1; round <= 4; round++ { diff --git a/internal/plugin/adventure_arena_test.go b/internal/plugin/adventure_arena_test.go index 2102bac..8266c6b 100644 --- a/internal/plugin/adventure_arena_test.go +++ b/internal/plugin/adventure_arena_test.go @@ -458,7 +458,7 @@ func TestArenaGearDropRates_Decreasing(t *testing.T) { // ── Render Tests (New/Updated) ───────────────────────────────────────────── func TestRenderArenaStreakEntry_ShowsMultipliers(t *testing.T) { - char := &DnDCharacter{Level: 50} + char := &DnDCharacter{Level: 20} tier := arenaGetTier(1) monster := arenaGetMonster(1, 1) @@ -491,9 +491,9 @@ func TestRenderArenaStreakEntry_HighLevel_AllEligible(t *testing.T) { monster := arenaGetMonster(1, 1) text := renderArenaStreakEntry(char, nil, tier, monster) - // Level 70 should make all tiers eligible (no 🔒) + // Level 20 (D&D cap) should make all tiers eligible (no 🔒) if strings.Contains(text, "🔒") { - t.Error("level 70 should have all tiers eligible (no locked icons)") + t.Error("level 20 should have all tiers eligible (no locked icons)") } } @@ -753,7 +753,7 @@ func TestArenaStreakSimulation_FullRun(t *testing.T) { TierEarnings: 0, XPAccumulated: 0, } - combatLevel := 30 + skillBonus := 20 for tierNum := 1; tierNum <= 5; tierNum++ { tier := arenaGetTier(tierNum) @@ -761,7 +761,7 @@ func TestArenaStreakSimulation_FullRun(t *testing.T) { for round := 1; round <= 4; round++ { run.Round = round - reward := arenaRoundReward(tier, round, combatLevel) + reward := arenaRoundReward(tier, round, skillBonus) run.TierEarnings += reward run.XPAccumulated += tier.BattleXP run.RoundsSurvived++ @@ -791,7 +791,7 @@ func TestArenaStreakSimulation_FullRun(t *testing.T) { for tierNum := 1; tierNum <= 5; tierNum++ { tier := arenaGetTier(tierNum) for round := 1; round <= 4; round++ { - rawTotal += arenaRoundReward(tier, round, combatLevel) + rawTotal += arenaRoundReward(tier, round, skillBonus) } rawTotal += tier.CompletionBonus }