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:
@@ -248,10 +248,14 @@ _IMPROVED_MANEUVER_FEATS: dict[str, str] = {
|
||||
}
|
||||
# PF1e (CRB, Metamagic Feats): a metamagic spell occupies a slot of its
|
||||
# level + the feat's adjustment. B7: applied per cast, never blanket-on.
|
||||
# Heighten Spell's RAW adjustment is variable (the caster picks the
|
||||
# heightened level); the engine's per-cast API carries feat names only, so
|
||||
# it is modeled at its minimum +1 (documented simplification).
|
||||
_METAMAGIC_SLOT_ADJUST: dict[str, int] = {
|
||||
"Empower Spell": 2,
|
||||
"Maximize Spell": 3,
|
||||
"Extend Spell": 1,
|
||||
"Heighten Spell": 1,
|
||||
}
|
||||
_COMBAT_CASTING_FEAT = "Combat Casting" # PF1e: +4 concentration (defensive/grappled only)
|
||||
# PF1e: skill checks that take Dexterity-based penalties (the "dex_skills"
|
||||
@@ -664,8 +668,10 @@ class PendingCast:
|
||||
your turn in the round after you began casting the spell. You then act
|
||||
normally after the spell is completed." Everything the delivery still
|
||||
needs is frozen at the start of the casting (the slot is already
|
||||
spent): ``slot_level`` carries the metamagic-inflated level (B7) the
|
||||
save DC uses, ``hp_at_cast`` is the caster's hp when the casting began
|
||||
spent): ``slot_level`` carries the metamagic-inflated level (B7) that
|
||||
drives the concentration checks and the Heighten Spell DC (other
|
||||
metamagic does not raise the DC - see _spell_dc), ``hp_at_cast`` is the
|
||||
caster's hp when the casting began
|
||||
— the net hp lost before resolution is the interruption damage that
|
||||
forces the concentration check (CRB "Concentration": injured while
|
||||
casting, DC 10 + damage + spell level).
|
||||
@@ -5867,15 +5873,30 @@ class CombatEngine:
|
||||
return (ability_score - 10) // 2
|
||||
|
||||
def _spell_dc(
|
||||
self, caster: CombatantState, spell: SpellSpec, *, slot_level: int | None = None
|
||||
self,
|
||||
caster: CombatantState,
|
||||
spell: SpellSpec,
|
||||
*,
|
||||
slot_level: int | None = None,
|
||||
metamagic: tuple[str, ...] = (),
|
||||
) -> int:
|
||||
"""Spell save DC: 10 + spell level + casting ability modifier.
|
||||
|
||||
PF1e (CRB, B7): a metamagic spell uses the level of the slot cast
|
||||
from, so ``slot_level`` overrides the spell's base level when given.
|
||||
PF1e (SRD, Metamagic Feats): a metamagic spell occupies a higher
|
||||
slot but its save DC uses the spell's BASE level - "An empowered
|
||||
spell uses up a spell slot two levels higher than the spell's
|
||||
actual level" while "Saving throws and opposed rolls are not
|
||||
affected" (feats/empower-spell.md). The one exception is Heighten
|
||||
Spell: "Unlike other metamagic feats, Heighten Spell actually
|
||||
increases the effective level of the spell [...] All effects
|
||||
dependent on spell level (such as saving throw DCs [...]) are
|
||||
calculated according to the heightened level"
|
||||
(feats/heighten-spell.md) - so ``slot_level`` (the level the spell
|
||||
is cast at) feeds the DC only when Heighten Spell is applied.
|
||||
"""
|
||||
level = spell.level if slot_level is None else slot_level
|
||||
return 10 + level + self._casting_ability_mod(caster)
|
||||
if "Heighten Spell" in metamagic and slot_level is not None:
|
||||
return 10 + slot_level + self._casting_ability_mod(caster)
|
||||
return 10 + spell.level + self._casting_ability_mod(caster)
|
||||
|
||||
def _spell_snapshot(self, state: CombatantState, spell_name: str) -> dict[str, object]:
|
||||
"""One per-spell dict for the Observation, enriched from the registry."""
|
||||
@@ -6344,14 +6365,18 @@ class CombatEngine:
|
||||
|
||||
``metamagic`` holds the feats chosen for THIS cast (already filtered
|
||||
to known ones, B7); ``slot_level`` is the spell's level in the slot
|
||||
used — the save DC is computed from it (PF1e CRB: a metamagic spell
|
||||
uses the DC of the higher slot level). ``spell_crit`` is a confirmed
|
||||
used - it drives the slot cost and the concentration checks, but NOT
|
||||
the save DC: metamagic other than Heighten Spell does not raise the
|
||||
DC (SRD feats/empower-spell.md: "Saving throws and opposed rolls are
|
||||
not affected"; feats/heighten-spell.md: only Heighten Spell
|
||||
"increases the effective level of the spell", and its DC is computed
|
||||
from the heightened level). ``spell_crit`` is a confirmed
|
||||
touch-delivery critical: the spell's damage dice are doubled (CRB
|
||||
"Range — Touch": "deals double damage on a successful critical hit").
|
||||
``attack_penalty`` (Spell Combat) folds into each projectile's ranged
|
||||
touch attack for multi-projectile ray spells.
|
||||
"""
|
||||
dc = self._spell_dc(caster, spell, slot_level=slot_level)
|
||||
dc = self._spell_dc(caster, spell, slot_level=slot_level, metamagic=metamagic)
|
||||
save_result = (
|
||||
self.resolve_save(
|
||||
target, spell.save, dc, save_descriptors=spell_save_descriptors(spell)
|
||||
|
||||
Reference in New Issue
Block a user