fix(rules): summon duration scales with caster level (SRD summon-monster: 1 round/level (D)) - SummonEffect gains duration_rounds_per_level resolved at cast time, summon_monster_i/ii JSON updated
This commit is contained in:
+76
-1
@@ -20,7 +20,7 @@ from pf1e_simulator.models import (
|
||||
Saves,
|
||||
)
|
||||
from pf1e_simulator.rng import SeededRng
|
||||
from pf1e_simulator.spells import SpellSpec, SummonEffect
|
||||
from pf1e_simulator.spells import SPELLS_DIR, SpellSpec, SummonEffect, load_spell
|
||||
|
||||
|
||||
def _make_caster() -> Combatant:
|
||||
@@ -260,3 +260,78 @@ class TestSummonedCreatureExpires:
|
||||
assert not wolf_states[0].active, (
|
||||
"Wolf should be inactive after expiration"
|
||||
)
|
||||
|
||||
|
||||
class TestSummonDurationScalesWithCasterLevel:
|
||||
"""SRD spells/summon-monster-1.md: "Duration 1 round/level (D)" - the
|
||||
summon's rounds resolve at cast time from the caster's level, mirroring
|
||||
the ConditionEffect per-level duration convention."""
|
||||
|
||||
def _cast(self, caster_level: int, *, per_level: int) -> CombatEngine:
|
||||
caster = _make_caster()
|
||||
caster = caster.model_copy(update={"level": caster_level})
|
||||
enemy = _make_enemy()
|
||||
c = CombatantState(combatant=caster, side="players", pos=(0, 0), hp=caster.hp_max)
|
||||
e = CombatantState(combatant=enemy, side="monsters", pos=(5, 0), hp=enemy.hp_max)
|
||||
spell = SpellSpec(
|
||||
name="Summon Monster II",
|
||||
level=2,
|
||||
school="conjuration",
|
||||
range="close",
|
||||
effects=(
|
||||
SummonEffect(
|
||||
creature="data/monsters/celestial_wolf.json",
|
||||
duration_rounds=1,
|
||||
duration_rounds_per_level=per_level,
|
||||
),
|
||||
),
|
||||
)
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
spec = MapSpec(name="test", terrain=tuple(["." * 12] * 12), legend=legend)
|
||||
engine = CombatEngine(
|
||||
SeededRng(1), Grid.from_spec(spec), [c, e],
|
||||
spell_registry={"Summon Monster II": spell},
|
||||
)
|
||||
engine._cast_spell(c, e, "Summon Monster II")
|
||||
return engine
|
||||
|
||||
def test_duration_equals_caster_level_rounds(self) -> None:
|
||||
# Given/When: a level 5 caster summons with 1 round/level
|
||||
# Then: the wolf lives for 5 rounds.
|
||||
engine = self._cast(5, per_level=1)
|
||||
wolf = next(s for s in engine._states if s.combatant.id == "celestial_wolf")
|
||||
assert wolf.summon_duration == 5
|
||||
|
||||
def test_duration_scales_for_a_higher_caster(self) -> None:
|
||||
# Given: a level 9 caster
|
||||
# Then: 9 rounds - the duration is not the fixed JSON value.
|
||||
engine = self._cast(9, per_level=1)
|
||||
wolf = next(s for s in engine._states if s.combatant.id == "celestial_wolf")
|
||||
assert wolf.summon_duration == 9
|
||||
|
||||
def test_fixed_duration_still_used_without_per_level_field(self) -> None:
|
||||
# Given: a summon declaring only a fixed duration (per_level = 0)
|
||||
# Then: the fixed value applies unchanged (pre-conformance fallback).
|
||||
caster = _make_caster()
|
||||
enemy = _make_enemy()
|
||||
c = CombatantState(combatant=caster, side="players", pos=(0, 0), hp=caster.hp_max)
|
||||
e = CombatantState(combatant=enemy, side="monsters", pos=(5, 0), hp=enemy.hp_max)
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
spec = MapSpec(name="test", terrain=tuple(["." * 12] * 12), legend=legend)
|
||||
reg = {"Summon Monster II": _make_summon_spell(duration_rounds=3)}
|
||||
engine = CombatEngine(
|
||||
SeededRng(1), Grid.from_spec(spec), [c, e], spell_registry=reg
|
||||
)
|
||||
engine._cast_spell(c, e, "Summon Monster II")
|
||||
wolf = next(s for s in engine._states if s.combatant.id == "celestial_wolf")
|
||||
assert wolf.summon_duration == 3
|
||||
|
||||
def test_shipped_summon_spells_declare_per_level_duration(self) -> None:
|
||||
# Given: the shipped spell data
|
||||
# When: Summon Monster I/II are loaded
|
||||
# Then: they carry the SRD 1 round/level duration (no fixed value).
|
||||
for name in ("summon_monster_i.json", "summon_monster_ii.json"):
|
||||
spell = load_spell(SPELLS_DIR / name)
|
||||
effect = spell.effects[0]
|
||||
assert isinstance(effect, SummonEffect)
|
||||
assert effect.duration_rounds_per_level == 1
|
||||
|
||||
Reference in New Issue
Block a user