feat(combat): area-of-effect spells

This commit is contained in:
2026-09-04 22:55:43 +02:00
parent cd09aa1087
commit 7e1aa25bc1
4 changed files with 155 additions and 16 deletions
+99
View File
@@ -83,6 +83,7 @@ from pf1e_simulator.conditions import CONDITIONS_BY_NAME
from pf1e_simulator.effects import ActiveBuff, ActiveCondition, StatModifier, resolve_modifiers
from pf1e_simulator.models import PREDEFINED_SKILLS
from pf1e_simulator.spells import (
AreaEffect,
BuffEffect,
ConditionEffect,
DamageEffect,
@@ -1040,6 +1041,8 @@ class CombatEngine:
maximize: bool = False,
saved: bool,
) -> str:
if isinstance(effect, AreaEffect):
return self._apply_area_effect(effect, target, caster_live, target_live, spell_name, saved, empower=empower, maximize=maximize)
if isinstance(effect, DamageEffect):
raw = effect.formula.roll(self._rng)
if maximize:
@@ -1078,6 +1081,7 @@ class CombatEngine:
target.hp = min(target.hp + heal, target.combatant.hp_max)
actual = target.hp - old_hp
return f"+{actual} HP"
# BuffEffect
target.active_buffs.append(
ActiveBuff(
modifiers=effect.modifiers,
@@ -1089,6 +1093,70 @@ class CombatEngine:
target.effects.extend(effect.modifiers)
return f"buff {len(effect.modifiers)} mods ({effect.duration_rounds}r)"
def _apply_area_effect(
self,
effect: AreaEffect,
target: CombatantState,
caster_live: _LiveStats,
target_live: _LiveStats,
spell_name: str,
saved: bool,
*,
empower: bool = False,
maximize: bool = False,
) -> str:
"""Apply an area effect to all creatures within radius.
The target parameter is used as the center point for the burst.
"""
radius_cells = effect.radius_ft // _SQUARE_FT
center = target.pos
# Find all creatures in radius
affected: list[CombatantState] = []
for state in self._states:
if not state.active:
continue
dist = self._grid.distance(center, state.pos)
if dist <= radius_cells:
affected.append(state)
if not affected:
return "no targets in area"
total_damage = 0
total_conditions = 0
total_buffs = 0
for affected_state in affected:
# Each creature gets a save
creature_saved = False
if effect.save_for_half:
# Use the primary target's save result for the "primary" target
# Others roll their own
if affected_state is target:
creature_saved = saved
else:
# Roll save for this creature
# For fireball, it's a Reflex save
save_result = self.resolve_save(affected_state, "ref", 15) # DC from spell
creature_saved = save_result.success
for sub_effect in effect.effects:
result = self._apply_single_spell_effect(
sub_effect, affected_state, caster_live, self._stats[affected_state.combatant.id],
spell_name=spell_name, saved=creature_saved, empower=empower, maximize=maximize
)
if "damage" in result:
total_damage += int(result.split()[0])
elif "damage" not in result and result:
if "negated" not in result and "buff" not in result:
total_conditions += 1
elif "buff" in result:
total_buffs += 1
return f"area: {len(affected)} targets, {total_damage} total damage, {total_conditions} conditions, {total_buffs} buffs"
def _apply_metamagic(
self, spell: SpellSpec, metamagic_feats: list[str]
) -> tuple[SpellEffect, ...]:
@@ -1116,6 +1184,37 @@ class CombatEngine:
elif isinstance(effect, HealEffect):
# Empower/Maximize also apply to healing
new_effects.append(effect)
elif isinstance(effect, AreaEffect):
new_sub_effects = []
for sub in effect.effects:
if isinstance(sub, DamageEffect):
new_sub_effects.append(sub)
elif isinstance(sub, ConditionEffect):
new_duration = sub.duration_rounds * 2 if has_extend else sub.duration_rounds
new_sub_effects.append(
ConditionEffect(
condition_name=sub.condition_name,
duration_rounds=new_duration,
save_negates=sub.save_negates,
)
)
elif isinstance(sub, HealEffect):
new_sub_effects.append(sub)
else: # BuffEffect
new_duration = sub.duration_rounds * 2 if has_extend else sub.duration_rounds
new_sub_effects.append(
BuffEffect(
modifiers=sub.modifiers,
duration_rounds=new_duration,
)
)
new_effects.append(
AreaEffect(
radius_ft=effect.radius_ft,
effects=tuple(new_sub_effects),
save_for_half=effect.save_for_half,
)
)
else: # BuffEffect
new_duration = effect.duration_rounds * 2 if has_extend else effect.duration_rounds
new_effects.append(