fix(spells): make the bleed spell bleed

PF1e (SRD spells/bleed.md): "Saving Throw Will negates" and the target takes 1 point of damage per round. Two divergences: the JSON declared a Fortitude save, and the bleeding condition never set the engine's bleed_damage field, so the spell was a no-op. Bleed now saves Will; _apply_condition_effect sets bleed_damage = 1 when a spell applies bleeding, and _decrement_durations lands the final tick as the last bleeding condition expires before clearing the field (heal-path clearing already existed in _stop_bleeding). README updated.

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-12 12:46:53 +02:00
parent 8c7e7075ad
commit 910ed1dc7d
5 changed files with 61 additions and 6 deletions
+21 -2
View File
@@ -1732,17 +1732,31 @@ class CombatEngine:
return speed_mult
def _decrement_durations(self, state: CombatantState) -> None:
"""Decrement duration rounds for buffs and conditions; remove expired ones."""
"""Decrement duration rounds for buffs and conditions; remove expired ones.
A bleeding condition's last round ends with its bleed tick: the DoT
lives on ``bleed_damage`` (CRB Conditions — bleeding), so the final
damage is applied as the last condition instance expires and the
field is cleared (mirrors ``_stop_bleeding`` on healing).
"""
state.active_buffs[:] = [
ActiveBuff(modifiers=b.modifiers, rounds_left=b.rounds_left - 1, source=b.source)
for b in state.active_buffs
if b.rounds_left > 1
]
state.active_conditions[:] = [
kept_conditions = [
ActiveCondition(condition=c.condition, rounds_left=c.rounds_left - 1, source=c.source)
for c in state.active_conditions
if c.rounds_left > 1
]
bleeding_expired = (
any(ac.condition.name == "bleeding" for ac in state.active_conditions)
and not any(ac.condition.name == "bleeding" for ac in kept_conditions)
)
state.active_conditions[:] = kept_conditions
if bleeding_expired and state.bleed_damage > 0:
self._apply_bleed_damage(state)
state.bleed_damage = 0
def _turn_gate(self, state: CombatantState) -> bool | None:
"""Start-of-turn effects: bleed tick, confused d% roll, cant_act flag.
@@ -5571,6 +5585,11 @@ class CombatEngine:
source=f"spell: {spell_name}",
)
)
if effect.condition_name == "bleeding":
# The bleeding condition's damage-over-time is engine-driven
# (CombatantState.bleed_damage, CRB Conditions — bleeding); the
# SRD Bleed cantrip (spells/bleed.md) deals 1 bleed damage/round.
target.bleed_damage = max(target.bleed_damage, 1)
return f"{effect.condition_name} ({effect.duration_rounds}r)"
def _apply_heal_effect(