feat(rules): encode arcane_caster and armor_spell_failure from the sheets

- 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.
This commit is contained in:
2026-09-20 14:30:57 +02:00
parent 91666d84b9
commit f6292f8bdc
14 changed files with 63 additions and 9 deletions
+2
View File
@@ -25,6 +25,8 @@
"ref": 5, "ref": 5,
"will": 1 "will": 1
}, },
"arcane_caster": false,
"armor_spell_failure": 0,
"attacks": [ "attacks": [
{ {
"id": "celestial_wolf:bite", "id": "celestial_wolf:bite",
+2
View File
@@ -29,6 +29,8 @@
"ref": 2, "ref": 2,
"will": 1 "will": 1
}, },
"arcane_caster": false,
"armor_spell_failure": 0,
"attacks": [ "attacks": [
{ {
"name": "bite", "name": "bite",
+2
View File
@@ -20,6 +20,8 @@
"initiative_mod": 6, "initiative_mod": 6,
"speed_land_ft": 30, "speed_land_ft": 30,
"saves": { "fort": 3, "ref": 2, "will": -1 }, "saves": { "fort": 3, "ref": 2, "will": -1 },
"arcane_caster": false,
"armor_spell_failure": 0,
"attacks": [ "attacks": [
{ {
"name": "short sword", "name": "short sword",
+2
View File
@@ -21,6 +21,8 @@
"initiative_mod": 0, "initiative_mod": 0,
"speed_land_ft": 30, "speed_land_ft": 30,
"saves": { "fort": 3, "ref": 0, "will": -1 }, "saves": { "fort": 3, "ref": 0, "will": -1 },
"arcane_caster": false,
"armor_spell_failure": 0,
"attacks": [ "attacks": [
{ {
"name": "falchion", "name": "falchion",
+2
View File
@@ -31,6 +31,8 @@
"will": 6 "will": 6
}, },
"casting_ability": "int", "casting_ability": "int",
"arcane_caster": true,
"armor_spell_failure": 0,
"spell_slots": { "spell_slots": {
"1": 5, "1": 5,
"2": 5, "2": 5,
+2
View File
@@ -26,6 +26,8 @@
"will": 5 "will": 5
}, },
"casting_ability": "int", "casting_ability": "int",
"arcane_caster": false,
"armor_spell_failure": 0,
"spell_slots": {}, "spell_slots": {},
"resources": {}, "resources": {},
"attacks": [ "attacks": [
+2
View File
@@ -57,6 +57,8 @@
], ],
"spells": [], "spells": [],
"casting_ability": "wis", "casting_ability": "wis",
"arcane_caster": false,
"armor_spell_failure": 0,
"resources": {}, "resources": {},
"metamagic_feats": [], "metamagic_feats": [],
"skills": {} "skills": {}
+2
View File
@@ -78,6 +78,8 @@
], ],
"spells": [], "spells": [],
"casting_ability": "wis", "casting_ability": "wis",
"arcane_caster": false,
"armor_spell_failure": 0,
"spell_slots": {}, "spell_slots": {},
"resources": {}, "resources": {},
"metamagic_feats": [], "metamagic_feats": [],
+2
View File
@@ -77,6 +77,8 @@
], ],
"spells": [], "spells": [],
"casting_ability": "wis", "casting_ability": "wis",
"arcane_caster": false,
"armor_spell_failure": 0,
"spell_slots": {}, "spell_slots": {},
"resources": {}, "resources": {},
"metamagic_feats": [], "metamagic_feats": [],
+2
View File
@@ -75,6 +75,8 @@
], ],
"spells": [], "spells": [],
"casting_ability": "wis", "casting_ability": "wis",
"arcane_caster": false,
"armor_spell_failure": 0,
"spell_slots": {}, "spell_slots": {},
"resources": {}, "resources": {},
"metamagic_feats": [], "metamagic_feats": [],
+2
View File
@@ -26,6 +26,8 @@
"will": 3 "will": 3
}, },
"casting_ability": "cha", "casting_ability": "cha",
"arcane_caster": true,
"armor_spell_failure": 0,
"spell_slots": { "spell_slots": {
"1": 4, "1": 4,
"2": 3 "2": 3
+2
View File
@@ -26,6 +26,8 @@
"will": 7 "will": 7
}, },
"casting_ability": "int", "casting_ability": "int",
"arcane_caster": true,
"armor_spell_failure": 0,
"spell_slots": { "spell_slots": {
"1": 4, "1": 4,
"2": 3 "2": 3
+2
View File
@@ -26,6 +26,8 @@
"will": 8 "will": 8
}, },
"casting_ability": "wis", "casting_ability": "wis",
"arcane_caster": false,
"armor_spell_failure": 0,
"spell_slots": { "spell_slots": {
"1": 4, "1": 4,
"2": 3 "2": 3
+37 -9
View File
@@ -320,15 +320,43 @@ def test_observation_exposes_components_and_caster_fields() -> None:
json.dumps(obs) json.dumps(obs)
def test_lich_is_arcane_oni_is_not() -> None: def test_combatant_caster_flags_and_asf_match_sheets() -> None:
"""Smoke test on the shipped data: the lich casts arcane spells with """Smoke test on the shipped data, one entry per combatant JSON.
no armor (ASF 0); Oni is a divine caster and never incurs ASF."""
lich = load_combatant(DATA_DIR / "monsters" / "lich.json") arcane_caster follows the sheet class (fiches_personnages identity.classes
oni = load_combatant(DATA_DIR / "pcs" / "oni.json") / bestiary KB): magus/bard/witch/sorcerer/wizard true, divine and
assert lich.arcane_caster is True non-casters false. armor_spell_failure is the EFFECTIVE percent:
assert lich.armor_spell_failure == 0 - lich: no armor (KB bestiary/lich.json, unarmored skeletal body).
assert oni.arcane_caster is False - misty: mithral shirt (sheet combat.ac note "+6 mithral shirt",
assert oni.armor_spell_failure == 0 light armor, RAW ASF 10%) but every spell she knows is a bard or
magus spell and both classes cast their own-list spells in light
armor with no ASF (classes/bard.md, classes/magus.md) -> 0.
- nairda: no armor (sheet combat.ac note "Pas d'armure") -> 0.
- thrune_agent: +1 studded leather (KB bestiary/thrune-agent.md gear,
light armor) but all its spells are magus spells and a magus casts
magus spells in light armor with no ASF (classes/magus.md) -> 0.
"""
expectations: dict[str, tuple[bool, int]] = {
"pcs/misty.json": (True, 0),
"pcs/nairda.json": (True, 0),
"pcs/oni.json": (False, 0),
"pcs/tammara.json": (False, 0),
"pcs/esha.json": (False, 0),
"pcs/esha_mount.json": (False, 0),
"pcs/harvie.json": (False, 0),
"pcs/ierlieth.json": (False, 0),
"pcs/jeanne.json": (False, 0),
"monsters/lich.json": (True, 0),
"monsters/thrune_agent.json": (True, 0),
"monsters/goblin.json": (False, 0),
"monsters/orc.json": (False, 0),
"monsters/celestial_wolf.json": (False, 0),
"monsters/giant_belostomatid.json": (False, 0),
}
for rel, (arcane, asf) in expectations.items():
loaded = load_combatant(DATA_DIR / rel)
assert loaded.arcane_caster is arcane, rel
assert loaded.armor_spell_failure == asf, rel
def test_registry_spells_declare_kb_components() -> None: def test_registry_spells_declare_kb_components() -> None: