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:
prosolis
2026-05-09 22:17:14 -07:00
parent de02907e69
commit c5217e9ecf
5 changed files with 67 additions and 18 deletions

View File

@@ -12,7 +12,10 @@ func TestApplyDnDHPScaling_FullHP(t *testing.T) {
c := &DnDCharacter{HPMax: 50, HPCurrent: 50}
applyDnDHPScaling(&stats, c)
if stats.MaxHP != 100 {
t.Errorf("full HP: MaxHP scaled to %d, want unchanged 100", stats.MaxHP)
t.Errorf("full HP: MaxHP changed to %d, want unchanged 100", stats.MaxHP)
}
if stats.StartHP != 0 {
t.Errorf("full HP: StartHP = %d, want 0 (unset)", stats.StartHP)
}
}
@@ -20,8 +23,11 @@ func TestApplyDnDHPScaling_HalfHP(t *testing.T) {
stats := CombatStats{MaxHP: 100}
c := &DnDCharacter{HPMax: 50, HPCurrent: 25}
applyDnDHPScaling(&stats, c)
if stats.MaxHP != 50 {
t.Errorf("50%% HP: MaxHP = %d, want 50", stats.MaxHP)
if stats.MaxHP != 100 {
t.Errorf("50%% HP: MaxHP = %d, want unchanged 100", stats.MaxHP)
}
if stats.StartHP != 50 {
t.Errorf("50%% HP: StartHP = %d, want 50", stats.StartHP)
}
}
@@ -29,21 +35,24 @@ func TestApplyDnDHPScaling_FloorAt25Pct(t *testing.T) {
stats := CombatStats{MaxHP: 100}
c := &DnDCharacter{HPMax: 50, HPCurrent: 0}
applyDnDHPScaling(&stats, c)
if stats.MaxHP != 25 {
t.Errorf("0%% HP: MaxHP = %d, want 25 (floor)", stats.MaxHP)
if stats.MaxHP != 100 {
t.Errorf("0%% HP: MaxHP = %d, want unchanged 100", stats.MaxHP)
}
if stats.StartHP != 25 {
t.Errorf("0%% HP: StartHP = %d, want 25 (floor)", stats.StartHP)
}
}
func TestApplyDnDHPScaling_NoOpIfNilOrZero(t *testing.T) {
stats := CombatStats{MaxHP: 100}
applyDnDHPScaling(&stats, nil)
if stats.MaxHP != 100 {
t.Errorf("nil char: scaled to %d, want unchanged", stats.MaxHP)
if stats.MaxHP != 100 || stats.StartHP != 0 {
t.Errorf("nil char: MaxHP=%d StartHP=%d, want 100/0", stats.MaxHP, stats.StartHP)
}
c := &DnDCharacter{HPMax: 0}
applyDnDHPScaling(&stats, c)
if stats.MaxHP != 100 {
t.Errorf("zero HPMax: scaled to %d, want unchanged", stats.MaxHP)
if stats.MaxHP != 100 || stats.StartHP != 0 {
t.Errorf("zero HPMax: MaxHP=%d StartHP=%d, want 100/0", stats.MaxHP, stats.StartHP)
}
}