- arcane_caster true: misty (Magus/Bard sheet class), nairda (Witch/
Sorcerer), thrune_agent (Human magus 8, KB bestiary/thrune-agent.md);
lich already true. False: oni (Cleric), tammara (Gunslinger/
Warpriest - divine), esha (Cavalier/Rogue, spell-like only), the
companions harvie/ierlieth/jeanne/esha_mount and the remaining
monsters.
- armor_spell_failure stays 0 everywhere, as the effective PF1e value:
nairda and the lich wear no armor (sheet note "Pas d'armure", KB lich
block); misty's mithral shirt (RAW ASF 10%, magic-items/mithral-shirt
.md) and the thrune agent's +1 studded leather (15%) are light armor
and every spell they cast is on their own class list - magus and bard
cast their own spells in light armor with no arcane spell failure
(classes/magus.md, classes/bard.md).
- tests/test_spell_failure.py: the lich/oni smoke test becomes a
full-data table pinning all 15 combatants' flags with per-entry
sources. No shipped combatant incurs a non-zero ASF; the failure roll
itself stays covered by the synthetic armored-caster tests.
- Every data/spells/*.json now carries a "components" field with the
V/S/M/F/DF tokens from its KB Components line (material/focus
descriptions dropped; M/DF and F/DF alternatives kept verbatim).
- 107/108 encoded. Create Alcool (data/spells/create_alcool.json) is
left without the field: it is a homebrew cantrip (level 0) with no KB
entry - the closest real spell, Enhance Water (Inner Sea Gods, V, S),
is already a separate registry entry.
- summon_monster_i/ii map to the KB's numeric files (summon-monster-1/2);
hidden_diplomacy maps to its KB name Egorian Diplomacy (same spell,
Spymaster's Handbook rename): V, S, M.
- tests/test_spell_failure.py: the placeholder asserting the registry was
not yet encoded is replaced by an encoded-state smoke test (all spells
but Create Alcool declare components; five spot-checks against the SRD
stat blocks). Golden transcripts unchanged: the component gates still
need deafened/ASF conditions no golden combat has.
- 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.