fix(combat): _spell_combat_actions gates on _has_slot - with exhausted slots the cast is rejected (NO SPELL SLOT) and spell_combat resolves as a bare full attack at -2, strictly worse than the plain full attack; the policy now falls through (mirrors _choose_spell_action's slot gate) + 2 regression tests

This commit is contained in:
2026-09-19 18:41:46 +02:00
parent 657f48474c
commit 31d47aee79
2 changed files with 60 additions and 2 deletions
+9 -1
View File
@@ -8892,12 +8892,20 @@ def _emit_hex(
def _spell_combat_actions(
engine: CombatEngine, state: CombatantState, target: CombatantState
) -> tuple[Action, ...] | None:
"""Misty's spell_combat (main) + inspire_courage (swift), or None if no offensive spell."""
"""Misty's spell_combat (main) + inspire_courage (swift), None if not castable.
Slot gate (mirror of ``_choose_spell_action``): with no slot left for the
chosen spell the cast is rejected (``NO SPELL SLOT``), so the executor
would run the action as a bare full attack at -2 - strictly worse than the
plain full attack the policy would otherwise choose. Fall through instead.
"""
if state.resources_left.get("bardic_rounds", 0) <= 0:
return None
offensive = _find_offensive_spell(engine, state.combatant.spells, state, target)
if offensive is None:
return None
if not _has_slot(engine, state, offensive):
return None
return (
Action(kind="inspire_courage", target_id=target.combatant.id),
Action(kind="spell_combat", target_id=target.combatant.id, spell_name=offensive),