From f6292f8bdc8008361584f0858741371adbbf9d34 Mon Sep 17 00:00:00 2001 From: ctan Date: Sun, 20 Sep 2026 14:30:57 +0200 Subject: [PATCH] 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. --- data/monsters/celestial_wolf.json | 2 ++ data/monsters/giant_belostomatid.json | 2 ++ data/monsters/goblin.json | 2 ++ data/monsters/orc.json | 2 ++ data/monsters/thrune_agent.json | 2 ++ data/pcs/esha.json | 2 ++ data/pcs/esha_mount.json | 2 ++ data/pcs/harvie.json | 2 ++ data/pcs/ierlieth.json | 2 ++ data/pcs/jeanne.json | 2 ++ data/pcs/misty.json | 2 ++ data/pcs/nairda.json | 2 ++ data/pcs/tammara.json | 2 ++ tests/test_spell_failure.py | 46 +++++++++++++++++++++------ 14 files changed, 63 insertions(+), 9 deletions(-) diff --git a/data/monsters/celestial_wolf.json b/data/monsters/celestial_wolf.json index 0f7b5c0..6e5f102 100644 --- a/data/monsters/celestial_wolf.json +++ b/data/monsters/celestial_wolf.json @@ -25,6 +25,8 @@ "ref": 5, "will": 1 }, + "arcane_caster": false, + "armor_spell_failure": 0, "attacks": [ { "id": "celestial_wolf:bite", diff --git a/data/monsters/giant_belostomatid.json b/data/monsters/giant_belostomatid.json index 4c57d98..85d2fe2 100644 --- a/data/monsters/giant_belostomatid.json +++ b/data/monsters/giant_belostomatid.json @@ -29,6 +29,8 @@ "ref": 2, "will": 1 }, + "arcane_caster": false, + "armor_spell_failure": 0, "attacks": [ { "name": "bite", diff --git a/data/monsters/goblin.json b/data/monsters/goblin.json index 977ed83..61978d2 100644 --- a/data/monsters/goblin.json +++ b/data/monsters/goblin.json @@ -20,6 +20,8 @@ "initiative_mod": 6, "speed_land_ft": 30, "saves": { "fort": 3, "ref": 2, "will": -1 }, + "arcane_caster": false, + "armor_spell_failure": 0, "attacks": [ { "name": "short sword", diff --git a/data/monsters/orc.json b/data/monsters/orc.json index 9aae290..db962fb 100644 --- a/data/monsters/orc.json +++ b/data/monsters/orc.json @@ -21,6 +21,8 @@ "initiative_mod": 0, "speed_land_ft": 30, "saves": { "fort": 3, "ref": 0, "will": -1 }, + "arcane_caster": false, + "armor_spell_failure": 0, "attacks": [ { "name": "falchion", diff --git a/data/monsters/thrune_agent.json b/data/monsters/thrune_agent.json index b0159bd..ed4e6c2 100644 --- a/data/monsters/thrune_agent.json +++ b/data/monsters/thrune_agent.json @@ -31,6 +31,8 @@ "will": 6 }, "casting_ability": "int", + "arcane_caster": true, + "armor_spell_failure": 0, "spell_slots": { "1": 5, "2": 5, diff --git a/data/pcs/esha.json b/data/pcs/esha.json index c44574e..1abe6b3 100644 --- a/data/pcs/esha.json +++ b/data/pcs/esha.json @@ -26,6 +26,8 @@ "will": 5 }, "casting_ability": "int", + "arcane_caster": false, + "armor_spell_failure": 0, "spell_slots": {}, "resources": {}, "attacks": [ diff --git a/data/pcs/esha_mount.json b/data/pcs/esha_mount.json index 6859e2b..d3f8786 100644 --- a/data/pcs/esha_mount.json +++ b/data/pcs/esha_mount.json @@ -57,6 +57,8 @@ ], "spells": [], "casting_ability": "wis", + "arcane_caster": false, + "armor_spell_failure": 0, "resources": {}, "metamagic_feats": [], "skills": {} diff --git a/data/pcs/harvie.json b/data/pcs/harvie.json index f5ea52f..0158a44 100644 --- a/data/pcs/harvie.json +++ b/data/pcs/harvie.json @@ -78,6 +78,8 @@ ], "spells": [], "casting_ability": "wis", + "arcane_caster": false, + "armor_spell_failure": 0, "spell_slots": {}, "resources": {}, "metamagic_feats": [], diff --git a/data/pcs/ierlieth.json b/data/pcs/ierlieth.json index 431be76..e3cd40f 100644 --- a/data/pcs/ierlieth.json +++ b/data/pcs/ierlieth.json @@ -77,6 +77,8 @@ ], "spells": [], "casting_ability": "wis", + "arcane_caster": false, + "armor_spell_failure": 0, "spell_slots": {}, "resources": {}, "metamagic_feats": [], diff --git a/data/pcs/jeanne.json b/data/pcs/jeanne.json index d29fa7b..7e9be21 100644 --- a/data/pcs/jeanne.json +++ b/data/pcs/jeanne.json @@ -75,6 +75,8 @@ ], "spells": [], "casting_ability": "wis", + "arcane_caster": false, + "armor_spell_failure": 0, "spell_slots": {}, "resources": {}, "metamagic_feats": [], diff --git a/data/pcs/misty.json b/data/pcs/misty.json index 39830a8..983f93d 100644 --- a/data/pcs/misty.json +++ b/data/pcs/misty.json @@ -26,6 +26,8 @@ "will": 3 }, "casting_ability": "cha", + "arcane_caster": true, + "armor_spell_failure": 0, "spell_slots": { "1": 4, "2": 3 diff --git a/data/pcs/nairda.json b/data/pcs/nairda.json index d5543f1..baa23b9 100644 --- a/data/pcs/nairda.json +++ b/data/pcs/nairda.json @@ -26,6 +26,8 @@ "will": 7 }, "casting_ability": "int", + "arcane_caster": true, + "armor_spell_failure": 0, "spell_slots": { "1": 4, "2": 3 diff --git a/data/pcs/tammara.json b/data/pcs/tammara.json index 2272058..7f274b8 100644 --- a/data/pcs/tammara.json +++ b/data/pcs/tammara.json @@ -26,6 +26,8 @@ "will": 8 }, "casting_ability": "wis", + "arcane_caster": false, + "armor_spell_failure": 0, "spell_slots": { "1": 4, "2": 3 diff --git a/tests/test_spell_failure.py b/tests/test_spell_failure.py index ce5a83d..e5b3874 100644 --- a/tests/test_spell_failure.py +++ b/tests/test_spell_failure.py @@ -320,15 +320,43 @@ def test_observation_exposes_components_and_caster_fields() -> None: json.dumps(obs) -def test_lich_is_arcane_oni_is_not() -> None: - """Smoke test on the shipped data: the lich casts arcane spells with - no armor (ASF 0); Oni is a divine caster and never incurs ASF.""" - lich = load_combatant(DATA_DIR / "monsters" / "lich.json") - oni = load_combatant(DATA_DIR / "pcs" / "oni.json") - assert lich.arcane_caster is True - assert lich.armor_spell_failure == 0 - assert oni.arcane_caster is False - assert oni.armor_spell_failure == 0 +def test_combatant_caster_flags_and_asf_match_sheets() -> None: + """Smoke test on the shipped data, one entry per combatant JSON. + + arcane_caster follows the sheet class (fiches_personnages identity.classes + / bestiary KB): magus/bard/witch/sorcerer/wizard true, divine and + non-casters false. armor_spell_failure is the EFFECTIVE percent: + - lich: no armor (KB bestiary/lich.json, unarmored skeletal body). + - misty: mithral shirt (sheet combat.ac note "+6 mithral shirt", + 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: