fix(rules): restricted charge - a creature limited to a standard action (staggered/disabled, surprise round) can still charge at 1x speed

This commit is contained in:
2026-09-17 23:06:03 +02:00
parent a5bd3c5a85
commit e253b03ffe
4 changed files with 74 additions and 20 deletions
+29 -13
View File
@@ -3759,6 +3759,18 @@ class CombatEngine:
state.pos = orig
return weapon is not None
def _charge_speed_mult(self, state: CombatantState) -> int:
"""Charge speed multiplier: 2x normally; 1x for a creature restricted
to a single standard action — PF1e (SRD "Charge"): "If you are able to
take only a standard action on your turn, you can still charge, but
you are only allowed to move up to your speed (instead of up to double
your speed)" — staggered/disabled creatures and the surprise round
(mirrors the Restricted Withdraw exception).
"""
if self._surprise_round or self._is_staggered_limited(state):
return 1
return 2
def can_charge(self, state: CombatantState, target: CombatantState) -> ChargeResult | None:
"""Return a ``ChargeResult`` if ``state`` can charge ``target`` this turn, else None.
@@ -3782,7 +3794,7 @@ class CombatEngine:
):
return ChargeResult(weapon=None, distance_ft=0, reason="no_line_of_effect")
speed_cells = _speed_ft(state) // _SQUARE_FT
max_cells = 2 * speed_cells
max_cells = self._charge_speed_mult(state) * speed_cells
blocked = self._blocked(state, target)
melee_weapons = [w for w in state.usable_attacks() if w.kind == "melee"]
if not melee_weapons:
@@ -3905,7 +3917,7 @@ class CombatEngine:
return
inner_cells, reach_cells = _melee_reach_zone_cells(attacker, weapon)
speed_cells = _speed_ft(attacker) // _SQUARE_FT
max_cells = 2 * speed_cells
max_cells = self._charge_speed_mult(attacker) * speed_cells
blocked = self._blocked(attacker, target)
end = self._charge_end(
attacker, target, reach_cells, max_cells, blocked, min_reach_cells=inner_cells
@@ -8003,11 +8015,14 @@ def _staggered_filter(
``full_attack`` (already in range) is downgraded to a single standard
``attack`` that consumes the one move-or-standard slot; a further
full-round action (charge, run, a full-round/one-round cast,
spell_combat) is dropped. Exception (SRD "Full-Round Actions — Restricted
Withdraw"): "If you are limited to taking only a standard action each
round you can withdraw as a standard action. In this case, you may move
up to your speed" — a ``withdraw`` is kept as the one standard action and
``_flee_path`` halves its budget to single speed.
spell_combat) is dropped. Exceptions (SRD "Full-Round Actions — Restricted
Withdraw", SRD "Charge — restricted"): "If you are limited to taking only
a standard action each round you can withdraw as a standard action. In
this case, you may move up to your speed" / "If you are able to take only
a standard action on your turn, you can still charge, but you are only
allowed to move up to your speed" — ``withdraw`` and ``charge`` are kept
as the one standard action and their movement budgets halve to single
speed (``_flee_path``, ``_charge_speed_mult``).
"""
kept: list[Action] = []
slot_used = False # the single move-OR-standard slot is spent
@@ -8021,8 +8036,8 @@ def _staggered_filter(
continue
slot = _effective_slot(action, cast_time, free_draw=free_draw)
if slot == "full_round":
if action.kind == "withdraw" and not slot_used:
# Restricted Withdraw: standard action, move up to speed.
if action.kind in ("withdraw", "charge") and not slot_used:
# Restricted Withdraw / Restricted Charge: standard action.
kept.append(action)
slot_used = True
continue
@@ -8050,9 +8065,10 @@ def _surprise_filter(
grants neither a swift action nor a full-round action, so swift and
full-round kinds are dropped — with the same conventions as
``_staggered_filter``: a ``full_attack`` collapses to a single standard
``attack``, and ``withdraw`` survives as the standard-action Restricted
Withdraw (SRD "Full-Round Actions": legal "if you are limited to taking
only a standard action each round").
``attack``, and ``withdraw`` / ``charge`` survive as the standard-action
Restricted Withdraw / Restricted Charge (SRD "Full-Round Actions",
"Charge": legal "if you are able to take only a standard action on your
turn", at single speed).
"""
kept: list[Action] = []
slot_used = False # the single standard-or-move slot is spent
@@ -8064,7 +8080,7 @@ def _surprise_filter(
continue
slot = _effective_slot(action, cast_time, free_draw=free_draw)
if slot == "full_round":
if action.kind == "withdraw" and not slot_used:
if action.kind in ("withdraw", "charge") and not slot_used:
kept.append(action)
slot_used = True
continue