fix(combat): sunder damages weapon not HP
This commit is contained in:
@@ -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} "
|
||||
|
||||
Reference in New Issue
Block a user