fix(combat): deed range gating
This commit is contained in:
@@ -1568,30 +1568,35 @@ class CombatEngine:
|
|||||||
"""Execute a gunslinger deed (up_close_and_deadly, quick_clear)."""
|
"""Execute a gunslinger deed (up_close_and_deadly, quick_clear)."""
|
||||||
if deed_name not in ("up_close_and_deadly", "quick_clear"):
|
if deed_name not in ("up_close_and_deadly", "quick_clear"):
|
||||||
return
|
return
|
||||||
if not state.spend_resource("grit", 1):
|
|
||||||
self._log(
|
|
||||||
f"round {self._current_round} {state.combatant.id}: deed failed (no grit)"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if deed_name == "up_close_and_deadly":
|
if deed_name == "up_close_and_deadly":
|
||||||
# Up Close and Deadly: +1d6 damage within 30ft
|
# Up Close and Deadly: +1d6 damage within 30ft
|
||||||
if target is None or not target.active:
|
if target is None or not target.active:
|
||||||
return
|
return
|
||||||
dist_ft = self._grid.distance(state.pos, target.pos) * _SQUARE_FT
|
dist_ft = self._grid.distance(state.pos, target.pos) * _SQUARE_FT
|
||||||
if dist_ft <= _UP_CLOSE_RANGE_FT:
|
if dist_ft > _UP_CLOSE_RANGE_FT:
|
||||||
# Apply +1d6 damage to next attack this round
|
|
||||||
state.deed_up_close_and_deadly = True
|
|
||||||
self._log(
|
|
||||||
f"round {self._current_round} {state.combatant.id}: "
|
|
||||||
f"deed Up Close and Deadly activated (+1d6 dmg within 30ft)"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._log(
|
self._log(
|
||||||
f"round {self._current_round} {state.combatant.id}: deed Up Close "
|
f"round {self._current_round} {state.combatant.id}: deed Up Close "
|
||||||
f"and Deadly - target out of range"
|
f"and Deadly - target out of range"
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
if not state.spend_resource("grit", 1):
|
||||||
|
self._log(
|
||||||
|
f"round {self._current_round} {state.combatant.id}: deed failed (no grit)"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Apply +1d6 damage to next attack this round
|
||||||
|
state.deed_up_close_and_deadly = True
|
||||||
|
self._log(
|
||||||
|
f"round {self._current_round} {state.combatant.id}: "
|
||||||
|
f"deed Up Close and Deadly activated (+1d6 dmg within 30ft)"
|
||||||
|
)
|
||||||
elif deed_name == "quick_clear":
|
elif deed_name == "quick_clear":
|
||||||
# Quick Clear: swift action, clears misfire
|
# Quick Clear: swift action, clears misfire
|
||||||
|
if not state.spend_resource("grit", 1):
|
||||||
|
self._log(
|
||||||
|
f"round {self._current_round} {state.combatant.id}: deed failed (no grit)"
|
||||||
|
)
|
||||||
|
return
|
||||||
self._log(
|
self._log(
|
||||||
f"round {self._current_round} {state.combatant.id}: "
|
f"round {self._current_round} {state.combatant.id}: "
|
||||||
f"deed Quick Clear - firearm cleared"
|
f"deed Quick Clear - firearm cleared"
|
||||||
@@ -2534,7 +2539,7 @@ def default_policy(engine: CombatEngine, state: CombatantState) -> tuple[Action,
|
|||||||
return spell_combat
|
return spell_combat
|
||||||
|
|
||||||
# Deed (tammara) and inspire_courage (misty melee fallback): swift/free bonuses.
|
# Deed (tammara) and inspire_courage (misty melee fallback): swift/free bonuses.
|
||||||
swift_bonus = _swift_bonus_actions(state, target)
|
swift_bonus = _swift_bonus_actions(engine, state, target)
|
||||||
return (*swift_bonus, *normal) if swift_bonus else normal
|
return (*swift_bonus, *normal) if swift_bonus else normal
|
||||||
|
|
||||||
|
|
||||||
@@ -2597,13 +2602,17 @@ def _spell_combat_actions(
|
|||||||
|
|
||||||
|
|
||||||
def _swift_bonus_actions(
|
def _swift_bonus_actions(
|
||||||
state: CombatantState, target: CombatantState
|
engine: CombatEngine, state: CombatantState, target: CombatantState
|
||||||
) -> tuple[Action, ...]:
|
) -> tuple[Action, ...]:
|
||||||
"""Swift/free class-ability actions (deed, inspire_courage) to prepend to the turn."""
|
"""Swift/free class-ability actions (deed, inspire_courage) to prepend to the turn."""
|
||||||
bonus: list[Action] = []
|
bonus: list[Action] = []
|
||||||
|
|
||||||
# Deed (tammara): swift/free action, emitted alongside the normal action.
|
# Deed (tammara): swift/free action, emitted alongside the normal action only
|
||||||
if state.resources_left.get("grit", 0) > 0:
|
# when the target is within Up Close and Deadly range (30 ft).
|
||||||
|
if (
|
||||||
|
state.resources_left.get("grit", 0) > 0
|
||||||
|
and engine.grid.distance(state.pos, target.pos) * _SQUARE_FT <= _UP_CLOSE_RANGE_FT
|
||||||
|
):
|
||||||
bonus.append(
|
bonus.append(
|
||||||
Action(
|
Action(
|
||||||
kind="deed",
|
kind="deed",
|
||||||
|
|||||||
@@ -192,6 +192,37 @@ def test_tammara_silent_when_grit_exhausted() -> None:
|
|||||||
assert not any(a.kind == "deed" for a in actions)
|
assert not any(a.kind == "deed" for a in actions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tammara_silent_when_target_out_of_deed_range() -> None:
|
||||||
|
tammara = _combatant("tammara", resources={"grit": 3})
|
||||||
|
goblin = _combatant("goblin", hp_max=6)
|
||||||
|
engine, me, _ = _engine(tammara, [])
|
||||||
|
far_goblin = _state(goblin, "monsters", (11, 11), hp=6)
|
||||||
|
engine.states.append(far_goblin)
|
||||||
|
|
||||||
|
actions = default_policy(engine, me)
|
||||||
|
|
||||||
|
assert not any(a.kind == "deed" for a in actions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tammara_deed_out_of_range_spends_no_grit() -> None:
|
||||||
|
tammara = _combatant("tammara", resources={"grit": 3})
|
||||||
|
goblin = _combatant("goblin", hp_max=6)
|
||||||
|
engine, me, _ = _engine(tammara, [])
|
||||||
|
far_goblin = _state(goblin, "monsters", (11, 11), hp=6)
|
||||||
|
engine.states.append(far_goblin)
|
||||||
|
|
||||||
|
engine._execute(
|
||||||
|
me,
|
||||||
|
Action(
|
||||||
|
kind="deed",
|
||||||
|
target_id=far_goblin.combatant.id,
|
||||||
|
deed_name="up_close_and_deadly",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert me.resources_left["grit"] == 3
|
||||||
|
|
||||||
|
|
||||||
# --- misty: spell_combat + inspire_courage ---
|
# --- misty: spell_combat + inspire_courage ---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user