fix(rules): lich stat block aligned to the KB entry - bestiary/lich.md gives hp 111 (11d6+55 plus 15 false life), Str 10, Int 22, Init +2 and 'Aura fear (60-ft. radius, DC 18)'; the JSON carried scenario-tuned 100 hp / Str 12 / Int 20 / Init +6 and the engine's feature-keyed aura constants carried 30 ft / DC 22 (BAB/touch/AC/saves/CON already conformed); the sim's own spell list is kept (the KB's prepared list includes spells outside data/spells/) and the choice is documented in the JSON notes and the README; golden tammara-vs-lich re-pinned for the two SRD-correct line changes (initiative d20=10+2=12 from the KB +2, Magic Missile DC17 = 10+1+6 from Int 22; damage unchanged - the 75 ft deployment distance stays beyond even the widened 60 ft aura, so no save die is consumed); +2 aura range tests (75 ft outside, 60 ft inside)

This commit is contained in:
2026-09-19 23:05:25 +02:00
parent 5fe2b72339
commit 4f8a131ba0
6 changed files with 79 additions and 19 deletions
+42 -5
View File
@@ -150,8 +150,9 @@ class TestParalyzingTouch:
class TestFearAura:
"""Given: an enemy within 30 ft at the start of the Lich's turn
When: it fails its Will save (DC 22)
"""Given: an enemy within 60 ft at the start of the Lich's turn
When: it fails its Will save (DC 18, KB bestiary/lich.md "Aura fear
(60-ft. radius, DC 18)")
Then: the enemy gains the shaken active condition."""
def test_failed_save_applies_shaken(self) -> None:
@@ -159,18 +160,39 @@ class TestFearAura:
foe = _make_foe(will=0)
lich_state = _make_state(lich, "monsters", (1, 1))
foe_state = _make_state(foe, "players", (1, 2)) # adjacent: 5 ft
engine = _make_engine([2], [lich_state, foe_state]) # d20=2 -> fails Will DC 22
engine = _make_engine([2], [lich_state, foe_state]) # d20=2 -> fails Will DC 18
engine._apply_fear_aura(lich_state)
shaken = [ac for ac in foe_state.active_conditions if ac.condition.name == "shaken"]
assert len(shaken) == 1
assert shaken[0].condition.modifiers
def test_enemy_beyond_60ft_not_tested(self) -> None:
"""KB radius is 60 ft: an enemy at 75 ft (15 cells diagonal) is outside
the aura - no save die is consumed (empty queue would raise if it were)."""
lich = _make_lich()
foe = _make_foe(will=0)
lich_state = _make_state(lich, "monsters", (0, 0))
foe_state = _make_state(foe, "players", (10, 10)) # 75 ft away
engine = _make_engine([], [lich_state, foe_state])
engine._apply_fear_aura(lich_state)
assert all(ac.condition.name != "shaken" for ac in foe_state.active_conditions)
def test_enemy_at_60ft_is_tested(self) -> None:
"""An enemy exactly at 60 ft (12 cells straight) is inside the aura."""
lich = _make_lich()
foe = _make_foe(will=0)
lich_state = _make_state(lich, "monsters", (0, 0))
foe_state = _make_state(foe, "players", (0, 12)) # 60 ft away
engine = _make_engine([2], [lich_state, foe_state]) # fails Will DC 18
engine._apply_fear_aura(lich_state)
assert any(ac.condition.name == "shaken" for ac in foe_state.active_conditions)
def test_successful_save_does_not_apply_shaken(self) -> None:
lich = _make_lich()
foe = _make_foe(will=30)
lich_state = _make_state(lich, "monsters", (1, 1))
foe_state = _make_state(foe, "players", (1, 2))
engine = _make_engine([2], [lich_state, foe_state]) # d20=2 + will 30 >= 22
engine = _make_engine([2], [lich_state, foe_state]) # d20=2 + will 30 >= 18
engine._apply_fear_aura(lich_state)
assert all(ac.condition.name != "shaken" for ac in foe_state.active_conditions)
@@ -232,12 +254,27 @@ class TestLichJsonIntegrity:
When: checked against the KB stat block (Bestiary "Lich":
"Base Atk +5", "Melee touch +5 (1d8+5 plus paralyzing touch)",
"AC 23, touch 14, flat-footed 21", "Fort +6, Ref +7, Will +11",
"hp 111 (11d6+55 plus 15 false life)", "**Init** +2",
"**Aura** fear (60-ft. radius, DC 18)",
"**Str** 10, **Dex** 14, **Con** —, **Int** 22, **Wis** 14, **Cha** 16",
and the template's "Being undead, a lich has no Constitution score")
Then: BAB, the natural touch attack, AC, saves and CON all match."""
Then: BAB, the natural touch attack, AC, saves, CON, hp, Str, Int,
initiative and the fear aura constants all match."""
data = json.loads(_DATA.read_text(encoding="utf-8"))
assert data["bab"] == 5
assert data["ac"] == {"total": 23, "touch": 14, "flat_footed": 21}
assert data["saves"] == {"fort": 6, "ref": 7, "will": 11}
# KB bestiary/lich.md gives hp 111 (11d6+55 plus 15 false life).
assert data["hp_max"] == 111
# KB Statistics: "Str 10, Dex 14, Con —, Int 22, Wis 14, Cha 16"
assert data["abilities"]["str_score"] == 10
assert data["abilities"]["int_score"] == 22
# KB bestiary/lich.md gives a +2 initiative.
assert data["initiative_mod"] == 2
# KB: "**Aura** fear (60-ft. radius, DC 18)" - the engine implements
# the aura as feature-keyed constants (only the lich carries it).
assert CombatEngine._FEAR_AURA_RANGE_FT == 60
assert CombatEngine._FEAR_AURA_DC == 18
touch = data["attacks"][0]
assert touch["attack_bonus"] == 5
# SRD lich template: "A lich has a touch attack that it can use once