data(spells): add batch 3 of PC spell list (32 spells)

This commit is contained in:
2026-09-10 19:43:07 +02:00
parent 543da6a931
commit 1ca5aed758
37 changed files with 579 additions and 8 deletions
+1 -1
View File
@@ -922,7 +922,7 @@ uv run basedpyright src # mode strict
## Feuille de route
- **Phase 1** — magie et états : largement réalisée. Sorts par emplacements
(`spells.py` + base `data/spells/`, 75 sorts), DD et sauvegardes, RD non
(`spells.py` + base `data/spells/`, 107 sorts), DD et sauvegardes, RD non
appliquée aux dégâts de sorts, concentration par situation, invocation,
conditions effectives (`conditions.py`), manœuvres de combat, dons (55 noms
résolus, `abilities.py` + chargeur) et capacités de classe (channel energy,
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Ray of Enfeeblement",
"level": 1,
"school": "necromancy",
"range": "close",
"save": "fort",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "fatigued",
"duration_rounds": 4,
"save_negates": true
}
],
"description": "A coruscating ray strikes the target (ranged touch attack), dealing 1d6 points of Strength damage +1 per two caster levels (maximum 1d6+5); a successful Fortitude save halves the damage. Simplification: the engine has no ability damage, so the Strength penalty is modeled as the fatigued condition (attack/damage penalties, no charge) for 4 rounds (1 round/level at caster level 4); Fortitude halves is approximated as Fortitude negates."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Ray of Frost",
"level": 0,
"school": "evocation",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "damage",
"formula": "1d3",
"types": ["cold"],
"half_on_save": false
}
],
"description": "A cantrip: a ray of freezing air and ice projects from your pointing finger. You must succeed on a ranged touch attack to deal 1d3 points of cold damage to the target. No save."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Read Magic",
"level": 0,
"school": "divination",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: you can decipher magical inscriptions on objects that would otherwise be unintelligible, at one page per minute, and you can thereafter read that particular writing without the spell. Duration 10 min./level. No combat effect; reading magical writing is not modeled by the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Resistance",
"level": 0,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: the subject gains a +1 resistance bonus on saving throws for 1 minute. Will negates (harmless). Simplified: the engine has no resistance bonus type and no self-buff encoding that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Restoration, Lesser",
"level": 2,
"school": "conjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Casting time 3 rounds. Dispels any magical effects reducing one of the subject's ability scores or cures 1d4 points of temporary ability damage to one ability score, and eliminates any fatigue (improving exhausted to fatigued). Will negates (harmless). No combat effect encoded: the engine has no ability damage, fatigue-from-spell, or magical-effect-reduction state to restore."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Sanctuary",
"level": 1,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "You ward a creature against attacks for 1 round/level: any opponent attempting to directly attack the warded creature must succeed on a Will save or be unable to follow through with that attack and cannot directly attack the subject for the duration. The subject cannot attack without ending the spell. Will negates. No combat effect encoded: the engine has no attack-refusal-by-save mechanic, so the protection cannot be represented."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Saving Finale",
"level": 1,
"school": "evocation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Immediate action, requires an active bardic performance: when a creature affected by your performance fails a saving throw, end the performance to let it immediately reroll the failed save and take the new result. Will negates (harmless). No combat effect encoded: conditional save rerolls tied to bardic performance are not modeled by the engine."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Scorching Ray",
"level": 2,
"school": "evocation",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "damage",
"formula": "4d6",
"types": ["fire"],
"half_on_save": false
}
],
"description": "You blast your enemy with a searing beam of fire: each ray requires a ranged touch attack and deals 4d6 points of fire damage. You fire one ray, plus one additional ray per four levels beyond 3rd (max three at 11th) — the engine models a single ray. No save."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Shard of Chaos",
"level": 2,
"school": "evocation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A dart-shaped projectile of chaotic energy (ranged touch attack): lawful creatures take 1d8 per two caster levels (max 5d8); lawful outsiders take 1d6 per caster level (max 10d6) and are slowed 1 round. A successful Will save halves the damage and negates the slow. Half damage to neutral creatures, no effect on chaotic ones. No combat effect encoded: the engine has no alignment, so the alignment-gated damage cannot be represented."
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Shield",
"level": 1,
"school": "abjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "ac", "value": 4, "bonus_type": "deflection" }
],
"duration_rounds": 10
}
],
"description": "An invisible shield of force hovers in front of you, granting a +4 shield bonus to AC for 1 min./level (modeled as 10 rounds; the shield bonus type is approximated as deflection since the engine has no shield bonus type). Negating magic missile and the ranged-attack deflection are not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Shield of Faith",
"level": 1,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A shimmering magical field grants the target a +2 deflection bonus to AC (+1 per six caster levels, max +5) for 1 min./level. Will negates (harmless). Simplified: the engine has no self-buff encoding that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Shocking Grasp",
"level": 1,
"school": "evocation",
"range": "touch",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "damage",
"formula": "1d6",
"types": ["electricity"],
"half_on_save": false
}
],
"description": "Your successful melee touch attack deals 1d6 points of electricity damage per caster level (maximum 5d6) — modeled at caster level 1 (1d6). No save; the +3 attack bonus against metal-armored foes is not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Silence",
"level": 2,
"school": "illusion",
"range": "long",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Casting time 1 round. A 20-ft.-radius emanation centered on a creature, object, or point in space: all sound is stopped in the area, spells with verbal components cannot be cast, and creatures inside are immune to sonic and language-dependent effects. Duration 1 round/level. Will negates (object). No combat effect encoded: the engine has no verbal component or sound propagation model."
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Sleep",
"level": 1,
"school": "enchantment",
"range": "medium",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 10,
"save_for_half": false,
"effects": [
{
"type": "condition",
"condition_name": "asleep",
"duration_rounds": 10,
"save_negates": true
}
]
}
],
"description": "A magical slumber comes upon 4 HD of living creatures in a 10-ft.-radius burst (fewest HD first; the HD budget is not modeled). Sleeping creatures are helpless (modeled as the asleep condition, 1 min./level approximated as 10 rounds). Will negates. Constructs and undead are immune (not modeled)."
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "Sound Burst",
"level": 2,
"school": "evocation",
"range": "close",
"save": "fort",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 10,
"save_for_half": true,
"effects": [
{
"type": "damage",
"formula": "1d8",
"types": ["sonic"],
"half_on_save": true
},
{
"type": "condition",
"condition_name": "stunned",
"duration_rounds": 1,
"save_negates": true
}
]
}
],
"description": "A tremendous cacophony blasts a 10-ft.-radius spread: every creature in the area takes 1d8 points of sonic damage and must succeed on a Fortitude save or be stunned for 1 round (Fortitude partial — modeled as save for half damage and negates the stun). Creatures that cannot hear take the damage but are not stunned (not modeled)."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Spark",
"level": 0,
"school": "evocation",
"range": "close",
"save": "fort",
"base_dc": 10,
"effects": [],
"description": "A cantrip: you create a tiny spark that ignites one unattended Fine flammable object within close range, in any weather (works as flint and steel but much faster). Fortitude negates (object). No combat effect: the spell targets objects, not creatures."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Spear of Purity",
"level": 2,
"school": "evocation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A spear of good energy (ranged touch attack): evil creatures take 1d8 per two caster levels (max 5d8); evil outsiders take 1d6 per caster level (max 10d6) and are blinded 1 round. A successful Will save halves the damage and negates the blind. Half damage to neutral creatures, no effect on good ones. No combat effect encoded: the engine has no alignment, so the alignment-gated damage cannot be represented."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Spiritual Weapon",
"level": 2,
"school": "evocation",
"range": "medium",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A force weapon appears and attacks foes at a distance as you direct it (1d8 force damage +1 per three caster levels), using your base attack bonus plus Wisdom modifier, once per round on your turn for 1 round/level. No combat effect encoded: the engine has no autonomous floating-attack creature to summon, and the weapon's per-round attacks are not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Stabilize",
"level": 0,
"school": "conjuration",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: you touch a living creature with -1 or fewer hit points (up to close range) and stabilize it automatically, so it stops losing hit points until it takes more damage. Will negates (harmless). No combat effect encoded: the engine's dying/stabilization track is not exposed to the spell-effect system."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Status",
"level": 2,
"school": "divination",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "You mentally monitor the relative position and general condition (unharmed, wounded, staggered, unconscious, dying, poisoned, and so on) of up to one creature per three caster levels for 1 hour/level. Will negates (harmless). No combat effect: out-of-combat scrying is not modeled by the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Stave Off Corruption",
"level": 2,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "You ward the target to slow the advancement of its corruption, granting a +2 circumstance bonus on saving throws against the advancement of that corruption for 1 day per 2 caster levels. Will negates (harmless). Simplified: the engine has no corruption track and no self-buff encoding that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Summon Instrument",
"level": 0,
"school": "conjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip (casting time 1 round): you summon one handheld musical instrument of your choice into your hands or at your feet; it plays only for you and disappears when the spell ends (1 min./level). No combat effect: summoned objects are not modeled by the engine."
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "Summon Monster I",
"level": 1,
"school": "conjuration",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "summon",
"creature": "data/monsters/celestial_wolf.json",
"duration_rounds": 4
}
],
"description": "Casting time 1 round: summons one extraplanar creature from the 1st-level list (e.g. dire rat, dog, eagle) to fight for you for 1 round/level. Simplified: the engine bestiary holds no 1st-level summon, so the celestial wolf model is reused and the duration is set to 4 rounds (1 round/level at caster level 4)."
}
@@ -0,0 +1,10 @@
{
"name": "Suppress Charms and Compulsions",
"level": 2,
"school": "abjuration",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "You bolster the target's willpower: either a +4 morale bonus on saving throws against charm and compulsion effects for 10 minutes, or (as concentration, max 1 round/level) suppression of all charm and compulsion effects on the targets. Will negates (harmless). Simplified: the engine has no conditional save bonus and no self-buff encoding that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Surmount Affliction",
"level": 2,
"school": "abjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "You temporarily overcome one harmful condition on yourself (blinded, confused, dazed, dazzled, deafened, fatigued, frightened, paralyzed, shaken, or sickened) for 1 round/level — the condition's effect is suspended, not ended. No combat effect encoded: the engine has no condition-suspension mechanic."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Tears to Wine",
"level": 2,
"school": "transmutation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "You transmute up to 1 cubic foot of nonmagical liquid per two caster levels (including spoiled, poisoned, or contaminated drink, tears, seawater) into mead or wine of average quality for 10 min./level (RAW range 10 ft., approximated as close). Will negates (object). No combat effect: liquid transmutation is not modeled by the engine."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Touch of Fatigue",
"level": 0,
"school": "necromancy",
"range": "touch",
"save": "fort",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "fatigued",
"duration_rounds": 4,
"save_negates": true
}
],
"description": "A cantrip: a melee touch attack channels negative energy, making the target fatigued for 1 round/level (modeled as 4 rounds at caster level 4; the effect ends with the duration rather than by long rest). No effect on an already fatigued creature or on creatures immune to fatigue. Fortitude negates."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Unseen Servant",
"level": 1,
"school": "conjuration",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "An invisible, mindless, shapeless force performs simple tasks at your command for 1 hour/level: fetching, opening unstuck doors, holding chairs, cleaning or mending (effective Strength 2, speed 15 ft., no attacks, DC 10 skill limit). No combat effect: object manipulation by invisible servants is not modeled by the engine."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Vanish",
"level": 1,
"school": "illusion",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "invisible",
"duration_rounds": 5,
"save_negates": false
}
],
"description": "As invisibility, but only for 1 round/level (maximum 5 rounds) — the spell ends if the subject attacks. Will negates (harmless) — modeled as always accepted by the target. Modeled as the invisible condition (total concealment for the target; the attack-ends-spell rule is not modeled)."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Virtue",
"level": 0,
"school": "transmutation",
"range": "touch",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: with a touch you infuse a creature with a tiny surge of life, granting it 1 temporary hit point for 1 minute. Spell resistance yes (harmless). Simplified: the engine has no temporary hit points and no self-buff encoding that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Weapon of Awe",
"level": 2,
"school": "transmutation",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "The touched weapon becomes an awe-inspiring instrument for 1 min./level: it gains a +2 sacred bonus on damage rolls, and a creature struck by a critical hit from it becomes shaken for 1 round (a mind-affecting fear effect). Will negates (harmless, object). Simplified: the engine has no weapon-targeted buff track and no self-buff encoding that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Web",
"level": 2,
"school": "conjuration",
"range": "medium",
"save": "ref",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 20,
"save_for_half": false,
"effects": [
{
"type": "condition",
"condition_name": "grappled",
"duration_rounds": 2,
"save_negates": true
}
]
}
],
"description": "You conjure a many-layered mass of sticky strands filling a 20-ft.-radius spread (modeled as a 20-ft. burst). Creatures caught in the area are grappled by the fibers (Reflex negates) until they break free; the web also makes the area difficult terrain and provides cover (not modeled). Duration 10 min./level approximated as 2 rounds of the grappled condition."
}
+2 -2
View File
@@ -24,11 +24,11 @@ MAP = ROOT / "data" / "maps" / "sample_arena.yaml"
ONI = ROOT / "data" / "pcs" / "oni.json"
GOBLIN = ROOT / "data" / "monsters" / "goblin.json"
_NUM_SPELLS = 75
_NUM_SPELLS = 107
def test_runner_loads_realtime_spell_registry() -> None:
"""The runner's registry source holds all 75 real spells from data/spells/."""
"""The runner's registry source holds all 107 real spells from data/spells/."""
registry = load_spell_registry(SPELLS_DIR)
assert len(registry) == _NUM_SPELLS
+2 -1
View File
@@ -115,7 +115,8 @@ class TestBatch2Loading:
def test_registry_grew_by_exactly_32(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
assert len(reg) == _BATCH1_COUNT + 32
# 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:
+146
View File
@@ -0,0 +1,146 @@
"""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
+4 -4
View File
@@ -123,13 +123,13 @@ def _make_engine(
class TestSpellLoading:
"""Given: data/spells/ directory with 75 JSON spell files
"""Given: data/spells/ directory with 107 JSON spell files
When: load_spell_registry is called
Then: all 75 spells are loaded with correct fields."""
Then: all 107 spells are loaded with correct fields."""
def test_loads_all_10_spells(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
assert len(reg) == 75
assert len(reg) == 107
def test_spell_names_present(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
@@ -140,7 +140,7 @@ class TestSpellLoading:
"Summon Monster II",
}
assert expected <= set(reg)
assert len(reg) == 75
assert len(reg) == 107
def test_fireball_fields(self) -> None:
spell = load_spell(_SPELLS_DIR / "fireball.json")