From de02907e693b0f2266ea758267ff8e3c6d84dbb0 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 9 May 2026 21:58:26 -0700 Subject: [PATCH] Combat: add Sudden Death phase + fix tiebreak to use absolute HP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Players reported the prior 6-round exhaustion timeout felt arbitrary when both sides still had plenty of HP. Two fixes: - Add a "Sudden Death" 4th phase (3-4 extra rounds) so most fights resolve naturally before any timeout. Total combat length now caps at ~10 rounds. - When the timeout does fire, tiebreak by absolute HP instead of HP%. The %-based logic was unintuitive — a player at 88/123 (71%) would lose to a boss at 83/97 (86%) despite having more HP remaining. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/combat_engine.go | 17 +++++++++++------ internal/plugin/combat_narrative.go | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/plugin/combat_engine.go b/internal/plugin/combat_engine.go index d2a63bc..b0ffc68 100644 --- a/internal/plugin/combat_engine.go +++ b/internal/plugin/combat_engine.go @@ -171,16 +171,23 @@ type MonsterAbility struct { // ── Default Phase Definitions ──────────────────────────────────────────────── +// Sudden Death is a fallback phase that runs only when none of the earlier +// phases produced a kill. Adds enough rounds to push total combat length +// to ~10 in the worst case, so the absolute-HP tiebreaker below is rarely +// hit. Players reported the prior 6-round insta-timeout felt arbitrary +// when both sides still had plenty of HP. var defaultCombatPhases = []CombatPhase{ {"Opening", 2, 0.6, 0.8, 1.5, 0.15}, {"Clash", 3, 1.2, 1.0, 0.8, 0.08}, {"Decisive", 1, 1.0, 0.7, 1.0, 0.05}, + {"Sudden Death", 3, 1.1, 0.6, 1.0, 0.05}, } var dungeonCombatPhases = []CombatPhase{ {"Opening", 1, 0.8, 0.8, 1.2, 0.10}, {"Clash", 2, 1.0, 1.0, 0.8, 0.08}, {"Decisive", 1, 1.0, 0.7, 1.0, 0.05}, + {"Sudden Death", 4, 1.1, 0.6, 1.0, 0.05}, } // ── Simulation ─────────────────────────────────────────────────────────────── @@ -305,12 +312,10 @@ func SimulateCombat(player, enemy Combatant, phases []CombatPhase) CombatResult } } - // If we exhaust all phases without a kill, the side with more HP% wins. - playerMax := max(1, player.Stats.MaxHP) - enemyMax := max(1, enemy.Stats.MaxHP) - playerPct := float64(st.playerHP) / float64(playerMax) - enemyPct := float64(st.enemyHP) / float64(enemyMax) - if playerPct < enemyPct { + // If we exhaust all phases without a kill, tiebreak by absolute HP. + // Was %-based, which produced unintuitive outcomes (e.g. 88/123 player + // losing to 83/97 boss because the boss had a higher percentage). + if st.playerHP < st.enemyHP { st.playerHP = 0 } else { st.enemyHP = 0 diff --git a/internal/plugin/combat_narrative.go b/internal/plugin/combat_narrative.go index 34619dc..aad3b49 100644 --- a/internal/plugin/combat_narrative.go +++ b/internal/plugin/combat_narrative.go @@ -153,6 +153,8 @@ func phaseHeader(name string) string { return pickRand(decisiveHeaders) case "pre_combat", "pre": return "⚔️ **The fight begins.**" + case "Sudden Death", "sudden_death": + return "⚔️ **Sudden Death — No Quarter**" case "exhaust": return "⚔️ **Exhaustion — Time Runs Out**" default: