fix(rules): undead and constructs are destroyed at hp <= 0

SRD Undead/Construct traits: 'immediately destroyed when reduced to 0 hit
points or fewer' — undead and constructs have no Constitution score, so
the PF1e hp <= -CON death rule does not apply to them.

- combat.py: CombatantState.death_threshold returns 0 for creature_type
  undead/construct (in addition to the opt-in dies_at_zero flag)
- tests: TestUndeadDeathThreshold pins undead dead at 0 and below, living
  keeps the disabled band at 0 and the dying band down to -CON
- golden unchanged: tammara-vs-lich has the lich winning in 1 round
  without taking damage
This commit is contained in:
2026-09-16 11:45:23 +02:00
parent 5e3505dc8c
commit 59caf67909
2 changed files with 42 additions and 1 deletions
+5 -1
View File
@@ -769,9 +769,13 @@ class CombatantState:
@property @property
def death_threshold(self) -> int: def death_threshold(self) -> int:
"""PF1e (CRB): death when negative hp reaches the CON score (no -10 rule). """PF1e (CRB): death when negative hp reaches the CON score (no -10 rule).
The optional simple monster-death rule raises the threshold to 0.""" Undead and constructs have no Constitution score and are "immediately
destroyed when reduced to 0 hit points or fewer" (SRD Undead/Construct
traits). The optional simple monster-death rule raises the threshold to 0."""
if self.dies_at_zero: if self.dies_at_zero:
return 0 return 0
if self.combatant.creature_type in ("undead", "construct"):
return 0
return -self.combatant.abilities.con_score return -self.combatant.abilities.con_score
+37
View File
@@ -34,6 +34,7 @@ def _make_lich() -> Combatant:
name="Lich", name="Lich",
level=11, level=11,
size="Medium", size="Medium",
creature_type="undead",
abilities=AbilityScores( abilities=AbilityScores(
str_score=12, dex_score=14, con_score=14, str_score=12, dex_score=14, con_score=14,
int_score=20, wis_score=14, cha_score=16, int_score=20, wis_score=14, cha_score=16,
@@ -173,6 +174,42 @@ class TestFearAura:
assert all(ac.condition.name != "shaken" for ac in foe_state.active_conditions) assert all(ac.condition.name != "shaken" for ac in foe_state.active_conditions)
# ── Undead death threshold ────────────────────────────────────────────────────
class TestUndeadDeathThreshold:
"""Given: an undead creature (no CON score, SRD Undead traits)
When: its hp reaches 0 or below
Then: it is immediately destroyed (dead), unlike a living creature
which keeps the disabled band at 0 and the dying band down to -CON."""
def test_undead_dead_at_zero(self) -> None:
lich = _make_lich()
state = _make_state(lich, "monsters", (1, 1))
state.hp = 0
assert state.dead is True
assert state.active is False
def test_undead_dead_below_zero(self) -> None:
lich = _make_lich()
state = _make_state(lich, "monsters", (1, 1))
state.hp = -1
assert state.dead is True
def test_living_not_dead_at_zero(self) -> None:
foe = _make_foe()
state = _make_state(foe, "players", (1, 1))
state.hp = 0
assert state.dead is False
assert state.active is True
def test_living_not_dead_in_dying_band(self) -> None:
foe = _make_foe()
state = _make_state(foe, "players", (1, 1))
state.hp = -1 # -1 > -CON (12)
assert state.dead is False
# ── JSON integrity ──────────────────────────────────────────────────────────── # ── JSON integrity ────────────────────────────────────────────────────────────