refactor(combat): replace ac_penalty with effect system
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user