feat(combat): expose class features and spell data in observation snapshot
This commit is contained in:
@@ -183,6 +183,29 @@ def _save_label(result: SaveResult | None) -> str:
|
||||
return "SAVED" if result.success else "FAILED"
|
||||
|
||||
|
||||
def _spell_effect_summary(effect: SpellEffect) -> str:
|
||||
"""Short serializable summary of one spell effect (never the description)."""
|
||||
if isinstance(effect, DamageEffect):
|
||||
parts = [dice_to_str(effect.formula), *effect.types]
|
||||
if effect.half_on_save:
|
||||
parts.append("half on save")
|
||||
return " ".join(parts)
|
||||
if isinstance(effect, ConditionEffect):
|
||||
return f"{effect.condition_name} {effect.duration_rounds}r"
|
||||
if isinstance(effect, HealEffect):
|
||||
return f"heal {dice_to_str(effect.formula)}"
|
||||
if isinstance(effect, BuffEffect):
|
||||
return f"buff {effect.duration_rounds}r"
|
||||
if isinstance(effect, AreaEffect):
|
||||
return f"area {effect.radius_ft}ft"
|
||||
return f"summon {effect.creature}"
|
||||
|
||||
|
||||
def _spell_summary(spell: SpellSpec) -> str:
|
||||
"""Short serializable summary of a spell's effects, joined with ', '."""
|
||||
return ", ".join(_spell_effect_summary(e) for e in spell.effects)
|
||||
|
||||
|
||||
_STEP_DELTAS: tuple[Pos, ...] = (
|
||||
(-1, -1),
|
||||
(-1, 0),
|
||||
@@ -637,6 +660,10 @@ class CombatEngine:
|
||||
}
|
||||
for attack in state.combatant.attacks
|
||||
),
|
||||
"features": tuple(sorted(f.name for f in state.combatant.features)),
|
||||
"spells": tuple(
|
||||
self._spell_snapshot(state, name) for name in state.combatant.spells
|
||||
),
|
||||
}
|
||||
|
||||
def _pair_snapshot(
|
||||
@@ -2561,6 +2588,36 @@ class CombatEngine:
|
||||
return
|
||||
self._charge(state, target, result.weapon)
|
||||
|
||||
def _spell_dc(self, caster: CombatantState, spell: SpellSpec) -> int:
|
||||
"""Spell save DC: 10 + spell level + casting ability modifier."""
|
||||
ability_name = caster.combatant.casting_ability.lower()
|
||||
ability_score = getattr(caster.combatant.abilities, f"{ability_name}_score")
|
||||
ability_mod = (ability_score - 10) // 2
|
||||
return 10 + spell.level + ability_mod
|
||||
|
||||
def _spell_snapshot(self, state: CombatantState, spell_name: str) -> dict[str, object]:
|
||||
"""One per-spell dict for the Observation, enriched from the registry."""
|
||||
spell = self._spell_registry.get(spell_name)
|
||||
if spell is None:
|
||||
return {
|
||||
"name": spell_name,
|
||||
"level": None,
|
||||
"school": "",
|
||||
"range_ft": 0,
|
||||
"save": "none",
|
||||
"dc": None,
|
||||
"summary": "",
|
||||
}
|
||||
return {
|
||||
"name": spell.name,
|
||||
"level": spell.level,
|
||||
"school": spell.school,
|
||||
"range_ft": spell_range_ft(spell, state.combatant.level),
|
||||
"save": spell.save,
|
||||
"dc": self._spell_dc(state, spell),
|
||||
"summary": _spell_summary(spell),
|
||||
}
|
||||
|
||||
def _cast_spell(
|
||||
self, caster: CombatantState, target: CombatantState | None, spell_name: str
|
||||
) -> None:
|
||||
@@ -2593,11 +2650,7 @@ class CombatEngine:
|
||||
if self._check_casting_aoo(caster, spell.level):
|
||||
return
|
||||
caster.spend_spell_slot(spell.level)
|
||||
# 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")
|
||||
ability_mod = (ability_score - 10) // 2
|
||||
dc = 10 + spell.level + ability_mod
|
||||
dc = self._spell_dc(caster, spell)
|
||||
save_result = (
|
||||
self.resolve_save(
|
||||
target, spell.save, dc, save_descriptors=spell_save_descriptors(spell)
|
||||
|
||||
Reference in New Issue
Block a user