"""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