fix(rules): metamagic no longer raises the save DC - only Heighten Spell does (SRD empower-spell: saving throws not affected; heighten-spell: DCs calculated at the heightened level, modeled at its minimum +1 slot adjustment)
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
"""PF1e conformance: spell-casting flow fixes B7 / B14 / B24.
|
||||
|
||||
B7 (CRB, Metamagic Feats): metamagic is chosen PER CAST, costs slot levels
|
||||
(Empower +2, Maximize +3, Extend +1), the DC uses the slot level, and
|
||||
only feats the caster knows can apply.
|
||||
(Empower +2, Maximize +3, Extend +1, Heighten +1 modeled minimum), and
|
||||
only feats the caster knows can apply. The save DC uses the spell's
|
||||
BASE level - metamagic does not raise it (SRD empower: "Saving throws
|
||||
and opposed rolls are not affected"); only Heighten Spell raises the DC
|
||||
to the heightened level (SRD heighten-spell).
|
||||
B14 (CRB, Magic, Range: Touch): delivering a touch spell to an unwilling
|
||||
creature requires a melee touch attack (miss = spell wasted); a willing
|
||||
target (self/ally) needs no roll.
|
||||
@@ -182,13 +185,16 @@ def _state(combatant: Combatant, side: str, pos: tuple[int, int]) -> CombatantSt
|
||||
|
||||
|
||||
class TestMetamagicPerCast:
|
||||
"""B7: metamagic is chosen per cast, costs slot levels, and the DC uses
|
||||
the slot level (CRB, Metamagic Feats)."""
|
||||
"""B7: metamagic is chosen per cast and costs slot levels, but the save
|
||||
DC stays at the spell's base level - only Heighten Spell raises it
|
||||
(SRD, Metamagic Feats)."""
|
||||
|
||||
def test_empower_costs_slot_two_levels_higher_and_dc_uses_it(self) -> None:
|
||||
def test_empower_costs_slot_two_levels_higher_but_dc_stays_base(self) -> None:
|
||||
# Given: a caster who knows Empower Spell with a level-3 slot
|
||||
# When: casting Bolt (lvl 1) with Empower Spell
|
||||
# Then: the level-3 slot burns and the DC is 10 + 3 + int 4 = 17.
|
||||
# Then: the level-3 slot burns but the DC is 10 + 1 + int 4 = 15
|
||||
# (SRD empower-spell: "Saving throws and opposed rolls are not
|
||||
# affected" - the higher slot does not raise the DC).
|
||||
caster = _caster(metamagic=["Empower Spell"])
|
||||
c, f = _state(caster, "players", (0, 0)), _state(_foe(), "monsters", (3, 0))
|
||||
engine = _engine([c, f], [3, 3]) # 2d4 -> 6, empowered -> 9
|
||||
@@ -196,21 +202,23 @@ class TestMetamagicPerCast:
|
||||
assert c.spell_slots_left[3] == 0
|
||||
assert c.spell_slots_left[1] == 1
|
||||
assert f.hp == 100 - 9
|
||||
assert any("DC17" in line for line in engine._transcript)
|
||||
assert any("DC15" in line for line in engine._transcript)
|
||||
assert not any("DC17" in line for line in engine._transcript)
|
||||
|
||||
def test_maximize_costs_slot_three_levels_higher(self) -> None:
|
||||
def test_maximize_costs_slot_three_levels_higher_dc_stays_base(self) -> None:
|
||||
# Given: a caster who knows Maximize Spell
|
||||
# When: casting Bolt maximized (2d4 max = 8)
|
||||
# Then: the level-4 slot burns and the DC is 10 + 4 + 4 = 18.
|
||||
# Then: the level-4 slot burns but the DC is 10 + 1 + 4 = 15.
|
||||
caster = _caster(metamagic=["Maximize Spell"])
|
||||
c, f = _state(caster, "players", (0, 0)), _state(_foe(), "monsters", (3, 0))
|
||||
engine = _engine([c, f], [1, 1]) # dice overwritten by maximize
|
||||
engine._cast_spell(c, f, "Bolt", metamagic=("Maximize Spell",))
|
||||
assert c.spell_slots_left[4] == 0
|
||||
assert f.hp == 100 - 8
|
||||
assert any("DC18" in line for line in engine._transcript)
|
||||
assert any("DC15" in line for line in engine._transcript)
|
||||
assert not any("DC18" in line for line in engine._transcript)
|
||||
|
||||
def test_extend_costs_one_slot_level_and_doubles_duration(self) -> None:
|
||||
def test_extend_costs_one_slot_level_doubles_duration_and_dc_stays_base(self) -> None:
|
||||
# Given: a caster who knows Extend Spell, self-targeting a touch buff
|
||||
# When: casting Grace (2r) extended
|
||||
# Then: the level-2 slot burns and the condition lasts 4 rounds.
|
||||
@@ -221,6 +229,30 @@ class TestMetamagicPerCast:
|
||||
assert c.spell_slots_left[2] == 0
|
||||
assert c.spell_slots_left[1] == 1
|
||||
assert c.active_conditions[0].rounds_left == 4
|
||||
# Extend raises the slot cost but never the DC: a hostile extended
|
||||
# Bolt still DCs at its base level.
|
||||
c2 = _state(caster, "players", (0, 0))
|
||||
f2 = _state(_foe(), "monsters", (3, 0))
|
||||
engine2 = _engine([c2, f2], [3, 3])
|
||||
engine2._cast_spell(c2, f2, "Bolt", metamagic=("Extend Spell",))
|
||||
assert any("DC15" in line for line in engine2._transcript)
|
||||
assert not any("DC16" in line for line in engine2._transcript)
|
||||
|
||||
def test_heighten_spell_raises_the_dc_to_the_heightened_level(self) -> None:
|
||||
# Given: a caster who knows Heighten Spell (modeled at its minimum
|
||||
# +1 adjustment - the engine's per-cast API carries feat names only)
|
||||
# When: casting Bolt (lvl 1) heightened
|
||||
# Then: the level-2 slot burns AND the DC is 10 + 2 + int 4 = 16
|
||||
# (SRD heighten-spell: "All effects dependent on spell level (such
|
||||
# as saving throw DCs [...]) are calculated according to the
|
||||
# heightened level").
|
||||
caster = _caster(metamagic=["Heighten Spell"])
|
||||
c, f = _state(caster, "players", (0, 0)), _state(_foe(), "monsters", (3, 0))
|
||||
engine = _engine([c, f], [3, 3])
|
||||
engine._cast_spell(c, f, "Bolt", metamagic=("Heighten Spell",))
|
||||
assert c.spell_slots_left[2] == 0
|
||||
assert c.spell_slots_left[1] == 1
|
||||
assert any("DC16" in line for line in engine._transcript)
|
||||
|
||||
def test_unknown_metamagic_feat_is_never_applied(self) -> None:
|
||||
# Given: a caster who knows NO metamagic
|
||||
|
||||
Reference in New Issue
Block a user