134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
"""Batch 2 (V6b): the next 32 PC-sheet spells added to data/spells/.
|
|
|
|
Given: the registry directory with the 11 original spells plus batch 1 (43)
|
|
When: load_spell_registry / load_spell is called
|
|
Then: every batch-2 spell loads with the expected level/school/range/save,
|
|
the registry grew by exactly 32 over batch 1, and every effect validates
|
|
against the schema (parsing raises on any unknown field value or condition).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pf1e_simulator.conditions import CONDITIONS_BY_NAME
|
|
from pf1e_simulator.spells import (
|
|
AreaEffect,
|
|
BuffEffect,
|
|
ConditionEffect,
|
|
DamageEffect,
|
|
HealEffect,
|
|
OneShotEffect,
|
|
SpellEffect,
|
|
SpellSpec,
|
|
SummonEffect,
|
|
load_spell,
|
|
load_spell_registry,
|
|
)
|
|
|
|
_SPELLS_DIR = Path(__file__).resolve().parent.parent / "data" / "spells"
|
|
|
|
_BATCH1_COUNT = 43
|
|
|
|
# name -> (file stem, level, school, range, save)
|
|
_BATCH2: dict[str, tuple[str, int, str, str, str]] = {
|
|
"Enhanced Diplomacy": ("enhanced_diplomacy", 0, "divination", "touch", "will"),
|
|
"Face of the Devourer": ("face_of_the_devourer", 1, "transmutation", "touch", "fort"),
|
|
"Flurry of Snowballs": ("flurry_of_snowballs", 2, "evocation", "close", "ref"),
|
|
"Forbid Action": ("forbid_action", 1, "enchantment", "close", "will"),
|
|
"Grasp": ("grasp", 0, "divination", "personal", "none"),
|
|
"Guidance": ("guidance", 0, "divination", "touch", "will"),
|
|
"Heroism": ("heroism", 2, "enchantment", "touch", "will"),
|
|
"Hex Vulnerability": ("hex_vulnerability", 1, "necromancy", "close", "will"),
|
|
"Hidden Diplomacy": ("hidden_diplomacy", 1, "enchantment", "close", "will"),
|
|
"Hide from Undead": ("hide_from_undead", 1, "abjuration", "touch", "will"),
|
|
"Ice Slick": ("ice_slick", 2, "evocation", "close", "ref"),
|
|
"Ill Omen": ("ill_omen", 1, "enchantment", "close", "none"),
|
|
"Inflict Light Wounds": ("inflict_light_wounds", 1, "necromancy", "touch", "will"),
|
|
"Ironskin": ("ironskin", 2, "transmutation", "personal", "none"),
|
|
"Light": ("light", 0, "evocation", "touch", "none"),
|
|
"Mage Hand": ("mage_hand", 0, "transmutation", "close", "none"),
|
|
"Make Whole": ("make_whole", 2, "transmutation", "close", "will"),
|
|
"Masterwork Transformation": (
|
|
"masterwork_transformation", 2, "transmutation", "touch", "none",
|
|
),
|
|
"Mending": ("mending", 0, "transmutation", "close", "will"),
|
|
"Message": ("message", 0, "transmutation", "medium", "none"),
|
|
"Mirror Image": ("mirror_image", 2, "illusion", "personal", "none"),
|
|
"Oath of Anonymity": ("oath_of_anonymity", 0, "illusion", "personal", "none"),
|
|
"Obscuring Mist": ("obscuring_mist", 1, "conjuration", "close", "none"),
|
|
"Open/Close": ("open_close", 0, "transmutation", "close", "will"),
|
|
"Owl's Wisdom": ("owls_wisdom", 2, "transmutation", "touch", "will"),
|
|
"Prestidigitation": ("prestidigitation", 0, "universal", "close", "none"),
|
|
"Protection from Evil": ("protection_from_evil", 1, "abjuration", "touch", "will"),
|
|
"Protection from Evil, Communal": (
|
|
"protection_from_evil_communal", 2, "abjuration", "touch", "will",
|
|
),
|
|
"Protection from Law": ("protection_from_law", 1, "abjuration", "touch", "will"),
|
|
"Protection from Law, Communal": (
|
|
"protection_from_law_communal", 2, "abjuration", "touch", "will",
|
|
),
|
|
"Purify Food and Drink": ("purify_food_and_drink", 0, "transmutation", "close", "will"),
|
|
"Putrefy Food and Drink": ("putrefy_food_and_drink", 0, "transmutation", "close", "will"),
|
|
}
|
|
|
|
_EFFECT_TYPES = (
|
|
DamageEffect,
|
|
ConditionEffect,
|
|
HealEffect,
|
|
BuffEffect,
|
|
OneShotEffect,
|
|
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 TestBatch2Loading:
|
|
"""Each batch-2 spell loads with the expected metadata."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("name", "stem", "level", "school", "range_", "save"),
|
|
[
|
|
(name, *fields) for name, fields in sorted(_BATCH2.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_grew_by_exactly_32(self) -> None:
|
|
reg = load_spell_registry(_SPELLS_DIR)
|
|
# Batch 3 adds more spells; the exact total is pinned there.
|
|
assert len(reg) >= _BATCH1_COUNT + 32
|
|
assert set(_BATCH2) <= set(reg)
|
|
|
|
def test_every_effect_validates(self) -> None:
|
|
reg = load_spell_registry(_SPELLS_DIR)
|
|
for name in _BATCH2:
|
|
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
|