fix(rules): cover grants a Reflex-save bonus against area effects (SRD cover.md 'Cover and Reflex Saves': +2 vs standard cover - half the AC value, so +1 partial/+2 standard/+4 improved; soft cover grants none) - _area_save adds terrain cover//2 from the effect's point of origin (burst center, caster square for cone/line) to Reflex rolls; resolve_save gains an explicit flat_bonus
This commit is contained in:
@@ -3385,6 +3385,7 @@ class CombatEngine:
|
||||
dc: int,
|
||||
*,
|
||||
save_descriptors: frozenset[str] = frozenset(),
|
||||
flat_bonus: int = 0,
|
||||
) -> SaveResult:
|
||||
"""Roll a saving throw (fort/ref/will) against ``dc``.
|
||||
|
||||
@@ -3393,13 +3394,16 @@ class CombatEngine:
|
||||
target) are applied via ``resolve_modifiers`` — same bonus-type
|
||||
stacking rules as attacks. ``save_descriptors`` (e.g. {"fear"},
|
||||
{"poison"}) gates conditional modifiers like Bitter or Born Damned.
|
||||
``flat_bonus`` is an untyped situational bonus added to the total
|
||||
(used for the cover bonus on Reflex saves, CRB "Cover and Reflex
|
||||
Saves"); natural 1/20 still override the roll regardless of it.
|
||||
"""
|
||||
roll = self._rng.d20()
|
||||
base = getattr(state.combatant.saves, save_type)
|
||||
bonus = resolve_modifiers(
|
||||
self._stat_modifiers(state, save_type, save_descriptors=save_descriptors)
|
||||
)
|
||||
total = roll + base + bonus
|
||||
total = roll + base + bonus + flat_bonus
|
||||
success = roll != _NATURAL_ONE and (roll == _NATURAL_TWENTY or total >= dc)
|
||||
return SaveResult(success=success, roll=roll, total=total, dc=dc)
|
||||
|
||||
@@ -7083,13 +7087,21 @@ class CombatEngine:
|
||||
total_damage = 0
|
||||
total_conditions = 0
|
||||
total_buffs = 0
|
||||
# Reflex-save cover is measured from the effect's point of origin:
|
||||
# the burst center, or the caster's square for a cone/line (CRB
|
||||
# "Cover and Reflex Saves": the bonus applies "against attacks that
|
||||
# originate or burst out from a point on the other side of the cover
|
||||
# from you").
|
||||
origin_cells = (
|
||||
frozenset({center}) if effect.shape == "burst" else caster.occupied_cells()
|
||||
)
|
||||
|
||||
for affected_state in affected:
|
||||
if spell_resistance and not self._check_spell_resistance(caster, affected_state):
|
||||
continue
|
||||
creature_saved = self._area_save(
|
||||
affected_state, target, effect, saved=saved, dc=dc,
|
||||
save_type=save_type,
|
||||
save_type=save_type, origin_cells=origin_cells,
|
||||
)
|
||||
for sub_effect in effect.effects:
|
||||
target_live = self._stats[affected_state.combatant.id]
|
||||
@@ -7182,29 +7194,66 @@ class CombatEngine:
|
||||
saved: bool,
|
||||
dc: int = 15,
|
||||
save_type: SaveType = "none",
|
||||
origin_cells: frozenset[Pos] = frozenset(),
|
||||
) -> bool:
|
||||
"""Save result for one creature in an area effect.
|
||||
|
||||
PF1e (CRB): every creature in the area rolls the save the SPELL lists
|
||||
(Will / Fortitude / Reflex) — "save negates" and "save for half" both
|
||||
(Will / Fortitude / Reflex) - "save negates" and "save for half" both
|
||||
grant it; whether a success negates or halves is decided per sub-effect
|
||||
(``save_negates`` / ``half_on_save``), never by skipping the roll.
|
||||
The resolved primary target re-uses its already-rolled save; every
|
||||
other creature in the area — and every creature of a point-targeted
|
||||
burst (no primary target, U7b) — rolls its own.
|
||||
other creature in the area - and every creature of a point-targeted
|
||||
burst (no primary target, U7b) - rolls its own.
|
||||
|
||||
PF1e (CRB "Cover and Reflex Saves"): cover grants a Reflex bonus
|
||||
against effects that burst from a point on the other side of the
|
||||
cover - half the AC value (+1 partial / +2 standard / +4 improved);
|
||||
soft cover from creatures grants none (CRB "Soft Cover"). The bonus
|
||||
is measured from the effect's origin to the rolling creature.
|
||||
Documented simplification: the primary target's reused save was
|
||||
rolled on the generic spell path before the area resolved; for a
|
||||
target-centered burst the origin IS its own square (bonus 0), so the
|
||||
reuse only under-protects a cone/line primary standing behind cover
|
||||
from the caster.
|
||||
"""
|
||||
if save_type == "none":
|
||||
return False
|
||||
if affected_state is target:
|
||||
return saved
|
||||
flat_bonus = (
|
||||
self._reflex_cover_bonus(origin_cells, affected_state)
|
||||
if save_type == "ref"
|
||||
else 0
|
||||
)
|
||||
save_result = self.resolve_save(
|
||||
affected_state,
|
||||
save_type,
|
||||
dc,
|
||||
save_descriptors=save_descriptors_for_effects(effect.effects),
|
||||
flat_bonus=flat_bonus,
|
||||
)
|
||||
return save_result.success
|
||||
|
||||
def _reflex_cover_bonus(self, origin_cells: frozenset[Pos], state: CombatantState) -> int:
|
||||
"""Reflex-save cover bonus from an area effect's origin (terrain only).
|
||||
|
||||
CRB "Cover and Reflex Saves": "Cover grants you a +2 bonus on Reflex
|
||||
saves against attacks that originate or burst out from a point on the
|
||||
other side of the cover from you" - the Reflex bonus is half the AC
|
||||
bonus (partial +2 AC/+1 Ref, standard +4/+2, improved +8/+4). CRB
|
||||
"Soft Cover": creatures "provide no bonus on Reflex saves", so the
|
||||
interposed-creature set is empty here.
|
||||
"""
|
||||
cover = cover_bonus_cells(
|
||||
self._grid,
|
||||
origin_cells,
|
||||
state.occupied_cells(),
|
||||
ranged=True,
|
||||
occupied=frozenset(),
|
||||
)
|
||||
return cover // 2
|
||||
|
||||
def _apply_metamagic(
|
||||
self, spell: SpellSpec, metamagic_feats: tuple[str, ...]
|
||||
) -> tuple[SpellEffect, ...]:
|
||||
|
||||
Reference in New Issue
Block a user