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>
This commit is contained in:
prosolis
2026-05-09 09:06:38 -07:00
parent 85a80e5c40
commit 3b4dfa44d3
9 changed files with 34 additions and 339 deletions

View File

@@ -10,99 +10,6 @@ import (
"maunium.net/go/mautrix/id"
)
// runArenaCombat executes arena combat using the new simulation engine.
// Returns the CombatResult and the post-combat Misty condition repair amount (if any).
func (p *AdventurePlugin) runArenaCombat(
userID id.UserID,
char *AdventureCharacter,
equip map[EquipmentSlot]*AdvEquipment,
monster *ArenaMonster,
arenaRound int,
arenaTier int,
) (CombatResult, int) {
bonuses := p.loadCombatBonuses(userID, char)
chatLvl := p.chatLevel(userID)
hasGrudge := char.GrudgeLocation != ""
playerStats, playerMods := DerivePlayerStats(char, equip, bonuses, chatLvl, char.CurrentStreak, hasGrudge)
// Load consumables from inventory and auto-select
consumables := p.loadConsumableInventory(userID)
enemyStats, _ := DeriveArenaMonsterStats(monster)
// All combat is D&D. If the player has no sheet yet (or only a draft),
// auto-migrate them to a sensible inferred character before fighting.
dndChar, freshMigrate, err := ensureDnDCharacterForCombat(userID, char)
if err != nil {
slog.Error("dnd: ensureDnDCharacterForCombat (arena) failed", "user", userID, "err", err)
return CombatResult{}, 0
}
if freshMigrate {
p.maybeSendDnDOnboarding(userID, char, dndChar)
}
applyDnDPlayerLayer(&playerStats, dndChar)
applyDnDEquipmentLayer(&playerStats, dndChar, equip)
applyDnDHPScaling(&playerStats, dndChar)
applyClassPassives(&playerStats, &playerMods, dndChar)
applyRacePassives(&playerStats, &playerMods, dndChar)
applySubclassPassives(&playerStats, &playerMods, dndChar)
if firedName, fired := applyArmedAbility(dndChar, &playerMods); fired {
slog.Info("dnd: armed ability fired", "user", userID, "ability", firedName)
}
applyDnDArenaMonsterLayer(&enemyStats, monster.ThreatLevel)
applyPendingCast(userID, dndChar, &playerStats, &playerMods, &enemyStats)
selected := SelectConsumables(consumables, playerStats, enemyStats, arenaRound, arenaTier)
ApplyConsumableMods(&playerStats, &playerMods, selected)
// Misty condition repair (post-combat, not part of engine)
condRepair := 0
now := time.Now().UTC()
if char.MistyBuffExpires != nil && now.Before(*char.MistyBuffExpires) {
if rand.Float64() < 0.20 {
condRepair = 5
}
}
player := Combatant{
Name: char.DisplayName,
Stats: playerStats,
Mods: playerMods,
IsPlayer: true,
}
enemy := Combatant{
Name: monster.Name,
Stats: enemyStats,
Mods: CombatModifiers{DamageReduct: 1.0},
Ability: monster.Ability,
}
result := SimulateCombat(player, enemy, defaultCombatPhases)
result = injectConsumableEvents(result, selected, len(consumables))
// Consume used items from inventory
for _, c := range selected {
if err := removeAdvInventoryItem(c.InventoryID); err != nil {
slog.Error("combat: failed to consume item", "id", c.InventoryID, "name", c.Def.Name, "err", err)
}
}
p.grantCombatAchievements(userID, result)
persistDnDHPAfterCombat(userID, result.PlayerStartHP, result.PlayerEndHP)
if err := persistDnDPostCombatSubclass(dndChar, playerMods.BerserkerRage, result, playerMods); err != nil {
slog.Error("dnd: post-combat subclass persist (arena)", "user", userID, "err", err)
}
if xp := arenaCombatXP(result, monster.ThreatLevel); xp > 0 {
if _, err := p.grantDnDXP(userID, xp); err != nil {
slog.Error("dnd: grantDnDXP arena", "user", userID, "err", err)
}
}
return result, condRepair
}
// grantCombatAchievements checks combat results for achievement-worthy moments.
func (p *AdventurePlugin) grantCombatAchievements(userID id.UserID, result CombatResult) {
if p.achievements == nil {
@@ -658,15 +565,3 @@ func (p *AdventurePlugin) resolveDungeonAction(
return result
}
// renderArenaCombatFinalMessage builds the post-combat text (rewards, tier progress, etc.)
// This is sent as the last message after the phase-by-phase combat messages.
func renderArenaCombatFinalMessage(result CombatResult, monster *ArenaMonster, reward int64, battleXP int, round int) string {
if result.PlayerWon {
closer := arenaWinCloser(monster.Name, round)
return fmt.Sprintf("💀 **%s** has been defeated.\n%s\n🏆 +%d XP | €%d earned",
monster.Name, closer, battleXP, reward)
}
closer := arenaLoseCloser(monster.Name, round)
return fmt.Sprintf("The healers are already moving.\n💀 **Defeated.**\n%s\n+%d XP (participation) | Back tomorrow.",
closer, arenaParticipationXP)
}