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

This commit is contained in:
2026-09-10 19:12:57 +02:00
parent 07e0d83300
commit 543da6a931
37 changed files with 547 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/`, 43 sorts), DD et sauvegardes, RD non
(`spells.py` + base `data/spells/`, 75 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,
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Enhanced Diplomacy",
"level": 0,
"school": "divination",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: the subject gains a +2 competence bonus on a single Diplomacy or Intimidate check, declared before the roll (duration 1 minute or until discharged). Will negates (harmless). Social skill effect only: the engine does not model skill-check modifiers, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Face of the Devourer",
"level": 1,
"school": "transmutation",
"range": "touch",
"save": "fort",
"base_dc": 10,
"effects": [],
"description": "The target's face becomes hideous: it gains a +4 circumstance bonus on Intimidate checks and, if it lacks one, a bite attack (1d6 for Medium) for 1 min/level. Fortitude negates (harmless). The skill bonus and the granted natural attack are not modeled by the engine, so the spell has no mechanical effects here."
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Flurry of Snowballs",
"level": 2,
"school": "evocation",
"range": "close",
"save": "ref",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 20,
"save_for_half": true,
"effects": [
{
"type": "damage",
"formula": "4d6",
"types": ["cold"],
"half_on_save": true
}
]
}
],
"description": "A flurry of snowballs hurtles at your foes: any creature in the area takes 4d6 points of cold damage. Reflex half. The RAW 30-ft cone-shaped burst is approximated as a 20-ft burst centered on the target point."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Forbid Action",
"level": 1,
"school": "enchantment",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "dazed",
"duration_rounds": 1,
"save_negates": true
}
],
"description": "You forbid the target a single course of action (attack, cast, communicate, draw, or move), which it avoids to the best of its ability for 1 round. Will negates. Simplification: the chosen-category restriction is approximated as dazed (the target loses its next turn); the specific forbidden action is not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Grasp",
"level": 0,
"school": "divination",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip cast as an immediate action when you fail a Climb check and would fall: you immediately retry the Climb check at a -2 penalty (increasing by 2 per successive use in the same situation); on success you don't fall. Climbing is not modeled by the engine, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Guidance",
"level": 0,
"school": "divination",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: the subject gains a +1 competence bonus on a single attack roll, saving throw, or skill check, declared before the roll (duration 1 minute or until discharged). Will negates (harmless). Single-roll discharge is not modeled by the engine, so the spell has no mechanical effects here."
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Heroism",
"level": 2,
"school": "enchantment",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "attack", "value": 2, "bonus_type": "morale" },
{ "target": "fort", "value": 2, "bonus_type": "morale" },
{ "target": "ref", "value": 2, "bonus_type": "morale" },
{ "target": "will", "value": 2, "bonus_type": "morale" }
],
"duration_rounds": 10
}
],
"description": "The subject gains a +2 morale bonus on attack rolls, saving throws, and skill checks. Duration 10 min/level approximated as 10 rounds; the skill-check bonus has no engine-wide target and is not modeled. Will negates (harmless)."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Hex Vulnerability",
"level": 1,
"school": "necromancy",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "The target becomes susceptible to a repeat use of your harmful hexes for 1 round/level, bypassing the once-per-day targeting limit. Will negates. No combat effect: the hex-reuse permission is outside the engine's model, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Hidden Diplomacy",
"level": 1,
"school": "enchantment",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "As part of casting, you attempt a single Intimidate check to make the target act friendly; its memory of the threats is clouded, so its attitude does not worsen afterward. Will negates. Social effect only: no combat mechanics are modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Hide from Undead",
"level": 1,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Undead cannot perceive warded creatures (nonintelligent undead ignore them automatically; intelligent undead get a single Will save) until the subject attacks, channels energy, or touches undead. Duration 10 min/level. Saving throw entries here cover the harmless-target save; the undead-perception effect is not modeled by the engine."
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "Ice Slick",
"level": 2,
"school": "evocation",
"range": "close",
"save": "ref",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 5,
"save_for_half": true,
"effects": [
{
"type": "damage",
"formula": "1d6",
"types": ["cold"],
"half_on_save": true
},
{
"type": "condition",
"condition_name": "prone",
"duration_rounds": 1,
"save_negates": true
}
]
}
],
"description": "A blast of intense cold coats a 5-ft-radius burst in ice: creatures in the area take 1d6 points of cold damage (Reflex half) and fall prone (no prone on a successful save). The lingering slippery surface (half speed, Acrobatics checks) is not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Ill Omen",
"level": 1,
"school": "enchantment",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "The target is afflicted with bad luck: on its next d20 roll (one per 5 caster levels, up to five) it must roll twice and take the less favorable result; duration 1 round/level. No saving throw; spell resistance applies. The forced worst-of-two reroll mechanic is not modeled by the engine, so the spell has no mechanical effects here."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Inflict Light Wounds",
"level": 1,
"school": "necromancy",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "damage",
"formula": "1d8+1",
"types": ["negative"],
"half_on_save": true
}
],
"description": "Touching a creature channels negative energy, dealing 1d8 points of damage + 1 per caster level (maximum +5); at caster level 1 this is modeled as 1d8+1. Will half. Undead are instead cured of a like amount of damage - the engine only models the damaging side against living targets."
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Ironskin",
"level": 2,
"school": "transmutation",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "ac", "value": 4, "bonus_type": "natural" }
],
"duration_rounds": 60
}
],
"description": "Your skin hardens like iron: you gain a +4 enhancement bonus to your natural armor bonus (rising to +7 at caster level 15; the scaling is not modeled). Duration 1 min/level approximated as 60 rounds. The ability to dismiss the spell to negate one confirmed critical or sneak attack is not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Light",
"level": 0,
"school": "evocation",
"range": "touch",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: a touched object glows like a torch, shedding normal light in a 20-foot radius (plus dim light for 20 more feet) for 10 min/level. No combat effect; illumination is not modeled by the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Mage Hand",
"level": 0,
"school": "transmutation",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: a spectral hand lifts and moves one unattended nonmagical object weighing up to 5 lb. from up to 25 ft. away, as far as 15 feet per move action while you concentrate. No combat effect; object manipulation is not modeled by the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Make Whole",
"level": 2,
"school": "transmutation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Repairs 1d6 points of damage per caster level (maximum 5d6) to one object or construct creature (10-minute casting). Will negates (harmless, object). Object/construct repair is outside the engine's combat model, so the spell has no mechanical effects here."
}
@@ -0,0 +1,10 @@
{
"name": "Masterwork Transformation",
"level": 2,
"school": "transmutation",
"range": "touch",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "After 1 hour of casting (plus masterwork-priced reagents), a non-masterwork weapon, armor, shield, tool, or skill kit becomes its masterwork equivalent. Item crafting is outside the engine's combat model, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Mending",
"level": 0,
"school": "transmutation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: after 10 minutes, the spell repairs 1d4 hit points of damage to one object (up to 1 lb./level), removing the broken condition if restored to at least half HP. Will negates (harmless, object). Object repair is outside the engine's combat model, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Message",
"level": 0,
"school": "transmutation",
"range": "medium",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: you whisper messages and receive replies with targeted creatures within range for 10 min/level; bystanders hear nothing without a DC 25 Perception check. Communication is not modeled by the engine, so the spell has no mechanical effects here."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Mirror Image",
"level": 2,
"school": "illusion",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "blurred",
"duration_rounds": 10,
"save_negates": false
}
],
"description": "Illusory doubles (1d4 + 1 per 3 caster levels, max 8) occupy your space, making attacks risk striking a figment instead. Duration 1 min/level approximated as 10 rounds. Simplification: the image-destruction mechanic is approximated as the blurred condition (20% miss chance); the real spell can deflect attacks entirely until the images are used up."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Oath of Anonymity",
"level": 0,
"school": "illusion",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: you swear an oath of secrecy - your voice, body language and gear are magically distorted, granting a +10 bonus on Disguise checks to conceal your identity (not to impersonate a specific person) for 10 min/level. Social skill effect only: the engine does not model skill-check modifiers, so the spell has no mechanical effects here."
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Obscuring Mist",
"level": 1,
"school": "conjuration",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 20,
"save_for_half": false,
"effects": [
{
"type": "condition",
"condition_name": "blurred",
"duration_rounds": 10,
"save_negates": false
}
]
}
],
"description": "A mist cloud spreads in a 20-ft radius from you for 1 min/level (approximated as 10 rounds): a creature 5 feet away has concealment (20% miss chance), farther away total concealment. Simplification: everyone in the area (allies included) gains the blurred 20% miss chance; the distance-scaled total concealment beyond 5 ft is not modeled."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Open/Close",
"level": 0,
"school": "transmutation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: you open or close one unsecured object or portal weighing up to 30 lb. at up to 25 ft. away (fails if a bar or lock resists). Will negates (object). Object manipulation is not modeled by the engine, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Owl's Wisdom",
"level": 2,
"school": "transmutation",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "The subject gains a +4 enhancement bonus to Wisdom for 1 min/level, adding the usual benefit to Wisdom-based skills, Will saves and spell DCs. Will negates (harmless). Ability-score increases are not modeled by the engine, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Prestidigitation",
"level": 0,
"school": "universal",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip: minor magical tricks for 1 hour - lift 1 lb. of material, color/clean/soil items in a 1-ft cube, chill/warm/flavor 1 lb. of nonliving material, or create crude trinkets. It can deal no damage and duplicates no other spell. No combat effect; the trivial manipulations are not modeled by the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Protection from Evil",
"level": 1,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A barrier wards the subject from evil for 1 min/level: +2 deflection bonus to AC and +2 resistance bonus on saves against attacks and effects created by evil creatures, renewed saves and suppression of mental control, and lawful summoned evil creatures cannot touch it. Will negates (harmless). The engine has no alignment system, so alignment-restricted bonuses cannot be modeled and the spell has no mechanical effects here."
}
@@ -0,0 +1,10 @@
{
"name": "Protection from Evil, Communal",
"level": 2,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Functions like protection from evil (+2 deflection to AC and +2 resistance on saves vs evil creatures, protection from mental control), except the 1 min/level duration is divided in 1-minute intervals among the touched creatures. Will negates (harmless). Alignment-restricted bonuses and duration-sharing are not modeled, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Protection from Law",
"level": 1,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A barrier wards the subject from lawful creatures for 1 min/level: +2 deflection bonus to AC and +2 resistance bonus on saves against attacks and effects created by lawful creatures, plus renewed saves against mental control by lawful creatures. Will negates (harmless). The engine has no alignment system, so alignment-restricted bonuses cannot be modeled and the spell has no mechanical effects here."
}
@@ -0,0 +1,10 @@
{
"name": "Protection from Law, Communal",
"level": 2,
"school": "abjuration",
"range": "touch",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "Functions like protection from law (+2 deflection to AC and +2 resistance on saves vs lawful creatures, protection from mental control), except the 1 min/level duration is divided in 1-minute intervals among the touched creatures. Will negates (harmless). Alignment-restricted bonuses and duration-sharing are not modeled, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Purify Food and Drink",
"level": 0,
"school": "transmutation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: cleanses 1 cu. ft./level of spoiled, rotten, diseased or poisoned food and water, making it pure and drinkable (no effect on creatures or magic potions). Will negates (object). Non-combat utility: the engine does not model objects, so the spell has no mechanical effects here."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Putrefy Food and Drink",
"level": 0,
"school": "transmutation",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [],
"description": "A cantrip: instantly rots 1 cu. ft./level of edible food and makes water and liquids brackish and undrinkable (can also spoil a single potion; no effect on creatures or unholy water). Will negates (object). Non-combat utility: the engine does not model objects, so the spell has no mechanical effects here."
}
+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 = 43
_NUM_SPELLS = 75
def test_runner_loads_realtime_spell_registry() -> None:
"""The runner's registry source holds all 43 real spells from data/spells/."""
"""The runner's registry source holds all 75 real spells from data/spells/."""
registry = load_spell_registry(SPELLS_DIR)
assert len(registry) == _NUM_SPELLS
+2 -1
View File
@@ -109,7 +109,8 @@ class TestBatch1Loading:
def test_registry_grew_by_exactly_32(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
assert len(reg) == _ORIGINAL_COUNT + 32
# exact total is pinned by test_spell_registry_batch2
assert len(reg) >= _ORIGINAL_COUNT + 32
assert set(_BATCH1) <= set(reg)
def test_every_effect_validates(self) -> None:
+130
View File
@@ -0,0 +1,130 @@
"""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,
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,
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)
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
+4 -4
View File
@@ -123,13 +123,13 @@ def _make_engine(
class TestSpellLoading:
"""Given: data/spells/ directory with 43 JSON spell files
"""Given: data/spells/ directory with 75 JSON spell files
When: load_spell_registry is called
Then: all 43 spells are loaded with correct fields."""
Then: all 75 spells are loaded with correct fields."""
def test_loads_all_10_spells(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
assert len(reg) == 43
assert len(reg) == 75
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) == 43
assert len(reg) == 75
def test_fireball_fields(self) -> None:
spell = load_spell(_SPELLS_DIR / "fireball.json")