refactor(combat): replace ac_penalty with effect system

This commit is contained in:
2026-08-17 22:49:50 +02:00
parent 8ac6f9eabe
commit 88f11de40f
2 changed files with 13 additions and 9 deletions
+11 -7
View File
@@ -51,8 +51,8 @@ Phase 0 documented deviations from PF1e (conventions):
- Charge: a full-round action that moves in a straight line (Bresenham) up to - Charge: a full-round action that moves in a straight line (Bresenham) up to
2x speed toward the closest square from which the charger can melee the 2x speed toward the closest square from which the charger can melee the
target, then makes a single melee attack at +2. The charger takes -2 AC target, then makes a single melee attack at +2. The charger takes -2 AC
until the start of their next turn (tracked in ``ac_penalty``, cleared in until the start of their next turn (tracked as a StatModifier in
``_take_turn``). The straight line must be clear of difficult terrain, ``effects``, cleared in ``_take_turn``). The straight line must be clear of difficult terrain,
obstacles, and creatures; minimum 2 cells (10 ft). ``default_policy`` obstacles, and creatures; minimum 2 cells (10 ft). ``default_policy``
chooses a charge when the enemy is out of reach but a valid charge path chooses a charge when the enemy is out of reach but a valid charge path
exists. exists.
@@ -76,9 +76,11 @@ Phase 0 documented deviations from PF1e (conventions):
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass, field
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pf1e_simulator.effects import StatModifier, resolve_modifiers
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Literal from typing import Literal
@@ -156,7 +158,7 @@ class CombatantState:
side: str side: str
pos: Pos pos: Pos
hp: int hp: int
ac_penalty: int = 0 effects: list[StatModifier] = field(default_factory=list)
moved_this_turn: bool = False moved_this_turn: bool = False
@property @property
@@ -250,7 +252,7 @@ class CombatEngine:
def _take_turn(self, state: CombatantState) -> bool: def _take_turn(self, state: CombatantState) -> bool:
"""Execute one combatant's full turn; return True if the battle is over.""" """Execute one combatant's full turn; return True if the battle is over."""
state.ac_penalty = 0 state.effects.clear()
state.moved_this_turn = False state.moved_this_turn = False
actions = self._policy(self, state) actions = self._policy(self, state)
before = len(self._transcript) before = len(self._transcript)
@@ -358,7 +360,9 @@ class CombatEngine:
flank = self._flanking_bonus(attacker, target) if weapon.kind == "melee" else 0 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 base_bonus = bonus_override if bonus_override is not None else weapon.attack_bonus
total = roll + base_bonus + penalty + flank total = roll + base_bonus + penalty + flank
ac = target.combatant.ac.total - target.ac_penalty ac = target.combatant.ac.total + resolve_modifiers(
e for e in target.effects if e.target == "ac"
)
occupied = frozenset( occupied = frozenset(
s.pos for s in self._states if s.active and s is not attacker and s is not target s.pos for s in self._states if s.active and s is not attacker and s is not target
) )
@@ -620,7 +624,7 @@ class CombatEngine:
) )
if not attacker.active: if not attacker.active:
return return
attacker.ac_penalty = _CHARGE_AC_PENALTY attacker.effects.append(StatModifier(target="ac", value=-_CHARGE_AC_PENALTY))
bonus = weapon.attack_bonus + _CHARGE_ATTACK_BONUS bonus = weapon.attack_bonus + _CHARGE_ATTACK_BONUS
self._resolve_swing( self._resolve_swing(
attacker, target, weapon, label="charge", bonus_override=bonus attacker, target, weapon, label="charge", bonus_override=bonus
+2 -2
View File
@@ -18,7 +18,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence from collections.abc import Iterable
BonusType = Literal[ BonusType = Literal[
"", # untyped — stacks with everything "", # untyped — stacks with everything
@@ -65,7 +65,7 @@ class StatModifier:
weapon_filter: str | None = None weapon_filter: str | None = None
def resolve_modifiers(modifiers: Sequence[StatModifier]) -> int: def resolve_modifiers(modifiers: Iterable[StatModifier]) -> int:
"""Sum stat modifiers applying PF1e stacking rules. """Sum stat modifiers applying PF1e stacking rules.
Stacking bonus types (dodge, racial, trait, untyped) all sum together. Stacking bonus types (dodge, racial, trait, untyped) all sum together.