fix(combat): _swift_bonus_actions emits up_close_and_deadly only when the turn will actually fire a firearm attack within 30 ft - the deed burns 1 grit at execution and its +1d6 rides only a firearm attack, so emitting it alongside a melee swing or a spell cast wasted grit for a lost bonus; + 2 policy tests, deed test now uses a firearm-only tammara

This commit is contained in:
2026-09-19 18:55:17 +02:00
parent e8b7a73d1f
commit f93c55b3fb
2 changed files with 71 additions and 12 deletions
+54 -8
View File
@@ -34,14 +34,27 @@ def _combatant(
spell_slots: dict[str, int] | None = None,
hp_max: int = 20,
creature_type: Literal["living", "undead", "construct"] = "living",
firearm: bool = False,
) -> Combatant:
attack = AttackSpec(
id=f"{cid}-w",
name="sword",
kind="melee",
attack_bonus=3,
damage=[DamageComponent(formula=parse_dice("1d6"), types=["slashing"])],
)
if firearm:
# Tammara-style: a firearm (pistol) as her ONLY weapon.
attack = AttackSpec(
id=f"{cid}-w",
name="pistol",
kind="touch",
attack_bonus=3,
damage=[DamageComponent(formula=parse_dice("1d6"), types=["piercing"])],
range_increment_ft=20,
firearm=True,
)
else:
attack = AttackSpec(
id=f"{cid}-w",
name="sword",
kind="melee",
attack_bonus=3,
damage=[DamageComponent(formula=parse_dice("1d6"), types=["slashing"])],
)
return Combatant(
id=cid,
name=cid,
@@ -373,7 +386,7 @@ def test_slumber_saved_leaves_target_awake() -> None:
def test_tammara_emits_deed_alongside_attack() -> None:
tammara = _combatant("tammara", resources={"grit": 3})
tammara = _combatant("tammara", resources={"grit": 3}, firearm=True)
goblin = _combatant("goblin", hp_max=6)
engine, me, _ = _engine(tammara, [goblin])
@@ -383,6 +396,39 @@ def test_tammara_emits_deed_alongside_attack() -> None:
assert any(a.kind in {"full_attack", "attack"} for a in actions)
def test_tammara_no_deed_when_followup_attack_is_melee() -> None:
"""Regression: Up Close and Deadly spends 1 grit at execution and its
+1d6 rides only a firearm attack within 30 ft; emitted alongside a melee
full attack the bonus is lost and the grit burned. The policy must skip
the deed when the weapon the turn will fire is not a firearm."""
tammara = _combatant("tammara", resources={"grit": 3}) # sword only
goblin = _combatant("goblin", hp_max=6)
engine, me, _ = _engine(tammara, [goblin])
actions = default_policy(engine, me)
assert not any(a.kind == "deed" for a in actions)
assert any(a.kind == "full_attack" for a in actions)
def test_tammara_no_deed_when_turn_casts_instead_of_attacking() -> None:
"""Even with a firearm selected, a turn whose normal action is a spell
cast resolves no weapon attack: the deed flag would burn grit unused."""
tammara = _combatant(
"tammara", resources={"grit": 3}, firearm=True,
spells=["Divine Favor"], spell_slots={1: 1},
)
goblin = _combatant("goblin", hp_max=6)
engine, me, _ = _engine(
tammara, [goblin], spell_registry=load_spell_registry(SPELLS_DIR)
)
actions = default_policy(engine, me)
assert any(a.kind == "cast_spell" for a in actions)
assert not any(a.kind == "deed" for a in actions)
def test_tammara_silent_when_grit_exhausted() -> None:
tammara = _combatant("tammara", resources={"grit": 0})
goblin = _combatant("goblin", hp_max=6)