fix(combat): sunder damages weapon not HP

This commit is contained in:
2026-09-04 22:55:59 +02:00
parent 4921db7b28
commit c01a3c1e89
2 changed files with 132 additions and 5 deletions
+39 -5
View File
@@ -117,6 +117,7 @@ _MAX_RANGE_INCREMENTS = 10 # PF1e: projectile weapons shoot up to 10 increments
_FLANK_BONUS = 2 # PF1e: +2 melee attack when ally threatens opposite side
_ITERATIVE_PENALTY = 5 # PF1e: each BAB iterative is at -5 from the previous
_MAX_ITERATIVES = 4 # PF1e: BAB +16 gives 4 attacks (at +16, +11, +6, +1)
_WEAPON_DURABILITY = 10 # Accumulated sunder damage that destroys a weapon.
_CHARGE_ATTACK_BONUS = 2 # PF1e: +2 attack roll on a charge
_CHARGE_AC_PENALTY = 2 # PF1e: -2 AC until start of next turn after charging
_CHARGE_MIN_CELLS = 2 # PF1e: charge must move at least 10 ft (2 squares)
@@ -202,6 +203,13 @@ class CombatantState:
deed_up_close_and_deadly: bool = False
summon_duration: int = 0
summon_caster_id: str | None = None
# Runtime weapon durability (not in the frozen data model): accumulated
# sunder damage per weapon id, and the set of weapon ids rendered unusable.
weapon_damage: dict[str, int] = field(default_factory=dict)
weapon_broken: set[str] = field(default_factory=set)
def usable_attacks(self) -> list[AttackSpec]:
return [w for w in self.combatant.attacks if w.id not in self.weapon_broken]
def __post_init__(self) -> None:
if not self.spell_slots_left:
@@ -497,7 +505,7 @@ class CombatEngine:
if not has_line_of_effect(self._grid, attacker.pos, target.pos):
return None
dist_ft = self._grid.distance(attacker.pos, target.pos) * _SQUARE_FT
for weapon in attacker.combatant.attacks:
for weapon in attacker.usable_attacks():
if weapon.kind in ("melee", "touch") and dist_ft <= weapon.reach_ft:
return weapon
if (
@@ -797,7 +805,7 @@ class CombatEngine:
"""Squares this combatant threatens with a melee weapon (8 adjacent for 5 ft reach)."""
if not state.active:
return frozenset()
if not any(w.kind == "melee" for w in state.combatant.attacks):
if not any(w.kind == "melee" for w in state.usable_attacks()):
return frozenset()
r, c = state.pos
return frozenset((r + dr, c + dc) for dr, dc in _STEP_DELTAS)
@@ -822,7 +830,7 @@ class CombatEngine:
if self.has_condition_flag(attacker, "cant_aoo"):
return False
self._aoo_used[attacker.combatant.id] = self._aoo_used.get(attacker.combatant.id, 0) + 1
weapon = next((w for w in attacker.combatant.attacks if w.kind == "melee"), None)
weapon = next((w for w in attacker.usable_attacks() if w.kind == "melee"), None)
if weapon is None:
return False
return self._resolve_swing(
@@ -936,7 +944,7 @@ class CombatEngine:
blocked = frozenset(
s.pos for s in self._states if s is not state and s is not target and s.active
)
for weapon in state.combatant.attacks:
for weapon in state.usable_attacks():
if weapon.kind != "melee":
continue
reach_cells = weapon.reach_ft // _SQUARE_FT
@@ -1226,8 +1234,34 @@ class CombatEngine:
if not m.success:
self._maneuver_fail(state, target, "sunder", m)
return
weapon = next((w for w in target.usable_attacks() if w.kind == "melee"), None)
if weapon is None:
self._log(
f"round {self._current_round} {state.combatant.id}: sunder vs "
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
f"CMD {m.target_cmd} -> SUCCESS (no weapon to damage)"
)
return
dmg = state.combatant.abilities.str_mod + 2
target.hp -= dmg
target.weapon_damage[weapon.id] = target.weapon_damage.get(weapon.id, 0) + dmg
if target.weapon_damage[weapon.id] >= _WEAPON_DURABILITY:
target.weapon_broken.add(weapon.id)
target.active_conditions.append(
ActiveCondition(
condition=Condition(
name="disarmed",
modifiers=(StatModifier(target="attack", value=-4),),
),
rounds_left=1,
source="maneuver: sunder",
)
)
self._log(
f"round {self._current_round} {state.combatant.id}: sunder vs "
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
f"CMD {m.target_cmd} -> SUCCESS (weapon destroyed)"
)
return
self._log(
f"round {self._current_round} {state.combatant.id}: sunder vs "
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "