fix(combat): Block run action when cant_charge flag is set
SRD Conditions Fatigued: A fatigued character can neither run nor charge. _execute_charge already checked cant_charge; _execute_run did not -- a fatigued/exhausted/entangled combatant could still run. Added cant_charge guard to _execute_run (mirrors _execute_charge), logging wait (cannot run) and returning ok. Two engine-level tests: fatigued cannot run, exhausted cannot run.
This commit is contained in:
@@ -4097,6 +4097,11 @@ class CombatEngine:
|
||||
if self.has_condition_flag(state, "cant_move"):
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot move)")
|
||||
return ActionResult(kind=kind, ok=True, reason=_REASON_OK)
|
||||
# SRD "Conditions — Fatigued": "A fatigued character can neither run
|
||||
# nor charge" (same for exhausted/entangled via cant_charge).
|
||||
if self.has_condition_flag(state, "cant_charge"):
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot run)")
|
||||
return ActionResult(kind=kind, ok=True, reason=_REASON_OK)
|
||||
if action.destination is not None:
|
||||
if action.destination == state.pos:
|
||||
path: list[Pos] = [] # already there: a valid zero-length run
|
||||
|
||||
@@ -8,7 +8,14 @@ Part (b): Condition behavioral effects enforced via action economy / AC modifier
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import Action, CombatantState, CombatEngine
|
||||
from pf1e_simulator.conditions import NAUSEATED, PARALYZED, PRONE, STAGGERED
|
||||
from pf1e_simulator.conditions import (
|
||||
EXHAUSTED,
|
||||
FATIGUED,
|
||||
NAUSEATED,
|
||||
PARALYZED,
|
||||
PRONE,
|
||||
STAGGERED,
|
||||
)
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.effects import ActiveCondition, StatModifier
|
||||
from pf1e_simulator.grid import Grid
|
||||
@@ -226,7 +233,76 @@ class TestStaggeredActionEconomy:
|
||||
assert h.pos != (10, 10)
|
||||
|
||||
|
||||
# ── Test 4: nauseated → single move action only ───────────────────────────────
|
||||
# ── Test 3b: fatigued/exhausted cannot run ────────────────────────────────────
|
||||
|
||||
|
||||
class TestFatiguedCannotRun:
|
||||
"""Given: a fatigued or exhausted combatant
|
||||
When: the engine executes a run action
|
||||
Then: the run is blocked (SRD: "A fatigued character can neither run nor
|
||||
charge"; exhausted and entangled share the cant_charge flag)."""
|
||||
|
||||
def test_fatigued_cannot_run(self) -> None:
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
grid = Grid.from_spec(
|
||||
MapSpec(name="big", terrain=tuple(["." * 20] * 20), legend=legend)
|
||||
)
|
||||
hero = _make_hero()
|
||||
foe = _make_foe()
|
||||
h = CombatantState(combatant=hero, side="players", pos=(0, 0), hp=hero.hp_max)
|
||||
f = CombatantState(combatant=foe, side="monsters", pos=(10, 0), hp=foe.hp_max)
|
||||
h.active_conditions.append(
|
||||
ActiveCondition(condition=FATIGUED, rounds_left=5, source="test")
|
||||
)
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([10, 9]),
|
||||
grid,
|
||||
[h, f],
|
||||
round_cap=1,
|
||||
policy=lambda _e, s: (
|
||||
(Action(kind="run", target_id="foe"),)
|
||||
if s.combatant.id == hero.id
|
||||
else (Action(kind="wait"),)
|
||||
),
|
||||
)
|
||||
engine.run()
|
||||
run_lines = [
|
||||
line for line in engine._transcript
|
||||
if "hero: run" in line or "hero: wait" in line
|
||||
]
|
||||
assert any("wait (cannot run)" in line for line in run_lines)
|
||||
assert h.pos == (0, 0)
|
||||
|
||||
def test_exhausted_cannot_run(self) -> None:
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
grid = Grid.from_spec(
|
||||
MapSpec(name="big", terrain=tuple(["." * 20] * 20), legend=legend)
|
||||
)
|
||||
hero = _make_hero()
|
||||
foe = _make_foe()
|
||||
h = CombatantState(combatant=hero, side="players", pos=(0, 0), hp=hero.hp_max)
|
||||
f = CombatantState(combatant=foe, side="monsters", pos=(10, 0), hp=foe.hp_max)
|
||||
h.active_conditions.append(
|
||||
ActiveCondition(condition=EXHAUSTED, rounds_left=5, source="test")
|
||||
)
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([10, 9]),
|
||||
grid,
|
||||
[h, f],
|
||||
round_cap=1,
|
||||
policy=lambda _e, s: (
|
||||
(Action(kind="run", target_id="foe"),)
|
||||
if s.combatant.id == hero.id
|
||||
else (Action(kind="wait"),)
|
||||
),
|
||||
)
|
||||
engine.run()
|
||||
run_lines = [
|
||||
line for line in engine._transcript
|
||||
if "hero: run" in line or "hero: wait" in line
|
||||
]
|
||||
assert any("wait (cannot run)" in line for line in run_lines)
|
||||
assert h.pos == (0, 0)
|
||||
|
||||
|
||||
class TestNauseatedActionEconomy:
|
||||
|
||||
Reference in New Issue
Block a user