fix(rules): Extend Spell doubles SummonEffect duration (SRD extend-spell: lasts twice as long; summon's 1 round/level is neither instantaneous, concentration nor permanent)
This commit is contained in:
@@ -7165,8 +7165,6 @@ class CombatEngine:
|
|||||||
self, effect: SpellEffect, *, has_extend: bool
|
self, effect: SpellEffect, *, has_extend: bool
|
||||||
) -> SpellEffect:
|
) -> SpellEffect:
|
||||||
"""Apply Extend Spell to one effect (recursing into AreaEffect sub-effects)."""
|
"""Apply Extend Spell to one effect (recursing into AreaEffect sub-effects)."""
|
||||||
if isinstance(effect, (DamageEffect, HealEffect)):
|
|
||||||
return effect
|
|
||||||
if isinstance(effect, ConditionEffect):
|
if isinstance(effect, ConditionEffect):
|
||||||
duration = effect.duration_rounds * 2 if has_extend else effect.duration_rounds
|
duration = effect.duration_rounds * 2 if has_extend else effect.duration_rounds
|
||||||
per_level = (
|
per_level = (
|
||||||
@@ -7210,6 +7208,23 @@ class CombatEngine:
|
|||||||
duration_rounds_per_level=duration,
|
duration_rounds_per_level=duration,
|
||||||
half_on_save=effect.half_on_save,
|
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):
|
if isinstance(effect, AreaEffect):
|
||||||
return AreaEffect(
|
return AreaEffect(
|
||||||
shape=effect.shape,
|
shape=effect.shape,
|
||||||
|
|||||||
@@ -335,3 +335,70 @@ class TestSummonDurationScalesWithCasterLevel:
|
|||||||
effect = spell.effects[0]
|
effect = spell.effects[0]
|
||||||
assert isinstance(effect, SummonEffect)
|
assert isinstance(effect, SummonEffect)
|
||||||
assert effect.duration_rounds_per_level == 1
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user