fix(combat): correct concentration DCs by situation
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""Tests for situation-dependent concentration check DCs (Todo 4).
|
||||
|
||||
The concentration DC depends on the casting situation (CRB Table 9-1):
|
||||
- injured while casting: DC = 10 + damage_taken + spell_level
|
||||
- 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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.grid import Grid
|
||||
from pf1e_simulator.map import MapSpec, TerrainType
|
||||
from pf1e_simulator.models import (
|
||||
AbilityScores,
|
||||
ACProfile,
|
||||
AttackSpec,
|
||||
Combatant,
|
||||
DamageComponent,
|
||||
Saves,
|
||||
)
|
||||
from pf1e_simulator.rng import ScriptedRng
|
||||
|
||||
|
||||
def _combatant(
|
||||
cid: str,
|
||||
*,
|
||||
level: int = 5,
|
||||
con: int = 12,
|
||||
cmb: int | None = None,
|
||||
) -> Combatant:
|
||||
attack = AttackSpec(
|
||||
id=f"{cid}-w",
|
||||
name="dagger",
|
||||
kind="melee",
|
||||
attack_bonus=2,
|
||||
damage=[DamageComponent(formula=parse_dice("1d4"), types=["piercing"])],
|
||||
)
|
||||
return Combatant(
|
||||
id=cid,
|
||||
name=cid,
|
||||
level=level,
|
||||
size="Medium",
|
||||
abilities=AbilityScores(
|
||||
str_score=10,
|
||||
dex_score=10,
|
||||
con_score=con,
|
||||
int_score=10,
|
||||
wis_score=10,
|
||||
cha_score=10,
|
||||
),
|
||||
hp_max=30,
|
||||
ac=ACProfile(total=12, touch=12, flat_footed=10),
|
||||
bab=1,
|
||||
initiative_mod=0,
|
||||
speed_land_ft=30,
|
||||
saves=Saves(fort=0, ref=0, will=0),
|
||||
attacks=[attack],
|
||||
cmb=cmb,
|
||||
)
|
||||
|
||||
|
||||
def _make_engine(
|
||||
caster: Combatant,
|
||||
grappler: Combatant,
|
||||
queue: list[int],
|
||||
*,
|
||||
grappled: bool = False,
|
||||
) -> tuple[CombatEngine, CombatantState, CombatantState]:
|
||||
c = CombatantState(combatant=caster, side="players", pos=(0, 0), hp=caster.hp_max)
|
||||
g = CombatantState(combatant=grappler, side="monsters", pos=(1, 0), hp=grappler.hp_max)
|
||||
if grappled:
|
||||
c.grappled_with = grappler.id
|
||||
g.grappled_with = caster.id
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
spec = MapSpec(name="test", terrain=tuple(["." * 4] * 4), legend=legend)
|
||||
grid = Grid.from_spec(spec)
|
||||
engine = CombatEngine(ScriptedRng(queue), grid, [c, g])
|
||||
return engine, c, g
|
||||
|
||||
|
||||
class TestConcentrationDc:
|
||||
"""Given: a caster making a concentration check in a given situation
|
||||
When: the situation DC is computed
|
||||
Then: it matches the CRB rule for that situation."""
|
||||
|
||||
def test_dc_when_injured_is_ten_plus_damage_plus_level(self) -> None:
|
||||
caster = _combatant("caster", level=5)
|
||||
grappler = _combatant("grappler")
|
||||
engine, c, g = _make_engine(caster, grappler, [])
|
||||
assert engine._concentration_dc(c, 1, damage_taken=4) == 10 + 4 + 1
|
||||
|
||||
def test_dc_when_grappled_uses_grappler_cmb_plus_level(self) -> None:
|
||||
caster = _combatant("caster", level=5)
|
||||
grappler = _combatant("grappler", cmb=7)
|
||||
engine, c, g = _make_engine(caster, grappler, [], grappled=True)
|
||||
assert engine._concentration_dc(c, 1, damage_taken=0) == 10 + 7 + 1
|
||||
|
||||
def test_dc_when_casting_defensively_is_15_plus_twice_level(self) -> None:
|
||||
caster = _combatant("caster", level=5)
|
||||
grappler = _combatant("grappler")
|
||||
engine, c, g = _make_engine(caster, grappler, [])
|
||||
assert engine._concentration_dc(c, 2, damage_taken=0) == 15 + 2 * 2
|
||||
|
||||
def test_check_passes_at_threshold_and_fails_one_below(self) -> None:
|
||||
caster = _combatant("caster", level=5, con=10)
|
||||
grappler = _combatant("grappler")
|
||||
engine_pass, c, _ = _make_engine(caster, grappler, [10])
|
||||
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
|
||||
Reference in New Issue
Block a user