"""Batch 3 (V6c): the final 32 PC-sheet spells added to data/spells/. Given: the registry directory with the 11 original spells plus batches 1-2 (75) When: load_spell_registry / load_spell is called Then: every batch-3 spell loads with the expected level/school/range/save, the registry reaches exactly 107 (11 + 96), every effect validates against the schema (parsing raises on any unknown field value or condition), and the registry covers EVERY spell name referenced by data/pcs/*.json (no missing spell remains). """ from __future__ import annotations import json from pathlib import Path import pytest from pf1e_simulator.conditions import CONDITIONS_BY_NAME from pf1e_simulator.spells import ( AreaEffect, BuffEffect, ConditionEffect, DamageEffect, HealEffect, SpellEffect, SpellSpec, SummonEffect, load_spell, load_spell_registry, ) _SPELLS_DIR = Path(__file__).resolve().parent.parent / "data" / "spells" _PCS_DIR = Path(__file__).resolve().parent.parent / "data" / "pcs" _BATCH2_COUNT = 75 # name -> (file stem, level, school, range, save) _BATCH3: dict[str, tuple[str, int, str, str, str]] = { "Ray of Enfeeblement": ("ray_of_enfeeblement", 1, "necromancy", "close", "fort"), "Ray of Frost": ("ray_of_frost", 0, "evocation", "close", "none"), "Read Magic": ("read_magic", 0, "divination", "personal", "none"), "Resistance": ("resistance", 0, "abjuration", "touch", "will"), "Restoration, Lesser": ("restoration_lesser", 2, "conjuration", "touch", "will"), "Sanctuary": ("sanctuary", 1, "abjuration", "touch", "will"), "Saving Finale": ("saving_finale", 1, "evocation", "close", "will"), "Scorching Ray": ("scorching_ray", 2, "evocation", "close", "none"), "Shard of Chaos": ("shard_of_chaos", 2, "evocation", "close", "will"), "Shield": ("shield", 1, "abjuration", "personal", "none"), "Shield of Faith": ("shield_of_faith", 1, "abjuration", "touch", "will"), "Shocking Grasp": ("shocking_grasp", 1, "evocation", "touch", "none"), "Silence": ("silence", 2, "illusion", "long", "will"), "Sleep": ("sleep", 1, "enchantment", "medium", "will"), "Sound Burst": ("sound_burst", 2, "evocation", "close", "fort"), "Spark": ("spark", 0, "evocation", "close", "fort"), "Spear of Purity": ("spear_of_purity", 2, "evocation", "close", "will"), "Spiritual Weapon": ("spiritual_weapon", 2, "evocation", "medium", "none"), "Stabilize": ("stabilize", 0, "conjuration", "close", "will"), "Status": ("status", 2, "divination", "touch", "will"), "Stave Off Corruption": ("stave_off_corruption", 2, "abjuration", "touch", "will"), "Summon Instrument": ("summon_instrument", 0, "conjuration", "personal", "none"), "Summon Monster I": ("summon_monster_i", 1, "conjuration", "close", "none"), "Suppress Charms and Compulsions": ( "suppress_charms_and_compulsions", 2, "abjuration", "close", "will", ), "Surmount Affliction": ("surmount_affliction", 2, "abjuration", "personal", "none"), "Tears to Wine": ("tears_to_wine", 2, "transmutation", "close", "will"), "Touch of Fatigue": ("touch_of_fatigue", 0, "necromancy", "touch", "fort"), "Unseen Servant": ("unseen_servant", 1, "conjuration", "close", "none"), "Vanish": ("vanish", 1, "illusion", "touch", "will"), "Virtue": ("virtue", 0, "transmutation", "touch", "none"), "Weapon of Awe": ("weapon_of_awe", 2, "transmutation", "touch", "will"), "Web": ("web", 2, "conjuration", "medium", "ref"), } _EFFECT_TYPES = ( DamageEffect, ConditionEffect, HealEffect, BuffEffect, AreaEffect, SummonEffect, ) def _walk_effects(effects: tuple[SpellEffect, ...]) -> list[SpellEffect]: flat: list[SpellEffect] = [] for effect in effects: flat.append(effect) if isinstance(effect, AreaEffect): flat.extend(_walk_effects(effect.effects)) return flat class TestBatch3Loading: """Each batch-3 spell loads with the expected metadata.""" @pytest.mark.parametrize( ("name", "stem", "level", "school", "range_", "save"), [ (name, *fields) for name, fields in sorted(_BATCH3.items()) ], ) def test_spell_fields( self, name: str, stem: str, level: int, school: str, range_: str, save: str ) -> None: spell = load_spell(_SPELLS_DIR / f"{stem}.json") assert spell.name == name assert spell.level == level assert spell.school == school assert spell.range == range_ assert spell.save == save assert spell.base_dc == 10 assert spell.description def test_registry_reaches_107(self) -> None: reg = load_spell_registry(_SPELLS_DIR) assert len(reg) == _BATCH2_COUNT + 32 == 107 assert set(_BATCH3) <= set(reg) def test_every_effect_validates(self) -> None: reg = load_spell_registry(_SPELLS_DIR) for name in _BATCH3: spell: SpellSpec = reg[name] for effect in _walk_effects(spell.effects): assert isinstance(effect, _EFFECT_TYPES) if isinstance(effect, ConditionEffect): assert effect.condition_name in CONDITIONS_BY_NAME if isinstance(effect, BuffEffect): assert effect.modifiers if isinstance(effect, SummonEffect): assert effect.creature class TestFullPcSpellCoverage: """The registry now covers every spell name referenced by the PC sheets.""" def test_no_pc_spell_is_missing_from_the_registry(self) -> None: reg = load_spell_registry(_SPELLS_DIR) missing: set[tuple[str, str]] = set() for path in sorted(_PCS_DIR.glob("*.json")): data = json.loads(path.read_text()) for spell_name in data.get("spells", []): if spell_name not in reg: missing.add((path.name, spell_name)) assert not missing