mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user