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": {
|
"abilities": {
|
||||||
"str_score": 12,
|
"str_score": 12,
|
||||||
"dex_score": 14,
|
"dex_score": 14,
|
||||||
"con_score": 14,
|
"con_score": 10,
|
||||||
"int_score": 20,
|
"int_score": 20,
|
||||||
"wis_score": 14,
|
"wis_score": 14,
|
||||||
"cha_score": 16
|
"cha_score": 16
|
||||||
},
|
},
|
||||||
"hp_max": 100,
|
"hp_max": 100,
|
||||||
"ac": {
|
"ac": {
|
||||||
"total": 25,
|
"total": 23,
|
||||||
"touch": 14,
|
"touch": 14,
|
||||||
"flat_footed": 23
|
"flat_footed": 21
|
||||||
},
|
},
|
||||||
"bab": 8,
|
"bab": 5,
|
||||||
"initiative_mod": 6,
|
"initiative_mod": 6,
|
||||||
"speed_land_ft": 30,
|
"speed_land_ft": 30,
|
||||||
"saves": {
|
"saves": {
|
||||||
"fort": 10,
|
"fort": 6,
|
||||||
"ref": 10,
|
"ref": 7,
|
||||||
"will": 14
|
"will": 11
|
||||||
},
|
},
|
||||||
"casting_ability": "int",
|
"casting_ability": "int",
|
||||||
"spell_slots": {
|
"spell_slots": {
|
||||||
@@ -39,14 +39,15 @@
|
|||||||
"id": "lich:touch",
|
"id": "lich:touch",
|
||||||
"name": "Paralyzing Touch",
|
"name": "Paralyzing Touch",
|
||||||
"kind": "touch",
|
"kind": "touch",
|
||||||
"attack_bonus": 10,
|
"attack_bonus": 5,
|
||||||
|
"natural": true,
|
||||||
"damage": [
|
"damage": [
|
||||||
{
|
{
|
||||||
"formula": "1d8+5",
|
"formula": "1d8+5",
|
||||||
"types": ["negative"]
|
"types": ["negative"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"damage_bonus": 5,
|
"damage_bonus": 0,
|
||||||
"crit_range": 20,
|
"crit_range": 20,
|
||||||
"crit_mult": 2,
|
"crit_mult": 2,
|
||||||
"reach_ft": 5,
|
"reach_ft": 5,
|
||||||
|
|||||||
+34
-6
@@ -36,23 +36,24 @@ def _make_lich() -> Combatant:
|
|||||||
size="Medium",
|
size="Medium",
|
||||||
creature_type="undead",
|
creature_type="undead",
|
||||||
abilities=AbilityScores(
|
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,
|
int_score=20, wis_score=14, cha_score=16,
|
||||||
),
|
),
|
||||||
hp_max=100,
|
hp_max=100,
|
||||||
ac=ACProfile(total=25, touch=14, flat_footed=23),
|
ac=ACProfile(total=23, touch=14, flat_footed=21),
|
||||||
bab=8,
|
bab=5,
|
||||||
initiative_mod=6,
|
initiative_mod=6,
|
||||||
speed_land_ft=30,
|
speed_land_ft=30,
|
||||||
saves=Saves(fort=10, ref=10, will=14),
|
saves=Saves(fort=6, ref=7, will=11),
|
||||||
attacks=[
|
attacks=[
|
||||||
AttackSpec(
|
AttackSpec(
|
||||||
id="lich:touch",
|
id="lich:touch",
|
||||||
name="Paralyzing Touch",
|
name="Paralyzing Touch",
|
||||||
kind="touch",
|
kind="touch",
|
||||||
attack_bonus=10,
|
attack_bonus=5,
|
||||||
|
natural=True,
|
||||||
damage=[DamageComponent(formula=parse_dice("1d8+5"), types=["negative"])],
|
damage=[DamageComponent(formula=parse_dice("1d8+5"), types=["negative"])],
|
||||||
damage_bonus=5,
|
damage_bonus=0,
|
||||||
crit_range=20,
|
crit_range=20,
|
||||||
crit_mult=2,
|
crit_mult=2,
|
||||||
reach_ft=5,
|
reach_ft=5,
|
||||||
@@ -225,3 +226,30 @@ class TestLichJsonIntegrity:
|
|||||||
assert not any("spell_slots_" in k for k in data)
|
assert not any("spell_slots_" in k for k in data)
|
||||||
assert len(data["casting_ability"]) > 0
|
assert len(data["casting_ability"]) > 0
|
||||||
assert data["spell_slots"]["6"] == 3
|
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