fix(rules): tick bleed and auto-fail stabilization for a dying creature

PF1e (CRB, Stable Characters and Recovery): characters taking continuous damage such as bleed automatically fail all Constitution checks made to stabilize, losing 1 hp per round in addition to the continuous damage. _process_dying never applied bleed (only _turn_gate, reached by active creatures did), so a bleeding dying creature lost 0 hp per round. It now ticks bleed_damage at the start of the dying turn and auto-fails the check (no d20 consumed), then evaluates death.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-11 21:06:10 +02:00
parent 32c09df264
commit 1b5cac234e
3 changed files with 68 additions and 2 deletions
+42
View File
@@ -170,6 +170,48 @@ def test_dying_bleeds_to_death_threshold() -> None:
assert s.hp == -10
def test_dying_with_bleed_ticks_damage_and_auto_fails_stabilization() -> None:
# PF1e (CRB, Stable Characters and Recovery): a creature taking continuous
# damage (bleed) automatically fails the stabilization check and loses 1 hp
# per round IN ADDITION to the bleed damage. No d20 is consumed (an empty
# queue would raise), and it dies faster than the 1-hp-per-round baseline.
eng, s = _solo(con_score=8) # death threshold -8
s.hp = -3
s.bleed_damage = 2
eng._current_round = 2
eng._rng = ScriptedRng([]) # no roll: the check is auto-failed
eng._process_dying(s)
# bleed 2 (hp -3 -> -5), then auto-fail loses 1 more (hp -5 -> -6).
assert s.hp == -6
assert s.stabilized_hp is None
lines = "\n".join(eng._transcript)
assert "bleed 2 (hp -3->-5)" in lines
assert "continuous damage auto-fails the Constitution check" in lines
assert "loses 1 hp (hp -5 -> -6)" in lines
def test_dying_with_bleed_reaches_death_faster_than_without() -> None:
# Same start; the bleeding creature hits -CON in fewer rounds than the
# non-bleeding one (which only loses 1 hp per failed check).
eng_b, bleed = _solo(con_score=8) # death threshold -8
bleed.hp = -3
bleed.bleed_damage = 2
eng_b._rng = ScriptedRng([]) # auto-fail, no roll
eng_b._process_dying(bleed) # -3 -2 bleed -1 = -6
assert bleed.hp == -6
assert not bleed.dead
eng_b._process_dying(bleed) # -6 -2 bleed -1 = -9 <= -8 -> dead
assert bleed.dead
eng_n, plain = _solo(con_score=8)
plain.hp = -3
eng_n._rng = ScriptedRng([1, 1]) # two failed CON checks: -3 -> -4 -> -5
eng_n._process_dying(plain)
eng_n._process_dying(plain)
assert plain.hp == -5 # still alive after two rounds
assert not plain.dead
def test_dying_tick_runs_on_initiative_slot_during_round() -> None:
# m1 is downed before the battle starts; both sides still have active
# members so the battle continues and m1's slot processes the dying check.