fix(rules): correct P1 PF1e conformance bugs (area saves, concentration, prone, slots, helpless, nauseated)
This commit is contained in:
@@ -73,6 +73,9 @@ def _make_caster(cid: str = "caster", *, level: int = 5) -> Combatant:
|
||||
saves=Saves(fort=3, ref=4, will=4),
|
||||
attacks=[attack],
|
||||
casting_ability="int",
|
||||
# B4 conformance: _cast_spell now refuses without a slot; test
|
||||
# casters carry a generous pool so fixtures stay about spells.
|
||||
spell_slots={0: 99, 1: 99, 2: 99, 3: 99, 4: 99, 5: 99, 6: 99},
|
||||
)
|
||||
|
||||
|
||||
@@ -260,6 +263,64 @@ class TestFireballSave:
|
||||
assert any("SAVED" in line for line in engine._transcript)
|
||||
|
||||
|
||||
class TestAreaSavesPerSpellSaveType:
|
||||
"""B1 (PF1e conformance): every creature in an area rolls the save the
|
||||
SPELL lists (Will/Fort/Ref) — "save negates" areas are no longer
|
||||
save-free, and secondary creatures do not always roll Reflex."""
|
||||
|
||||
def test_sleep_secondary_creature_rolls_will_and_negates(self) -> None:
|
||||
# Sleep (Will negates, save_for_half=False): primary target saves on
|
||||
# natural 20 (first roll), the caster — second creature in the area —
|
||||
# rolls its own Will and fails on natural 1 (second roll).
|
||||
caster = _make_caster()
|
||||
target = _make_target(hp_max=100)
|
||||
engine, c, t = _make_engine(
|
||||
caster, target, [20, 1], target_pos=(2, 0)
|
||||
)
|
||||
engine._cast_spell(c, t, "Sleep")
|
||||
assert not any(ac.condition.name == "asleep" for ac in t.active_conditions)
|
||||
assert any(ac.condition.name == "asleep" for ac in c.active_conditions)
|
||||
|
||||
def test_color_spray_primary_save_is_not_discarded(self) -> None:
|
||||
# Color Spray (Will negates): the primary's pre-rolled save decides —
|
||||
# natural 1 fails (stunned), the caster's own natural 20 succeeds.
|
||||
caster = _make_caster()
|
||||
target = _make_target(hp_max=100)
|
||||
engine, c, t = _make_engine(
|
||||
caster, target, [1, 20], target_pos=(2, 0)
|
||||
)
|
||||
engine._cast_spell(c, t, "Color Spray")
|
||||
assert any(ac.condition.name == "stunned" for ac in t.active_conditions)
|
||||
assert not any(ac.condition.name == "stunned" for ac in c.active_conditions)
|
||||
|
||||
def test_web_secondary_creature_rolls_reflex(self) -> None:
|
||||
# Web (Reflex negates): secondary creature rolls its own Reflex.
|
||||
caster = _make_caster()
|
||||
target = _make_target(hp_max=100)
|
||||
engine, c, t = _make_engine(
|
||||
caster, target, [20, 1], target_pos=(2, 0)
|
||||
)
|
||||
engine._cast_spell(c, t, "Web")
|
||||
assert not any(ac.condition.name == "grappled" for ac in t.active_conditions)
|
||||
assert any(ac.condition.name == "grappled" for ac in c.active_conditions)
|
||||
|
||||
def test_sound_burst_secondary_rolls_fortitude_not_reflex(self) -> None:
|
||||
# Sound Burst (Fortitude partial): caster has ref +4, fort +3 and the
|
||||
# DC is 16 — d20 12 passes Reflex but fails Fortitude. The caster
|
||||
# being stunned proves the FORT save was rolled (B1's hardcoded ref
|
||||
# would have "saved" and left the caster unstunned).
|
||||
caster = _make_caster()
|
||||
target = _make_target(hp_max=200)
|
||||
# rolls: primary (target) fort 20 = save; caster fort 12 = fail;
|
||||
# then the 1d8 damage dice for each creature in state order.
|
||||
engine, c, t = _make_engine(
|
||||
caster, target, [20, 12, 3, 3], target_pos=(2, 0)
|
||||
)
|
||||
engine._cast_spell(c, t, "Sound Burst")
|
||||
assert any(ac.condition.name == "stunned" for ac in c.active_conditions)
|
||||
assert t.hp == 200 - 1 # saved: 3 // 2
|
||||
|
||||
|
||||
class TestTargetedSpellSaveNegates:
|
||||
"""Given: a targeted spell with a save (half_on_save=False)
|
||||
When: the target succeeds / fails its save
|
||||
|
||||
Reference in New Issue
Block a user