fix(rules): lich data conforms to the KB stat block
Verified against the KB (bestiary/lich.md, Bestiary Lich CR 12):
- bab 8 -> 5 ('Base Atk +5')
- Paralyzing Touch attack_bonus 10 -> 5 ('Melee touch +5 (1d8+5 plus
paralyzing touch)') and marked natural: true (template: 'A lich has a
touch attack that it can use once per round as a natural weapon [...]
treated as a primary natural weapon' - no BAB iteratives per e5691bd)
- damage_bonus 5 -> 0: the +5 (1 point per 2 HD) is already in the
1d8+5 formula and _resolve_swing adds damage_bonus on top (double
counting: the old data dealt 1d8+10)
- ac 25/14/23 -> 23/14/21 ('AC 23, touch 14, flat-footed 21'; touch was
already correct)
- saves 10/10/14 -> 6/7/11 ('Fort +6, Ref +7, Will +11')
- con_score 14 -> 10: 'Being undead, a lich has no Constitution score'
(Con '—'); the int-only schema encodes 'no score' as the neutral 10
(modifier +0), inert anyway since undead are destroyed at hp <= 0.
Left as intentional engine tuning (residual divergences, documented in
the final report): hp 100 vs 111, Str 12 vs 10, Int 20 vs 22,
initiative +6 vs +2, fear aura 30 ft/DC 22 vs 60 ft/DC 18.
Golden transcript untouched (the lich wins via Magic Missile before any
weapon swing; MM DC depends on Int, unchanged). Pinned a new
test_json_matches_srd_stat_block.
This commit is contained in:
+10
-9
@@ -6,24 +6,24 @@
|
||||
"abilities": {
|
||||
"str_score": 12,
|
||||
"dex_score": 14,
|
||||
"con_score": 14,
|
||||
"con_score": 10,
|
||||
"int_score": 20,
|
||||
"wis_score": 14,
|
||||
"cha_score": 16
|
||||
},
|
||||
"hp_max": 100,
|
||||
"ac": {
|
||||
"total": 25,
|
||||
"total": 23,
|
||||
"touch": 14,
|
||||
"flat_footed": 23
|
||||
"flat_footed": 21
|
||||
},
|
||||
"bab": 8,
|
||||
"bab": 5,
|
||||
"initiative_mod": 6,
|
||||
"speed_land_ft": 30,
|
||||
"saves": {
|
||||
"fort": 10,
|
||||
"ref": 10,
|
||||
"will": 14
|
||||
"fort": 6,
|
||||
"ref": 7,
|
||||
"will": 11
|
||||
},
|
||||
"casting_ability": "int",
|
||||
"spell_slots": {
|
||||
@@ -39,14 +39,15 @@
|
||||
"id": "lich:touch",
|
||||
"name": "Paralyzing Touch",
|
||||
"kind": "touch",
|
||||
"attack_bonus": 10,
|
||||
"attack_bonus": 5,
|
||||
"natural": true,
|
||||
"damage": [
|
||||
{
|
||||
"formula": "1d8+5",
|
||||
"types": ["negative"]
|
||||
}
|
||||
],
|
||||
"damage_bonus": 5,
|
||||
"damage_bonus": 0,
|
||||
"crit_range": 20,
|
||||
"crit_mult": 2,
|
||||
"reach_ft": 5,
|
||||
|
||||
+34
-6
@@ -36,23 +36,24 @@ def _make_lich() -> Combatant:
|
||||
size="Medium",
|
||||
creature_type="undead",
|
||||
abilities=AbilityScores(
|
||||
str_score=12, dex_score=14, con_score=14,
|
||||
str_score=12, dex_score=14, con_score=10,
|
||||
int_score=20, wis_score=14, cha_score=16,
|
||||
),
|
||||
hp_max=100,
|
||||
ac=ACProfile(total=25, touch=14, flat_footed=23),
|
||||
bab=8,
|
||||
ac=ACProfile(total=23, touch=14, flat_footed=21),
|
||||
bab=5,
|
||||
initiative_mod=6,
|
||||
speed_land_ft=30,
|
||||
saves=Saves(fort=10, ref=10, will=14),
|
||||
saves=Saves(fort=6, ref=7, will=11),
|
||||
attacks=[
|
||||
AttackSpec(
|
||||
id="lich:touch",
|
||||
name="Paralyzing Touch",
|
||||
kind="touch",
|
||||
attack_bonus=10,
|
||||
attack_bonus=5,
|
||||
natural=True,
|
||||
damage=[DamageComponent(formula=parse_dice("1d8+5"), types=["negative"])],
|
||||
damage_bonus=5,
|
||||
damage_bonus=0,
|
||||
crit_range=20,
|
||||
crit_mult=2,
|
||||
reach_ft=5,
|
||||
@@ -225,3 +226,30 @@ class TestLichJsonIntegrity:
|
||||
assert not any("spell_slots_" in k for k in data)
|
||||
assert len(data["casting_ability"]) > 0
|
||||
assert data["spell_slots"]["6"] == 3
|
||||
|
||||
def test_json_matches_srd_stat_block(self) -> None:
|
||||
"""Given: the lich bestiary JSON
|
||||
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",
|
||||
and the template's "Being undead, a lich has no Constitution score")
|
||||
Then: BAB, the natural touch attack, AC, saves and CON 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}
|
||||
touch = data["attacks"][0]
|
||||
assert touch["attack_bonus"] == 5
|
||||
# SRD lich template: "A lich has a touch attack that it can use once
|
||||
# per round as a natural weapon [...] treated as a primary natural
|
||||
# weapon" - no BAB iteratives (CRB "Natural Attacks").
|
||||
assert touch["natural"] is True
|
||||
assert touch["damage"][0]["formula"] == "1d8+5"
|
||||
# The +5 (1 point per 2 HD, SRD "Damage") is already in the formula;
|
||||
# damage_bonus would be added on top (double counting).
|
||||
assert touch["damage_bonus"] == 0
|
||||
# Undead have no CON score: encoded as the neutral 10 (modifier +0);
|
||||
# the engine destroys undead at hp <= 0 (SRD Undead traits), so the
|
||||
# score is never used for the dying band.
|
||||
assert data["creature_type"] == "undead"
|
||||
assert data["abilities"]["con_score"] == 10
|
||||
|
||||
Reference in New Issue
Block a user