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:
@@ -319,7 +319,12 @@ Règles modélisées :
|
||||
ramenant à `hp ≥ 0` réinitialise la stabilisation pour toute chute future.
|
||||
La mort survient quand les pv négatifs atteignent le score de CON
|
||||
(`hp ≤ -CON`) — conformance P2 : la règle du −10 de D&D 3.5 n'existe pas
|
||||
en PF1e et n'est plus modélisée.
|
||||
en PF1e et n'est plus modélisée. Une créature mourante qui subit des dégâts
|
||||
continus (saignement) perd ses dégâts de saignement au début de son tour,
|
||||
puis **échoue automatiquement** son jet de stabilisation (aucun d20 jeté) et
|
||||
perd donc 1 pv de plus par tour en plus des dégâts continus (CRB « Stable
|
||||
Characters and Recovery ») — elle meurt ainsi plus vite qu'une créature
|
||||
mourante non saignante.
|
||||
- Mort simplifiée des monstres (option `--simple-monster-death`, défaut
|
||||
désactivé) : quand l'option est présente dans la commande, un **monstre**
|
||||
meurt dès `hp ≤ 0` — ni bande d'invalidité à 0 pv, ni bande d'agonie entre
|
||||
|
||||
@@ -1816,7 +1816,9 @@ class CombatEngine:
|
||||
(d20 + Con modifier - negative hp total); a natural 20 auto-succeeds.
|
||||
Failure bleeds 1 more hp; success stabilizes (no further loss while
|
||||
stable). New damage below the stabilizing hp resumes dying; healing to
|
||||
hp >= 0 resets stabilization for any future downing.
|
||||
hp >= 0 resets stabilization for any future downing. A creature taking
|
||||
continuous damage (bleed) instead ticks the bleed and AUTOMATICALLY
|
||||
FAILS the check (CRB, Stable Characters and Recovery).
|
||||
"""
|
||||
if state.dead:
|
||||
return
|
||||
@@ -1827,6 +1829,23 @@ class CombatEngine:
|
||||
if state.hp >= state.stabilized_hp:
|
||||
return
|
||||
state.stabilized_hp = None
|
||||
# PF1e (CRB, Stable Characters and Recovery): "Characters taking
|
||||
# continuous damage, such as from an acid arrow or a bleed effect,
|
||||
# automatically fail all Constitution checks made to stabilize. Such
|
||||
# characters lose 1 hit point per round in addition to the continuous
|
||||
# damage." This is the inactive creature's once-per-round slot (the
|
||||
# active path already ticked bleed in _turn_gate), so the bleed damage
|
||||
# is applied here and the d20 check is skipped (auto-fail).
|
||||
if state.bleed_damage > 0:
|
||||
self._apply_bleed_damage(state)
|
||||
hp_before = state.hp
|
||||
state.hp -= 1
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: dying — continuous "
|
||||
f"damage auto-fails the Constitution check, loses 1 hp "
|
||||
f"(hp {hp_before} -> {state.hp})"
|
||||
)
|
||||
return
|
||||
con_mod = (state.combatant.abilities.con_score - 10) // 2
|
||||
penalty = -state.hp
|
||||
roll = self._rng.d20()
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user