feat(conditions): add condition system with 18 PF1e conditions

This commit is contained in:
2026-08-17 22:49:50 +02:00
parent 2cd8a33a4d
commit a32444f804
4 changed files with 558 additions and 12 deletions
+20 -8
View File
@@ -84,6 +84,7 @@ from pf1e_simulator.effects import StatModifier, resolve_modifiers
if TYPE_CHECKING:
from typing import Literal
from pf1e_simulator.conditions import Condition
from pf1e_simulator.grid import Grid
from pf1e_simulator.map import Pos
from pf1e_simulator.models import AttackSpec, Combatant, DamageReduction
@@ -159,6 +160,7 @@ class CombatantState:
pos: Pos
hp: int
effects: list[StatModifier] = field(default_factory=list)
conditions: list[Condition] = field(default_factory=list)
moved_this_turn: bool = False
@property
@@ -355,6 +357,13 @@ class CombatEngine:
return _FLANK_BONUS
return 0
def _stat_modifiers(self, state: CombatantState, target: str) -> list[StatModifier]:
"""Collect all StatModifiers for ``target`` from effects + conditions."""
mods = [e for e in state.effects if e.target == target]
for cond in state.conditions:
mods.extend(m for m in cond.modifiers if m.target == target)
return mods
def resolve_attack(
self,
attacker: CombatantState,
@@ -369,10 +378,10 @@ class CombatEngine:
penalty = self.range_penalty(weapon, dist_ft)
flank = self._flanking_bonus(attacker, target) if weapon.kind == "melee" else 0
base_bonus = bonus_override if bonus_override is not None else weapon.attack_bonus
total = roll + base_bonus + penalty + flank
ac = target.combatant.ac.total + resolve_modifiers(
e for e in target.effects if e.target == "ac"
)
atk_mods = self._stat_modifiers(attacker, "attack")
attack_mod = resolve_modifiers(atk_mods)
total = roll + base_bonus + penalty + flank + attack_mod
ac = target.combatant.ac.total + resolve_modifiers(self._stat_modifiers(target, "ac"))
occupied = frozenset(
s.pos for s in self._states if s.active and s is not attacker and s is not target
)
@@ -390,9 +399,11 @@ class CombatEngine:
if hit:
if roll != _NATURAL_ONE and roll >= weapon.crit_range:
confirm = self._rng.d20()
confirm_total = confirm + base_bonus + penalty + flank
confirm_total = confirm + base_bonus + penalty + flank + attack_mod
crit = confirm != _NATURAL_ONE and confirm_total >= ac
dmg_mods = self._stat_modifiers(attacker, "damage")
damage = sum(c.formula.roll(self._rng) for c in weapon.damage) + weapon.damage_bonus
damage += resolve_modifiers(dmg_mods)
if crit:
damage *= weapon.crit_mult
damage = self._apply_dr(damage, weapon, target)
@@ -417,12 +428,13 @@ class CombatEngine:
"""Roll a saving throw (fort/ref/will) against ``dc``.
PF1e: natural 1 = automatic failure, natural 20 = automatic success.
Effect modifiers (StatModifier with matching target) are applied via
``resolve_modifiers`` — same bonus-type stacking rules as attacks.
Effect modifiers and condition modifiers (StatModifier with matching
target) are applied via ``resolve_modifiers`` — same bonus-type
stacking rules as attacks.
"""
roll = self._rng.d20()
base = getattr(state.combatant.saves, save_type)
bonus = resolve_modifiers(e for e in state.effects if e.target == save_type)
bonus = resolve_modifiers(self._stat_modifiers(state, save_type))
total = roll + base + bonus
success = roll != _NATURAL_ONE and (roll == _NATURAL_TWENTY or total >= dc)
return SaveResult(success=success, roll=roll, total=total, dc=dc)