Files
ctan 1f8f068dd1 feat(rules): spell components + deafened verbal failure and arcane spell failure rolls
- SpellSpec.components (frozenset of V/S/M/F/DF, JSON key accepting the RAW
  string notation "V, S, M/DF" or a token list; slash = alternative; unknown
  token is a data error). Default empty = not encoded yet -> no failure.
- Combatant.armor_spell_failure (0-100 percent, authored on the JSON - the
  model has no equipment) and arcane_caster (divine casters never incur ASF);
  both load directly through load_combatant.
- _cast_spell: after the start-of-casting gates and before delivery, two
  conditional d100 rolls (convention mirrors the concealment roll, <=):
  deafened + V component spoils the spell on <=20 (SRD cast-a-spell.md);
  arcane_caster + S component + armor_spell_failure > 0 fails the spell on
  <= the ASF percent (SRD arcane-spells-and-armor.md). The slot stays spent
  (mirrors the concentration fizzle). No condition -> no roll: the RNG
  stream is bit-identical for every current combat (registry encodes no
  components yet - full encoding is a follow-up task).
- Observation: spell summary gains "components" (sorted tuple), combatant
  snapshot gains "armor_spell_failure"/"arcane_caster"; README documents
  the keys and the two former out-of-scope bullets now describe the modeled
  rule with its data-encoding follow-up.
- Data smoke test: lich arcane_caster true / ASF 0, oni arcane_caster false.
- tests/test_spell_failure.py: 21 tests (parsing, thresholds <=20 / <=ASF,
  divine/no-ASF/no-component never roll, roll order, slot stays spent,
  observation keys). Golden transcripts unchanged.
2026-09-20 14:02:10 +02:00

88 lines
1.8 KiB
JSON

{
"name": "Lich",
"alignment": "NE",
"level": 11,
"size": "Medium",
"creature_type": "undead",
"abilities": {
"str_score": 10,
"dex_score": 14,
"con_score": 10,
"int_score": 22,
"wis_score": 14,
"cha_score": 16
},
"hp_max": 111,
"ac": {
"total": 23,
"touch": 14,
"flat_footed": 21
},
"bab": 5,
"initiative_mod": 2,
"speed_land_ft": 30,
"saves": {
"fort": 6,
"ref": 7,
"will": 11
},
"casting_ability": "int",
"arcane_caster": true,
"armor_spell_failure": 0,
"spell_slots": {
"1": 4,
"2": 4,
"3": 4,
"4": 4,
"5": 4,
"6": 3
},
"attacks": [
{
"id": "lich:touch",
"name": "Paralyzing Touch",
"kind": "touch",
"attack_bonus": 5,
"natural": true,
"damage": [
{
"formula": "1d8+5",
"types": ["negative"]
}
],
"damage_bonus": 0,
"crit_range": 20,
"crit_mult": 2,
"reach_ft": 5,
"count": 1
}
],
"dr": {
"amount": 15,
"bypass": ["bludgeoning", "magic"]
},
"source": "bestiary",
"notes": "Stat block aligned to the KB lich entry (bestiary/lich.md): hp 111 (11d6+55 plus 15 false life), Str 10, Int 22, Init +2, fear aura 60 ft / DC 18. The spell list is the sim's own encounter-authored list (the KB's prepared list includes spells outside the data/spells registry); only the stat block was aligned.",
"features": [
"Fear Aura",
"Paralyzing Touch",
"Undead Traits"
],
"spells": [
"Mage Armor",
"Shield",
"Magic Missile",
"Scorching Ray",
"Fireball",
"Lightning Bolt",
"Hold Person",
"Wall of Fire",
"Cloudkill",
"Disintegrate",
"Horrid Wilting",
"Power Word Kill"
],
"metamagic_feats": [],
"skills": {}
}