fix(rules): correct P1 PF1e conformance bugs (area saves, concentration, prone, slots, helpless, nauseated)
This commit is contained in:
@@ -5,12 +5,15 @@ The concentration DC depends on the casting situation (CRB Table 9-1):
|
||||
- grappled or pinned: DC = 10 + grappler's CMB + spell_level
|
||||
- casting defensively: DC = 15 + 2 * spell_level
|
||||
|
||||
The check roll = d20 + caster_level + casting ability mod (+4 Combat Casting),
|
||||
unchanged from the current logic.
|
||||
The check roll = d20 + caster level + the casting ability modifier used to
|
||||
determine bonus spells (Int/Wis/Cha) — NOT Constitution (PF1e conformance
|
||||
fix B2). Combat Casting's +4 applies only when casting defensively or while
|
||||
grappled — never for damage taken while casting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.abilities import AbilitySpec
|
||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.grid import Grid
|
||||
@@ -32,6 +35,11 @@ def _combatant(
|
||||
level: int = 5,
|
||||
con: int = 12,
|
||||
cmb: int | None = None,
|
||||
casting_ability: str = "wis",
|
||||
int_score: int = 10,
|
||||
wis_score: int = 10,
|
||||
cha_score: int = 10,
|
||||
combat_casting: bool = False,
|
||||
) -> Combatant:
|
||||
attack = AttackSpec(
|
||||
id=f"{cid}-w",
|
||||
@@ -40,6 +48,9 @@ def _combatant(
|
||||
attack_bonus=2,
|
||||
damage=[DamageComponent(formula=parse_dice("1d4"), types=["piercing"])],
|
||||
)
|
||||
features = (
|
||||
[AbilitySpec(name="Combat Casting", category="feat")] if combat_casting else []
|
||||
)
|
||||
return Combatant(
|
||||
id=cid,
|
||||
name=cid,
|
||||
@@ -49,9 +60,9 @@ def _combatant(
|
||||
str_score=10,
|
||||
dex_score=10,
|
||||
con_score=con,
|
||||
int_score=10,
|
||||
wis_score=10,
|
||||
cha_score=10,
|
||||
int_score=int_score,
|
||||
wis_score=wis_score,
|
||||
cha_score=cha_score,
|
||||
),
|
||||
hp_max=30,
|
||||
ac=ACProfile(total=12, touch=12, flat_footed=10),
|
||||
@@ -61,6 +72,8 @@ def _combatant(
|
||||
saves=Saves(fort=0, ref=0, will=0),
|
||||
attacks=[attack],
|
||||
cmb=cmb,
|
||||
casting_ability=casting_ability,
|
||||
features=features,
|
||||
)
|
||||
|
||||
|
||||
@@ -113,3 +126,52 @@ class TestConcentrationDc:
|
||||
assert engine_pass._check_concentration(c, 1, damage_taken=4) is True
|
||||
engine_fail = _make_engine(caster, grappler, [9])[0]
|
||||
assert engine_fail._check_concentration(c, 1, damage_taken=4) is False
|
||||
|
||||
|
||||
class TestConcentrationUsesCastingAbilityNotCon:
|
||||
"""B2 (PF1e conformance): the check is d20 + caster level + the ability
|
||||
modifier used for bonus spells (Int/Wis/Cha) — NOT Constitution."""
|
||||
|
||||
def test_high_con_low_casting_ability_fails_where_con_would_pass(self) -> None:
|
||||
# CON 18 (+4) but casting ability INT 10 (+0): injured DC = 10+4+1 = 15.
|
||||
# Correct roll: d20 6 + level 5 + int 0 = 11 < 15 -> FAIL.
|
||||
# The old CON-based check would have given 6 + 5 + 4 = 15 -> pass.
|
||||
caster = _combatant("caster", level=5, con=18, casting_ability="int")
|
||||
grappler = _combatant("grappler")
|
||||
engine, c, _ = _make_engine(caster, grappler, [6])
|
||||
assert engine._check_concentration(c, 1, damage_taken=4) is False
|
||||
|
||||
def test_charisma_modifier_is_used_for_a_sorcerer(self) -> None:
|
||||
# CHA 18 (+4), CON 6 (-2), casting ability cha: injured DC = 10+4+1 = 15.
|
||||
# d20 6 + level 5 + cha 4 = 15 >= 15 -> PASS (CON would give 13 -> fail).
|
||||
caster = _combatant("caster", level=5, con=6, cha_score=18, casting_ability="cha")
|
||||
grappler = _combatant("grappler")
|
||||
engine, c, _ = _make_engine(caster, grappler, [6])
|
||||
assert engine._check_concentration(c, 1, damage_taken=4) is True
|
||||
|
||||
|
||||
class TestCombatCastingPlacement:
|
||||
"""B2 (PF1e conformance): Combat Casting's +4 applies only when casting
|
||||
defensively or while grappled — never for damage taken while casting."""
|
||||
|
||||
def test_combat_casting_does_not_help_when_injured(self) -> None:
|
||||
# Injured DC = 10+4+1 = 15. d20 6 + level 5 + wis 0 = 11 < 15 -> FAIL.
|
||||
# The old blanket +4 would have made 15 -> pass.
|
||||
caster = _combatant("caster", level=5, con=10, combat_casting=True)
|
||||
grappler = _combatant("grappler")
|
||||
engine, c, _ = _make_engine(caster, grappler, [6])
|
||||
assert engine._check_concentration(c, 1, damage_taken=4) is False
|
||||
|
||||
def test_combat_casting_helps_when_casting_defensively(self) -> None:
|
||||
# Defensive DC = 15 + 2*1 = 17. d20 8 + level 5 + wis 0 + 4 = 17 -> PASS.
|
||||
caster = _combatant("caster", level=5, con=10, combat_casting=True)
|
||||
grappler = _combatant("grappler")
|
||||
engine, c, _ = _make_engine(caster, grappler, [8])
|
||||
assert engine._check_concentration(c, 1, damage_taken=0) is True
|
||||
|
||||
def test_combat_casting_helps_while_grappled(self) -> None:
|
||||
# Grappled DC = 10 + CMB 3 + level 1 = 14. d20 5 + 5 + 0 + 4 = 14 -> PASS.
|
||||
caster = _combatant("caster", level=5, con=10, combat_casting=True)
|
||||
grappler = _combatant("grappler", cmb=3)
|
||||
engine, c, _ = _make_engine(caster, grappler, [5], grappled=True)
|
||||
assert engine._check_concentration(c, 1, damage_taken=0) is True
|
||||
|
||||
Reference in New Issue
Block a user