mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Combat: fix wounded-carry HP display + auto-crit narration
Two bugs reported back-to-back from a Rogue's run: 1. HP display reset between battles. End of fight 1: 101/123 → start of fight 2: 100/100. applyDnDHPScaling was overwriting Stats.MaxHP with the scaled wound value, so the display denominator dropped along with the numerator. Wounded carry-over became invisible. Fix: introduce CombatStats.StartHP. Combat engine reads it as the entry-HP when set; MaxHP stays put. Display now reads "100/123" as intended. 2. Auto-crit fired on a roll of 11 with no narrative tell. Rogue passive AutoCritFirst was triggering correctly, but the renderer used the generic crit pool, so the player saw "🎲 11 vs AC 10 → CRIT" with no indication their *class* caused it. Fix: tag the crit event with Desc="auto_crit" when the passive (not the dice) caused it; new narrativePlayerAutoCrit pool calls out the training/instinct/exploit theme. Test bound for T5 dungeon death rate loosened from 0.02 to 0.01 — the new Sudden Death phase from the previous commit shifted geared-T5 fights slightly toward player wins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,10 @@ import "math/rand/v2"
|
||||
|
||||
type CombatStats struct {
|
||||
MaxHP int
|
||||
// StartHP is the HP the combatant *enters* this fight at. Zero means
|
||||
// "use MaxHP" (full health). Wounded carry-over uses this so MaxHP
|
||||
// stays stable across fights — display reads "100/123", not "100/100".
|
||||
StartHP int
|
||||
Attack int
|
||||
Defense int
|
||||
Speed int
|
||||
@@ -237,9 +241,17 @@ type combatState struct {
|
||||
}
|
||||
|
||||
func SimulateCombat(player, enemy Combatant, phases []CombatPhase) CombatResult {
|
||||
playerStart := player.Stats.MaxHP
|
||||
if player.Stats.StartHP > 0 && player.Stats.StartHP < player.Stats.MaxHP {
|
||||
playerStart = player.Stats.StartHP
|
||||
}
|
||||
enemyStart := enemy.Stats.MaxHP
|
||||
if enemy.Stats.StartHP > 0 && enemy.Stats.StartHP < enemy.Stats.MaxHP {
|
||||
enemyStart = enemy.Stats.StartHP
|
||||
}
|
||||
st := &combatState{
|
||||
playerHP: player.Stats.MaxHP,
|
||||
enemyHP: enemy.Stats.MaxHP,
|
||||
playerHP: playerStart,
|
||||
enemyHP: enemyStart,
|
||||
wardCharges: player.Mods.WardCharges,
|
||||
sporeRounds: player.Mods.SporeCloud,
|
||||
reflectFrac: player.Mods.ReflectNext,
|
||||
@@ -607,8 +619,14 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
}
|
||||
|
||||
isCrit := isCritRoll
|
||||
if st.autoCrit {
|
||||
autoCritFired := false
|
||||
if !isCritRoll && st.autoCrit {
|
||||
isCrit = true
|
||||
autoCritFired = true
|
||||
st.autoCrit = false
|
||||
} else if st.autoCrit && isCritRoll {
|
||||
// Natural crit consumes the auto-crit charge but doesn't get the
|
||||
// "passive fired" flavor — the dice already explain it.
|
||||
st.autoCrit = false
|
||||
}
|
||||
if isCrit {
|
||||
@@ -651,8 +669,12 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
dmg = max(1, dmg)
|
||||
|
||||
action := "hit"
|
||||
desc := ""
|
||||
if isCrit {
|
||||
action = "crit"
|
||||
if autoCritFired {
|
||||
desc = "auto_crit"
|
||||
}
|
||||
} else if blocked {
|
||||
action = "block"
|
||||
}
|
||||
@@ -661,7 +683,7 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
st.events = append(st.events, CombatEvent{
|
||||
Round: st.round, Phase: phaseName, Actor: "player", Action: action,
|
||||
Damage: dmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
|
||||
Roll: roll, RollAgainst: enemy.Stats.AC,
|
||||
Roll: roll, RollAgainst: enemy.Stats.AC, Desc: desc,
|
||||
})
|
||||
return st.enemyHP <= 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user