fix(combat): deed range gating

This commit is contained in:
2026-09-04 22:56:05 +02:00
parent fc62a19c9e
commit 9819c3bda9
2 changed files with 57 additions and 17 deletions
+26 -17
View File
@@ -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",
+31
View File
@@ -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 ---