Files
gogobee/internal/plugin/combat_narrative_test.go
prosolis 3b4dfa44d3 Adv 2.0 L2: rip legacy arena combat path; boss flow is the only path
Cancels the planned ARENA_BOSS_FLOW soak. The boss flow is shipped
unconditionally with no legacy fallback — boss-flow errors now surface
to the player and abort the round rather than silently falling back to
the old CombatPower path.

Deleted:
- runArenaCombat (combat_bridge.go) — legacy CombatPower-vs-Lethality
  arena combat driver.
- RenderCombatLogArena, renderArenaOutcome (combat_narrative.go) — legacy
  arena renderers. RenderCombatLogArena was a thin alias for
  RenderCombatLog; renderArenaOutcome was already dead.
- renderArenaCombatFinalMessage (combat_bridge.go) — legacy "rewards +
  closer" trailing text.
- arenaWinCloser, arenaLoseCloser (adventure_arena_combat.go) — flavor
  helpers that only fed the deleted renderers. File now just holds
  arenaParticipationXP.
- arenaBossFlowEnabled + the ARENA_BOSS_FLOW env var (adventure_arena.go,
  .env.example). The env gate served only the now-removed fallback.
- sendArenaCombatMessages (adventure_arena.go) — wrapper that switched
  pacing between boss flow and legacy. Replaced with direct
  sendZoneCombatMessages calls at the three call sites.
- TestArenaBossFlowEnabled (bossflow test) and
  TestRenderCombatLogArena_ProducesPhaseMessages (narrative test).

Simplified resolveArenaRound / resolveArenaSurvival / resolveArenaDeath:
the bossNarr-vs-nil branches collapsed since narration is now always
produced by resolveArenaBoss. Equipment degradation and the death-save
reprieve hook are unchanged — both already worked off the boss-flow
CombatResult.

Migration doc (§4) updated to record the cancellation and the
remaining tuning approach (playtest-driven, no flag soak window).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00

172 lines
5.0 KiB
Go

