fix(rules): correct P1 PF1e conformance bugs (area saves, concentration, prone, slots, helpless, nauseated)

This commit is contained in:
2026-09-10 20:52:28 +02:00
parent 1ca5aed758
commit 5afe495c4a
15 changed files with 790 additions and 86 deletions
+27 -9
View File
@@ -108,7 +108,9 @@ def _cast_scene(
(distance 0) but not foe_out (distance 5) nor the caster (distance 9).
"""
caster = make_state(
make_combatant("caster", hp=100, spells=["Fireball"]), "players", (7, 0)
make_combatant(
"caster", hp=100, spells=["Fireball"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (7, 0)
)
foe_in = make_state(make_combatant("foe_in", hp=100), "monsters", (0, 4))
foe_out = make_state(make_combatant("foe_out", hp=100), "monsters", (4, 7))
@@ -187,7 +189,9 @@ def test_point_cast_without_line_of_effect_rejected() -> None:
When: cast_spell at that unseen point
Then: invalid_destination rejection without casting."""
caster = make_state(
make_combatant("caster", hp=100, spells=["Fireball"]), "players", (0, 0)
make_combatant(
"caster", hp=100, spells=["Fireball"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (0, 0)
)
foe = make_state(make_combatant("foe", hp=100), "monsters", (0, 6))
eng = _engine([caster, foe], [], grid=_walled_grid())
@@ -216,7 +220,9 @@ def test_point_cast_rolls_a_save_per_creature_in_area() -> None:
When: a target-less point cast at (0,4)
Then: no primary save is rolled (queue would shift otherwise) and EACH
creature rolls its own: foe_a fails (2) full 24, foe_b saves (20) half 12."""
caster = make_state(make_combatant("caster", hp=100, spells=["Fireball"]), "players", (7, 0))
caster = make_state(make_combatant(
"caster", hp=100, spells=["Fireball"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (7, 0))
foe_a = make_state(make_combatant("foe_a", hp=100), "monsters", (0, 4))
foe_b = make_state(make_combatant("foe_b", hp=100), "monsters", (0, 5))
eng = _engine([caster, foe_a, foe_b], [2] + [3] * 8 + [20] + [3] * 8)
@@ -233,7 +239,9 @@ def test_point_cast_no_save_area_spell_rolls_damage_dice_only() -> None:
When: a target-less point cast
Then: no saving throw is rolled at all — only the 8 damage dice."""
caster = make_state(
make_combatant("caster", hp=100, spells=["Spark Burst"]), "players", (7, 0)
make_combatant(
"caster", hp=100, spells=["Spark Burst"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (7, 0)
)
foe = make_state(make_combatant("foe", hp=100), "monsters", (0, 4))
eng = _engine([caster, foe], [3] * 8)
@@ -254,7 +262,9 @@ def test_creature_centered_cast_transcript_pinned() -> None:
When: cast_spell with target_id and NO target_pos (the legacy path)
Then: the transcript is the pinned creature-centered line — primary save
rolled once, reused for the target in the area, 8d6 all 3s."""
caster = make_state(make_combatant("caster", hp=100, spells=["Fireball"]), "players", (7, 0))
caster = make_state(make_combatant(
"caster", hp=100, spells=["Fireball"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (7, 0))
foe = make_state(make_combatant("foe", hp=100), "monsters", (0, 4))
eng = _engine([caster, foe], [2] + [3] * 8)
result = eng._execute(
@@ -276,13 +286,17 @@ def test_target_pos_at_target_cell_matches_creature_centered() -> None:
target_pos set to the target's own cell (target_id kept)
When: both casts resolve with identical scripted rolls
Then: transcripts and hp outcomes are byte-identical."""
caster = make_state(make_combatant("caster", hp=100, spells=["Fireball"]), "players", (7, 0))
caster = make_state(make_combatant(
"caster", hp=100, spells=["Fireball"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (7, 0))
foe = make_state(make_combatant("foe", hp=100), "monsters", (0, 4))
plain = _engine([caster, foe], [2] + [3] * 8)
plain._execute(
caster, Action(kind="cast_spell", target_id="foe", spell_name="Fireball")
)
caster2 = make_state(make_combatant("caster", hp=100, spells=["Fireball"]), "players", (7, 0))
caster2 = make_state(make_combatant(
"caster", hp=100, spells=["Fireball"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (7, 0))
foe2 = make_state(make_combatant("foe", hp=100), "monsters", (0, 4))
pointed = _engine([caster2, foe2], [2] + [3] * 8)
pointed._execute(
@@ -304,7 +318,9 @@ def test_non_area_spell_with_target_pos_resolves_on_target() -> None:
Then: target_pos is ignored — the missile hits its creature (1d4+1 = 4)
and the transcript is the ordinary creature-centered line."""
caster = make_state(
make_combatant("caster", hp=100, spells=["Magic Missile"]), "players", (0, 0)
make_combatant(
"caster", hp=100, spells=["Magic Missile"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (0, 0)
)
foe = make_state(make_combatant("foe", hp=100), "monsters", (0, 2))
eng = _engine([caster, foe], [3])
@@ -325,7 +341,9 @@ def test_non_area_point_cast_without_target_is_harmless_no_op() -> None:
When: the action is dispatched with an empty roll queue
Then: nothing crashes, nothing is cast, no die is rolled."""
caster = make_state(
make_combatant("caster", hp=100, spells=["Magic Missile"]), "players", (0, 0)
make_combatant(
"caster", hp=100, spells=["Magic Missile"], spell_slots={1: 9, 2: 9, 3: 9}
), "players", (0, 0)
)
foe = make_state(make_combatant("foe", hp=100), "monsters", (0, 2))
eng = _engine([caster, foe], [])