diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 18274f7..ac62c59 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -7165,8 +7165,6 @@ class CombatEngine: self, effect: SpellEffect, *, has_extend: bool ) -> SpellEffect: """Apply Extend Spell to one effect (recursing into AreaEffect sub-effects).""" - if isinstance(effect, (DamageEffect, HealEffect)): - return effect if isinstance(effect, ConditionEffect): duration = effect.duration_rounds * 2 if has_extend else effect.duration_rounds per_level = ( @@ -7210,6 +7208,23 @@ class CombatEngine: duration_rounds_per_level=duration, half_on_save=effect.half_on_save, ) + if isinstance(effect, SummonEffect): + # PF1e (SRD feats/extend-spell.md): "An extended spell lasts + # twice as long as normal. A spell with a duration of + # concentration, instantaneous, or permanent is not affected" - + # summon monster's "1 round/level (D)" is none of those, so + # Extend doubles it (both the fixed and the per-level form). + duration = effect.duration_rounds * 2 if has_extend else effect.duration_rounds + per_level = ( + effect.duration_rounds_per_level * 2 + if has_extend + else effect.duration_rounds_per_level + ) + return SummonEffect( + creature=effect.creature, + duration_rounds=duration, + duration_rounds_per_level=per_level, + ) if isinstance(effect, AreaEffect): return AreaEffect( shape=effect.shape, diff --git a/tests/test_summoning.py b/tests/test_summoning.py index c77ccac..4b5e6d8 100644 --- a/tests/test_summoning.py +++ b/tests/test_summoning.py @@ -335,3 +335,70 @@ class TestSummonDurationScalesWithCasterLevel: effect = spell.effects[0] assert isinstance(effect, SummonEffect) assert effect.duration_rounds_per_level == 1 + + +class TestExtendSpellOnSummon: + """SRD feats/extend-spell.md: "An extended spell lasts twice as long as + normal. A spell with a duration of concentration, instantaneous, or + permanent is not affected" - summon monster's "1 round/level (D)" is + none of those, so Extend must double it.""" + + def test_extend_doubles_the_per_level_summon_duration(self) -> None: + # Given: a level 5 caster who knows Extend Spell + # When: summoning with Extend Spell (1 round/level -> 2 rounds/level) + # Then: the wolf is bound for 10 rounds, and the level-3 slot burns. + caster = _make_caster().model_copy( + update={ + "metamagic_feats": ["Extend Spell"], + "spell_slots": {1: 3, 2: 3, 3: 3}, + } + ) + 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=1, + ), + ), + ) + 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", metamagic=("Extend Spell",)) + wolf = next(s for s in engine._states if s.combatant.id == "celestial_wolf") + assert wolf.summon_duration == 10 + assert c.spell_slots_left[3] == 2 # extended: level-3 slot + + def test_extend_doubles_a_fixed_summon_duration(self) -> None: + # Given: a summon with only a fixed duration (per_level = 0) + # When: the summon is cast extended (feat applied to this cast) + # Then: the fixed rounds double (2 -> 4). + caster = _make_caster().model_copy( + update={ + "metamagic_feats": ["Extend Spell"], + "spell_slots": {1: 3, 2: 3, 3: 3}, + } + ) + 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) + reg = {"Summon Monster II": _make_summon_spell(duration_rounds=2)} + 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=reg + ) + engine._cast_spell(c, e, "Summon Monster II", metamagic=("Extend Spell",)) + wolf = next(s for s in engine._states if s.combatant.id == "celestial_wolf") + assert wolf.summon_duration == 4