feat(combat): area-of-effect spells

This commit is contained in:
2026-09-04 22:55:43 +02:00
parent 09062b37ea
commit 8ae344fd16
4 changed files with 155 additions and 16 deletions
+21 -8
View File
@@ -19,6 +19,7 @@ from pf1e_simulator.models import (
)
from pf1e_simulator.rng import ScriptedRng
from pf1e_simulator.spells import (
AreaEffect,
BuffEffect,
ConditionEffect,
DamageEffect,
@@ -132,9 +133,15 @@ class TestSpellLoading:
assert spell.range == "long"
assert spell.save == "ref"
assert spell.base_dc == 10
assert len(spell.effects) == 1
assert isinstance(spell.effects[0], DamageEffect)
assert spell.effects[0].half_on_save is True
assert isinstance(spell.effects[0], AreaEffect)
area_eff = spell.effects[0]
assert isinstance(area_eff, AreaEffect)
assert area_eff.radius_ft == 20
assert area_eff.save_for_half is True
assert len(area_eff.effects) == 1
inner = area_eff.effects[0]
assert isinstance(inner, DamageEffect)
assert inner.half_on_save is True
def test_hold_person_has_condition_effect(self) -> None:
spell = load_spell(_SPELLS_DIR / "hold_person.json")
@@ -211,7 +218,8 @@ class TestFireballSave:
def test_failed_save_full_damage(self) -> None:
caster = _make_caster()
target = _make_target(hp_max=200)
engine, c, t = _make_engine(caster, target, [2, 3, 3, 3, 3, 3, 3])
# 2 save rolls (2 targets in area) + 6 damage dice
engine, c, t = _make_engine(caster, target, [2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
hp_before = t.hp
engine._cast_spell(c, t, "Fireball")
assert t.hp == hp_before - (3 + 3 + 3 + 3 + 3 + 3)
@@ -219,7 +227,8 @@ class TestFireballSave:
def test_successful_save_half_damage(self) -> None:
caster = _make_caster()
target = _make_target(hp_max=200)
engine, c, t = _make_engine(caster, target, [20, 3, 3, 3, 3, 3, 3])
# 2 save rolls (2 targets in area) + 6 damage dice
engine, c, t = _make_engine(caster, target, [20, 20, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
hp_before = t.hp
engine._cast_spell(c, t, "Fireball")
full = 3 + 3 + 3 + 3 + 3 + 3
@@ -228,11 +237,13 @@ class TestFireballSave:
def test_transcript_shows_saved(self) -> None:
caster = _make_caster()
target = _make_target()
engine, c, t = _make_engine(caster, target, [20, 3, 3, 3, 3, 3, 3])
# 2 save rolls (2 targets in area) + 6 damage dice
engine, c, t = _make_engine(caster, target, [20, 20, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
engine._cast_spell(c, t, "Fireball")
assert any("SAVED" in line for line in engine._transcript)
class TestHoldPerson:
"""Given: a caster casting Hold Person (will negates, paralyzed)
When: target fails save
@@ -375,7 +386,8 @@ class TestSpellDcScalesWithLevel:
def test_dc_is_ten_plus_spell_level_plus_ability_mod(self) -> None:
caster = _make_caster(level=5) # int 18 -> mod +4
target = _make_target()
engine, c, t = _make_engine(caster, target, [20, 3, 3, 3, 3, 3, 3])
# 2 save rolls (2 targets in area) + 6 damage dice
engine, c, t = _make_engine(caster, target, [20, 20, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
engine._cast_spell(c, t, "Fireball") # fireball is level 3
# DC = 10 + spell_level(3) + int_mod(4) = 17
assert any("DC17" in line for line in engine._transcript)
@@ -393,7 +405,8 @@ class TestSpellDamageWithDr:
target_with_dr = target.model_copy(update={
"dr": DamageReduction(amount=5, bypass=frozenset()),
})
engine, c, t = _make_engine(caster, target_with_dr, [2, 3, 3, 3, 3, 3, 3])
# 2 save rolls (2 targets in area) + 6 damage dice
engine, c, t = _make_engine(caster, target_with_dr, [2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
hp_before = t.hp
engine._cast_spell(c, t, "Fireball")
# Save fails (2), 6d6 rolls all 3 = 18 damage, DR 5/- -> 13 damage