package plugin
import (
"strings"
"testing"
)
func TestRenderCombatLog_ProducesMessages(t *testing.T) {
player := Combatant{
Name: "Hero",
Stats: CombatStats{MaxHP: 100, Attack: 20, Defense: 10, Speed: 8, CritRate: 0.05},
Mods: CombatModifiers{DamageReduct: 1.0},
IsPlayer: true,
}
enemy := Combatant{
Name: "Goblin",
Stats: CombatStats{MaxHP: 60, Attack: 12, Defense: 5, Speed: 6, CritRate: 0.03},
Mods: CombatModifiers{DamageReduct: 1.0},
}
result := SimulateCombat(player, enemy, defaultCombatPhases)
msgs := RenderCombatLog(result, "Hero", "Goblin")
if len(msgs) < 1 {
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
}
}
func TestRenderCombatLog_PhaseHeaders(t *testing.T) {
player := Combatant{
Name: "Hero",
Stats: CombatStats{MaxHP: 100, Attack: 20, Defense: 10, Speed: 8, CritRate: 0.05},
Mods: CombatModifiers{DamageReduct: 1.0},
IsPlayer: true,
}
enemy := Combatant{
Name: "Rat",
Stats: CombatStats{MaxHP: 40, Attack: 8, Defense: 3, Speed: 4},
Mods: CombatModifiers{DamageReduct: 1.0},
}
result := SimulateCombat(player, enemy, defaultCombatPhases)
msgs := RenderCombatLog(result, "Hero", "Rat")
hasOpening := false
for _, m := range msgs {
if strings.Contains(m, "Opening") {
hasOpening = true
}
}
if !hasOpening {
t.Error("should have an Opening phase header")
}
}
func TestRenderCombatLog_WinPhaseMessages(t *testing.T) {
result := CombatResult{
PlayerWon: true,
PlayerStartHP: 100,
EnemyStartHP: 50,
PlayerEndHP: 80,
EnemyEndHP: 0,
TotalRounds: 3,
Closeness: 0.2,
Events: []CombatEvent{
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 25, EnemyHP: 25, PlayerHP: 100},
{Round: 2, Phase: "clash", Actor: "player", Action: "hit", Damage: 25, EnemyHP: 0, PlayerHP: 80},
},
}
msgs := RenderCombatLog(result, "Hero", "Slime")
if len(msgs) < 1 {
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
}
// Phase messages should contain damage numbers
found := false
for _, m := range msgs {
if strings.Contains(m, "25") {
found = true
}
}
if !found {
t.Error("phase messages should contain damage values")
}
}
func TestRenderCombatLog_LossPhaseMessages(t *testing.T) {
result := CombatResult{
PlayerWon: false,
PlayerStartHP: 100,
EnemyStartHP: 80,
PlayerEndHP: 0,
EnemyEndHP: 40,
TotalRounds: 4,
Closeness: 0.5,
Events: []CombatEvent{
{Round: 1, Phase: "opening", Actor: "enemy", Action: "hit", Damage: 50, PlayerHP: 50, EnemyHP: 80},
{Round: 2, Phase: "clash", Actor: "enemy", Action: "hit", Damage: 50, PlayerHP: 0, EnemyHP: 80},
},
}
msgs := RenderCombatLog(result, "Hero", "Dragon")
if len(msgs) < 1 {
t.Fatalf("expected at least 1 phase message, got %d", len(msgs))
}
}
func TestRenderCombatLog_ConsumableEvents(t *testing.T) {
result := CombatResult{
PlayerWon: true,
PlayerStartHP: 100,
EnemyStartHP: 60,
PlayerEndHP: 80,
EnemyEndHP: 0,
Events: []CombatEvent{
{Round: 0, Phase: "pre_combat", Actor: "consumable", Action: "use_consumable", Desc: "Goblin Grease"},
{Round: 0, Phase: "pre_combat", Actor: "consumable", Action: "flat_damage", Damage: 8, PlayerHP: 100, EnemyHP: 52},
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 30, PlayerHP: 100, EnemyHP: 22},
{Round: 2, Phase: "clash", Actor: "player", Action: "hit", Damage: 22, PlayerHP: 80, EnemyHP: 0},
},
}
msgs := RenderCombatLog(result, "Hero", "Imp")
found := false
for _, m := range msgs {
if strings.Contains(m, "Goblin Grease") {
found = true
}
}
if !found {
t.Error("should render consumable use event mentioning item name")
}
}
func TestRenderCombatLog_MonsterAbilityEvents(t *testing.T) {
result := CombatResult{
PlayerWon: true,
PlayerStartHP: 100,
EnemyStartHP: 60,
PlayerEndHP: 50,
EnemyEndHP: 0,
Events: []CombatEvent{
{Round: 1, Phase: "opening", Actor: "enemy", Action: "armor_break", Damage: 0, PlayerHP: 100, EnemyHP: 60},
{Round: 1, Phase: "opening", Actor: "player", Action: "hit", Damage: 30, PlayerHP: 100, EnemyHP: 30},
{Round: 2, Phase: "clash", Actor: "enemy", Action: "hit", Damage: 25, PlayerHP: 75, EnemyHP: 30},
{Round: 2, Phase: "clash", Actor: "enemy", Action: "poison_tick", Damage: 5, PlayerHP: 70, EnemyHP: 30},
{Round: 3, Phase: "decisive", Actor: "player", Action: "crit", Damage: 30, PlayerHP: 50, EnemyHP: 0},
},
}
msgs := RenderCombatLog(result, "Hero", "Wyrm")
hasArmorBreak := false
hasPoison := false
for _, m := range msgs {
if strings.Contains(m, "armor") || strings.Contains(m, "Armor") || strings.Contains(m, "defense") || strings.Contains(m, "Defense") {
hasArmorBreak = true
}
if strings.Contains(m, "poison") || strings.Contains(m, "Poison") || strings.Contains(m, "toxin") || strings.Contains(m, "Venom") || strings.Contains(m, "venom") {
hasPoison = true
}
}
if !hasArmorBreak {
t.Error("should render armor_break event")
}
if !hasPoison {
t.Error("should render poison_tick event")
}
}