feat(combat): AoO and concentration on casting
This commit is contained in:
@@ -640,6 +640,49 @@ class CombatEngine:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_casting_aoo(self, caster: CombatantState, spell_level: int) -> bool:
|
||||
"""Check for AoO from casting a spell in a threatened square.
|
||||
|
||||
Returns True if the caster dropped or failed concentration (spell fizzles).
|
||||
"""
|
||||
for enemy in self._enemies_threatening(caster, caster.pos):
|
||||
if enemy.combatant.id in self._aoo_used:
|
||||
continue
|
||||
hp_before = caster.hp
|
||||
self._resolve_aoo(enemy, caster)
|
||||
if not caster.active:
|
||||
self._log(
|
||||
f"round {self._current_round} {caster.combatant.id}: "
|
||||
f"casting interrupted by AoO from {enemy.combatant.id}"
|
||||
)
|
||||
return True
|
||||
# Concentration check if damaged
|
||||
dmg = hp_before - caster.hp
|
||||
if dmg > 0 and not self._check_concentration(caster, spell_level, dmg):
|
||||
self._log(
|
||||
f"round {self._current_round} {caster.combatant.id}: "
|
||||
f"concentration check failed (DC {15 + 2 * spell_level}), spell fizzles"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_concentration(
|
||||
self, caster: CombatantState, spell_level: int, damage_taken: int
|
||||
) -> bool:
|
||||
"""Concentration check: DC = 15 + 2 * spell_level + damage_taken.
|
||||
|
||||
Combat Casting feat gives +4 bonus.
|
||||
"""
|
||||
dc = 15 + 2 * spell_level + damage_taken
|
||||
has_combat_casting = any(f.name == "Combat Casting" for f in caster.combatant.features)
|
||||
bonus = 4 if has_combat_casting else 0
|
||||
roll = self._rng.d20()
|
||||
# Use caster level + ability mod for concentration check
|
||||
caster_level = caster.combatant.level
|
||||
# For concentration, the check is d20 + caster_level + ability_mod (usually Con)
|
||||
con_mod = (caster.combatant.abilities.con_score - 10) // 2
|
||||
total = roll + caster_level + con_mod + bonus
|
||||
return total >= dc
|
||||
def can_step5_to_attack(self, state: CombatantState, target: CombatantState) -> bool:
|
||||
"""True if a 5-foot step toward ``target`` would put ``state`` in weapon range."""
|
||||
path = self._move_path(state, target)
|
||||
@@ -878,6 +921,10 @@ class CombatEngine:
|
||||
f"round {self._current_round} {caster.combatant.id}: "
|
||||
f"cast {spell_name} -> NO LINE OF EFFECT"
|
||||
)
|
||||
return
|
||||
# Check for AoO from casting in threatened square (before resolving spell)
|
||||
if self._check_casting_aoo(caster, spell.level):
|
||||
return
|
||||
# DC = 10 + spell level + caster's casting ability modifier
|
||||
ability_name = caster.combatant.casting_ability.lower()
|
||||
ability_score = getattr(caster.combatant.abilities, f"{ability_name}_score")
|
||||
|
||||
Reference in New Issue
Block a user