fix(combat): _choose_spell_action heal branch gates on spell range + line of effect - a touch-range Cure on a distant ally was rejected by _cast_spell (OUT OF RANGE / NO LINE OF EFFECT) and wasted the caster's whole turn; extracted the offensive branch's reach check as _spell_reaches and reused it (the duplicated below-half check in the branch was a no-op copy of _find_ally_needing_heal, dropped) + 3 policy tests
This commit is contained in:
@@ -23,7 +23,7 @@ from pf1e_simulator.models import (
|
||||
Saves,
|
||||
)
|
||||
from pf1e_simulator.rng import ScriptedRng
|
||||
from pf1e_simulator.spells import SPELLS_DIR, SpellSpec, load_spell_registry
|
||||
from pf1e_simulator.spells import SPELLS_DIR, HealEffect, SpellSpec, load_spell_registry
|
||||
|
||||
|
||||
def _combatant(
|
||||
@@ -424,6 +424,95 @@ def test_tammara_deed_out_of_range_spends_no_grit() -> None:
|
||||
assert me.resources_left["grit"] == 3
|
||||
|
||||
|
||||
# --- policy heal branch: range gate ---
|
||||
|
||||
|
||||
def _healer(hp_max: int = 20) -> Combatant:
|
||||
return _combatant(
|
||||
"cleric", spells=["Cure Light Wounds"], spell_slots={1: 1}, hp_max=hp_max
|
||||
)
|
||||
|
||||
|
||||
def test_heal_cast_when_wounded_ally_in_touch_range() -> None:
|
||||
"""A touch-range Cure is proposed only when the ally is within the spell's
|
||||
range: ally at 5 ft (1 cell) -> cast_spell on the ally."""
|
||||
cleric = _healer()
|
||||
engine, me, _ = _engine(
|
||||
cleric, [_combatant("goblin", hp_max=6)],
|
||||
spell_registry=load_spell_registry(SPELLS_DIR),
|
||||
)
|
||||
hurt_ally = _state(_combatant("ally", hp_max=20), "players", (5, 4), hp=5)
|
||||
engine.states.append(hurt_ally)
|
||||
|
||||
actions = default_policy(engine, me)
|
||||
|
||||
assert actions[0].kind == "cast_spell"
|
||||
assert actions[0].target_id == "ally"
|
||||
assert actions[0].spell_name == "Cure Light Wounds"
|
||||
|
||||
|
||||
def test_no_heal_cast_when_wounded_ally_out_of_range() -> None:
|
||||
"""Regression: a touch-range Cure on an ally 20 ft away is rejected by
|
||||
_cast_spell (OUT OF RANGE), wasting the caster's whole turn. The policy
|
||||
must skip the cast and fall through to the attack path."""
|
||||
cleric = _healer()
|
||||
engine, me, _ = _engine(
|
||||
cleric, [_combatant("goblin", hp_max=6)],
|
||||
spell_registry=load_spell_registry(SPELLS_DIR),
|
||||
)
|
||||
hurt_ally = _state(_combatant("ally", hp_max=20), "players", (5, 9), hp=5)
|
||||
engine.states.append(hurt_ally)
|
||||
|
||||
actions = default_policy(engine, me)
|
||||
|
||||
assert not any(a.kind == "cast_spell" for a in actions)
|
||||
assert any(a.kind == "full_attack" for a in actions)
|
||||
|
||||
|
||||
def test_no_heal_cast_when_wounded_ally_blocked_by_wall() -> None:
|
||||
"""Line-of-effect gate (distinct from range): a close-range Cure reaches
|
||||
the ally by distance (15 ft < 35 ft at level 5) but a solid barrier
|
||||
cancels line of effect, so the cast would be rejected NO LINE OF EFFECT
|
||||
and the policy must fall through."""
|
||||
cleric = _combatant(
|
||||
"cleric", spells=["Wall Blocked Cure"], spell_slots={1: 1}, hp_max=20
|
||||
)
|
||||
cure = SpellSpec(
|
||||
name="Wall Blocked Cure", level=1, school="conjuration", range="close",
|
||||
effects=(HealEffect(formula=parse_dice("1d8")),),
|
||||
)
|
||||
legend = {
|
||||
".": TerrainType(type="floor", move_cost=1),
|
||||
"#": TerrainType(type="wall", move_cost=None, blocks_los=True),
|
||||
}
|
||||
rows = (
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
".....#.#####",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
)
|
||||
spec = MapSpec(name="walled", terrain=rows, legend=legend)
|
||||
me = _state(cleric, "players", (5, 5))
|
||||
enemy = _state(_combatant("goblin", hp_max=6), "monsters", (6, 6))
|
||||
hurt_ally = _state(_combatant("ally", hp_max=20), "players", (5, 8), hp=5)
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([]), Grid.from_spec(spec), [me, enemy, hurt_ally],
|
||||
spell_registry={"Wall Blocked Cure": cure},
|
||||
)
|
||||
|
||||
actions = default_policy(engine, me)
|
||||
|
||||
assert not any(a.kind == "cast_spell" for a in actions)
|
||||
|
||||
|
||||
# --- misty: spell_combat + inspire_courage ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user