fix(rules): disarm drops the wielded weapon (weapon_dropped) instead of a synthetic -4 penalty
This commit is contained in:
@@ -178,6 +178,7 @@ _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)
|
||||
_UP_CLOSE_RANGE_FT = 30 # PF1e: Up Close and Deadly works within 30 ft
|
||||
_GRAPPLED_CMB_PENALTY = 2 # PF1e: grappled/pinned -2 on non-grapple/escape CMB
|
||||
_DISARM_RETALIATION_MARGIN = 10 # PF1e SRD: disarm fails by 10 -> attacker drops
|
||||
# PF1e: Point-Blank Shot grants +1/+1 within 30 ft with ranged weapons
|
||||
_POINT_BLANK_RANGE_FT = 30
|
||||
_FIRING_MELEE_PENALTY = -4 # PF1e: ranged attack into melee
|
||||
@@ -701,6 +702,11 @@ class CombatantState:
|
||||
# Runtime weapon theft (not in the frozen data model): weapon ids taken by
|
||||
# steal maneuvers.
|
||||
weapon_stolen: set[str] = field(default_factory=set)
|
||||
# Runtime disarmed weapons (not in the frozen data model): weapon ids the
|
||||
# combatant DROPPED on a failed disarm maneuver (SRD Disarm: "your
|
||||
# opponent drops the weapon"). A retrieve_weapon move action picks the
|
||||
# weapon back up.
|
||||
weapon_dropped: set[str] = field(default_factory=set)
|
||||
# Dying & stabilization (not in the frozen data model): the hp at which
|
||||
# this combatant stabilized while dying; None = not stabilized.
|
||||
stabilized_hp: int | None = None
|
||||
@@ -752,8 +758,11 @@ class CombatantState:
|
||||
|
||||
def usable_attacks(self) -> list[AttackSpec]:
|
||||
return [
|
||||
w for w in self.combatant.attacks
|
||||
if w.id not in self.weapon_broken and w.id not in self.weapon_stolen
|
||||
w
|
||||
for w in self.combatant.attacks
|
||||
if w.id not in self.weapon_broken
|
||||
and w.id not in self.weapon_stolen
|
||||
and w.id not in self.weapon_dropped
|
||||
]
|
||||
|
||||
def occupied_cells(self) -> frozenset[Pos]:
|
||||
@@ -1422,6 +1431,7 @@ class CombatEngine:
|
||||
"resources_left": resources,
|
||||
"weapon_broken": tuple(sorted(state.weapon_broken)),
|
||||
"weapon_stolen": tuple(sorted(state.weapon_stolen)),
|
||||
"weapon_dropped": tuple(sorted(state.weapon_dropped)),
|
||||
"grappled_with": state.grappled_with,
|
||||
"moved_this_turn": state.moved_this_turn,
|
||||
"power_attack": state.power_attack,
|
||||
@@ -4674,10 +4684,14 @@ class CombatEngine:
|
||||
self._resolve_aoo(target, state)
|
||||
|
||||
def _retrieve_weapon(self, state: CombatantState, weapon_id: str | None) -> None:
|
||||
"""Restore a stolen weapon to its owner (remove it from ``weapon_stolen``).
|
||||
"""Pick a stolen or dropped weapon back up.
|
||||
|
||||
No-op (with a log line) when ``weapon_id`` is None or the id was never
|
||||
stolen. Once restored, the weapon is usable again via ``usable_attacks()``.
|
||||
Removes ``weapon_id`` from ``weapon_stolen`` (steal maneuvers) or
|
||||
``weapon_dropped`` (disarm maneuvers — SRD Disarm: the disarmed
|
||||
creature "drops one item it is carrying"; retrieving a dropped item
|
||||
is a move action). No-op (with a log line) when ``weapon_id`` is
|
||||
None or the id was never stolen or dropped. Once restored, the
|
||||
weapon is usable again via ``usable_attacks()``.
|
||||
"""
|
||||
if weapon_id is None:
|
||||
self._log(
|
||||
@@ -4685,13 +4699,16 @@ class CombatEngine:
|
||||
f"retrieve: nothing to retrieve"
|
||||
)
|
||||
return
|
||||
if weapon_id not in state.weapon_stolen:
|
||||
if weapon_id in state.weapon_stolen:
|
||||
state.weapon_stolen.remove(weapon_id)
|
||||
elif weapon_id in state.weapon_dropped:
|
||||
state.weapon_dropped.remove(weapon_id)
|
||||
else:
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"retrieve {weapon_id}: nothing to retrieve"
|
||||
)
|
||||
return
|
||||
state.weapon_stolen.remove(weapon_id)
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"retrieve {weapon_id} (restored)"
|
||||
@@ -4822,21 +4839,38 @@ class CombatEngine:
|
||||
) -> None:
|
||||
if not m.success:
|
||||
self._maneuver_fail(state, target, "disarm", m)
|
||||
# PF1e (SRD Disarm): "If your attack fails by 10 or more, you
|
||||
# drop the weapon that you were using to attempt the disarm."
|
||||
if m.total <= m.target_cmd - _DISARM_RETALIATION_MARGIN:
|
||||
own = next(
|
||||
(w for w in state.usable_attacks() if w.kind == "melee"), None
|
||||
)
|
||||
if own is not None:
|
||||
state.weapon_dropped.add(own.id)
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"disarm backfire -> drops {own.name}"
|
||||
)
|
||||
return
|
||||
target.active_conditions.append(
|
||||
ActiveCondition(
|
||||
condition=Condition(
|
||||
name="disarmed",
|
||||
modifiers=(StatModifier(target="attack", value=-4),),
|
||||
),
|
||||
rounds_left=1,
|
||||
source="maneuver: disarm",
|
||||
# PF1e (SRD Disarm): "If your attack is successful, your target drops
|
||||
# one item it is carrying of your choice" — the weapon lands on the
|
||||
# ground (unusable until a retrieve_weapon move action picks it back
|
||||
# up), NOT a synthetic -4 attack penalty. The engine chooses the
|
||||
# target's first usable melee weapon (the wielded weapon, same choice
|
||||
# as the AoO machinery).
|
||||
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}: disarm vs "
|
||||
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
|
||||
f"CMD {m.target_cmd} -> SUCCESS (no weapon to drop)"
|
||||
)
|
||||
)
|
||||
return
|
||||
target.weapon_dropped.add(weapon.id)
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: disarm vs "
|
||||
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
|
||||
f"CMD {m.target_cmd} -> SUCCESS (disarmed)"
|
||||
f"CMD {m.target_cmd} -> SUCCESS ({weapon.name} dropped)"
|
||||
)
|
||||
|
||||
def _maneuver_bull_rush(
|
||||
|
||||
Reference in New Issue
Block a user