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

This commit is contained in:
2026-09-10 18:49:54 +02:00
parent 91f70f19ca
commit 07e0d83300
36 changed files with 571 additions and 7 deletions
+1 -1
View File
@@ -922,7 +922,7 @@ uv run basedpyright src # mode strict
## Feuille de route ## Feuille de route
- **Phase 1** — magie et états : largement réalisée. Sorts par emplacements - **Phase 1** — magie et états : largement réalisée. Sorts par emplacements
(`spells.py` + base `data/spells/`, 11 sorts), DD et sauvegardes, RD non (`spells.py` + base `data/spells/`, 43 sorts), DD et sauvegardes, RD non
appliquée aux dégâts de sorts, concentration par situation, invocation, appliquée aux dégâts de sorts, concentration par situation, invocation,
conditions effectives (`conditions.py`), manœuvres de combat, dons (55 noms conditions effectives (`conditions.py`), manœuvres de combat, dons (55 noms
résolus, `abilities.py` + chargeur) et capacités de classe (channel energy, résolus, `abilities.py` + chargeur) et capacités de classe (channel energy,
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Abundant Ammunition",
"level": 0,
"school": "transmutation",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that conjures duplicate ammunition: attacks with the affected ammunition do not expend it. No combat-mechanical effect in the engine (ammunition is not tracked)."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Acid Splash",
"level": 0,
"school": "conjuration",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "damage",
"formula": "1d3",
"types": ["acid"],
"half_on_save": false
}
],
"description": "You fire a small orb of acid at the target, dealing 1d3 points of acid damage (ranged touch attack, no save)."
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "Aid",
"level": 1,
"school": "enchantment",
"range": "long",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "attack", "value": 1, "bonus_type": "morale" },
{ "target": "will", "value": 1, "bonus_type": "morale" }
],
"duration_rounds": 10
}
],
"description": "The subject gains a +1 morale bonus on attack rolls and on saving throws against fear effects (modeled as Will). Duration 10 min/level approximated as 10 rounds."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Air Bubble",
"level": 1,
"school": "conjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "You create a sphere of air around your head (or a touched creature's) that lasts 10 min/level, allowing breathing underwater. No combat-mechanical effect in the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Alarm",
"level": 1,
"school": "abjuration",
"range": "medium",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "Wards a area (20-ft radius, 20 ft high) against intrusion for 2 hours/level, alerting you mentally. No combat-mechanical effect in the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Arcane Mark",
"level": 0,
"school": "divination",
"range": "touch",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "Creates a permanent visible inscribed mark of your choosing on an object or surface, optionally invisible and keyed to you. Utility cantrip, no combat effect."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Bane",
"level": 1,
"school": "enchantment",
"range": "medium",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "shaken",
"duration_rounds": 10,
"save_negates": true
}
],
"description": "Enemies within range take a -1 penalty on attack rolls and saving throws (approximated with the shaken condition, -2). Will negates. Concentration 1 min/level approximated as 10 rounds."
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "Bear's Endurance",
"level": 2,
"school": "transmutation",
"range": "touch",
"save": "fort",
"base_dc": 10,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "fort", "value": 2, "bonus_type": "enhancement" },
{ "target": "hp", "value": 2, "bonus_type": "enhancement" }
],
"duration_rounds": 60
}
],
"description": "The subject gains a +4 enhancement bonus to Constitution, granting +2 Fortitude and +2 hit points (approximated as +2/+2). Fortitude negates (harmless). Duration 10 min/level approximated as 60 rounds."
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Bladed Dash",
"level": 1,
"school": "transmutation",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "speed_ft", "value": 30, "bonus_type": "enhancement" }
],
"duration_rounds": 1
}
],
"description": "You move up your speed as a move action and make one melee attack at the end. Simplified as a +30 ft speed buff for 1 round; the extra attack is not modeled."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Bleed",
"level": 0,
"school": "necromancy",
"range": "close",
"save": "fort",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "bleeding",
"duration_rounds": 1,
"save_negates": true
}
],
"description": "A cantrip that causes a wound to bleed: the target takes 1 point of damage per round until healed. Fortitude negates. Modeled as the bleeding condition for 1 round."
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "Bless",
"level": 1,
"school": "enchantment",
"range": "medium",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 20,
"save_for_half": false,
"effects": [
{
"type": "buff",
"modifiers": [
{ "target": "attack", "value": 1, "bonus_type": "morale" },
{ "target": "will", "value": 1, "bonus_type": "morale" }
],
"duration_rounds": 10
}
]
}
],
"description": "All allies within 20 ft/level (modeled as a 20-ft burst applied to all creatures in area) gain a +1 morale bonus on attack rolls and on saving throws against fear effects (modeled as Will). Duration 1 min/level approximated as 10 rounds."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Blindness/Deafness",
"level": 2,
"school": "necromancy",
"range": "medium",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "blinded",
"duration_rounds": 100,
"save_negates": true
}
],
"description": "The target becomes blind or deaf (modeled as blinded) until removed by a caster of higher level. Permanent duration approximated as 100 rounds. Will negates."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Charm Person",
"level": 1,
"school": "enchantment",
"range": "medium",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "fascinated",
"duration_rounds": 100,
"save_negates": true
}
],
"description": "The charmed person regards the caster as a trusted friend (modeled as the fascinated condition; the charmed condition is not in the engine). Duration 1 hour/level approximated as 100 rounds. Will negates."
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Color Spray",
"level": 1,
"school": "enchantment",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 15,
"save_for_half": false,
"effects": [
{
"type": "condition",
"condition_name": "stunned",
"duration_rounds": 1,
"save_negates": true
}
]
}
],
"description": "A cone of chaotic colors (modeled as a 15-ft burst) blinds and stuns creatures with fewer HD than your level for 1d4 rounds (approximated as 1 round of stunned). Will negates."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Comprehend Languages",
"level": 0,
"school": "divination",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that lets you understand the meaning of any spoken or written language you encounter for 1 min/level. Utility spell, no combat effect."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Create Alcool",
"level": 0,
"school": "conjuration",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip (Create Alcohol, spelled as on the PC sheet) that transforms one liquid per level into an alcoholic drink of equal volume. Utility spell, no combat effect."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Create Water",
"level": 1,
"school": "conjuration",
"range": "medium",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "Conjures 4 gallons of clean water per level in an open space or container. Utility spell, no combat effect."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Dancing Lights",
"level": 0,
"school": "conjuration",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that creates up to four flickering torch-like lights you move as desired (concentration, 1 min/level). Utility spell, no combat effect."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Darkness",
"level": 2,
"school": "evocation",
"range": "touch",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "area",
"radius_ft": 20,
"save_for_half": false,
"effects": []
}
],
"description": "All light in a 20-ft radius (spread from the touched point) becomes supernatural darkness; creatures inside lose visual sight-based advantages for 10 min/level. The engine does not model area concealment, so the burst carries no mechanical effect."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Daze",
"level": 0,
"school": "enchantment",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "dazed",
"duration_rounds": 1,
"save_negates": true
}
],
"description": "A cantrip that clouds the mind of a humanoid creature with 5 or fewer HD, making it unable to act for 1 round. Will negates."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Deja Vu",
"level": 0,
"school": "divination",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that lets you reroll one attack roll, saving throw, or skill check made in the past round (once per casting). Not modeled by the engine's action flow, so it carries no mechanical effect."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Delay Disease",
"level": 3,
"school": "abjuration",
"range": "touch",
"save": "fort",
"base_dc": 10,
"effects": [],
"description": "You suppress the progression of one disease the subject carries for 1 hour/level, keeping it dormant. Utility spell, no combat-mechanical effect in the engine. Fortitude negates (harmless)."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Delay Poison",
"level": 2,
"school": "abjuration",
"range": "touch",
"save": "fort",
"base_dc": 10,
"effects": [],
"description": "You temporarily stave off incoming poison or eliminate one already in the subject's system for 1 hour/level. Utility spell, no combat-mechanical effect in the engine. Fortitude negates (harmless)."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Detect Fiendish Presence",
"level": 0,
"school": "divination",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that reveals whether a creature of the outsider (evil) subtype has been in the area within 10 minutes/HD, and hints at its nature. Utility spell, no combat effect."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Detect Magic",
"level": 0,
"school": "divination",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that detects magical auras in a 60-ft cone (2 rounds to identify strength and school). Utility spell, no combat effect."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Detect Poison",
"level": 0,
"school": "divination",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "A cantrip that determines whether a creature, object, or area is poisoned, venomous, or afflicted by acid. Utility spell, no combat effect."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Disrupt Undead",
"level": 0,
"school": "necromancy",
"range": "close",
"save": "none",
"base_dc": 10,
"effects": [
{
"type": "damage",
"formula": "1d6",
"types": ["force"],
"half_on_save": false
}
],
"description": "A cantrip: a line of crackling energy strikes one undead creature, dealing 1d6 points of damage (ranged touch attack, no save). The engine does not restrict the damage to undead targets."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Divine Favor",
"level": 1,
"school": "evocation",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "You draw upon divine power for 1 round/level: a +1 luck bonus on attack rolls and weapon damage per 4 caster levels. Simplified: the engine has no luck-bonus self-buff that preserves the pinned default-policy casting order, so no mechanical effect is applied."
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Doom",
"level": 1,
"school": "necromancy",
"range": "close",
"save": "will",
"base_dc": 10,
"effects": [
{
"type": "condition",
"condition_name": "shaken",
"duration_rounds": 10,
"save_negates": true
}
],
"description": "The target is paralyzed with fear and becomes shaken (-2 on attacks and saves; on a successful save it takes -1 instead, not modeled). Will negates. Duration 1 min/level approximated as 10 rounds."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Drunkard's Breath",
"level": 1,
"school": "abjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "For 1 hour/level you gain DR 5/- against alcohol effects and cannot become drunk. The engine has no DR buff target, so no mechanical effect is applied."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Endure Elements",
"level": 1,
"school": "abjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "For 24 hours you tolerate comfortable extremes of heat and cold (from -50 to 140 degrees F) without ill effect. Environmental protection, no combat-mechanical effect in the engine."
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "Enhance Water",
"level": 2,
"school": "abjuration",
"range": "personal",
"save": "none",
"base_dc": 10,
"effects": [],
"description": "You purify and bless 1 gallon/level of water (or one vial of holy/unholy water). Utility spell, no combat-mechanical effect in the engine."
}
+2 -2
View File
@@ -24,11 +24,11 @@ MAP = ROOT / "data" / "maps" / "sample_arena.yaml"
ONI = ROOT / "data" / "pcs" / "oni.json" ONI = ROOT / "data" / "pcs" / "oni.json"
GOBLIN = ROOT / "data" / "monsters" / "goblin.json" GOBLIN = ROOT / "data" / "monsters" / "goblin.json"
_NUM_SPELLS = 11 _NUM_SPELLS = 43
def test_runner_loads_realtime_spell_registry() -> None: def test_runner_loads_realtime_spell_registry() -> None:
"""The runner's registry source holds all 11 real spells from data/spells/.""" """The runner's registry source holds all 43 real spells from data/spells/."""
registry = load_spell_registry(SPELLS_DIR) registry = load_spell_registry(SPELLS_DIR)
assert len(registry) == _NUM_SPELLS assert len(registry) == _NUM_SPELLS
+124
View File
@@ -0,0 +1,124 @@
"""Batch 1 (V6a): the 32 PC-sheet spells added to data/spells/.
Given: the registry directory with the 11 original spells plus batch 1
When: load_spell_registry / load_spell is called
Then: every new spell loads with the expected level/school/range/save,
the registry grew by exactly 32, 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"
_ORIGINAL_COUNT = 11
# name -> (file stem, level, school, range, save)
_BATCH1: dict[str, tuple[str, int, str, str, str]] = {
"Abundant Ammunition": ("abundant_ammunition", 0, "transmutation", "close", "none"),
"Acid Splash": ("acid_splash", 0, "conjuration", "close", "none"),
"Aid": ("aid", 1, "enchantment", "long", "none"),
"Air Bubble": ("air_bubble", 1, "conjuration", "personal", "none"),
"Alarm": ("alarm", 1, "abjuration", "medium", "none"),
"Arcane Mark": ("arcane_mark", 0, "divination", "touch", "none"),
"Bane": ("bane", 1, "enchantment", "medium", "will"),
"Bear's Endurance": ("bears_endurance", 2, "transmutation", "touch", "fort"),
"Bladed Dash": ("bladed_dash", 1, "transmutation", "close", "none"),
"Bleed": ("bleed", 0, "necromancy", "close", "fort"),
"Bless": ("bless", 1, "enchantment", "medium", "none"),
"Blindness/Deafness": ("blindness_deafness", 2, "necromancy", "medium", "will"),
"Charm Person": ("charm_person", 1, "enchantment", "medium", "will"),
"Color Spray": ("color_spray", 1, "enchantment", "close", "will"),
"Comprehend Languages": ("comprehend_languages", 0, "divination", "personal", "none"),
"Create Alcool": ("create_alcool", 0, "conjuration", "close", "none"),
"Create Water": ("create_water", 1, "conjuration", "medium", "none"),
"Dancing Lights": ("dancing_lights", 0, "conjuration", "close", "none"),
"Darkness": ("darkness", 2, "evocation", "touch", "none"),
"Daze": ("daze", 0, "enchantment", "close", "will"),
"Deja Vu": ("deja_vu", 0, "divination", "personal", "none"),
"Delay Disease": ("delay_disease", 3, "abjuration", "touch", "fort"),
"Delay Poison": ("delay_poison", 2, "abjuration", "touch", "fort"),
"Detect Fiendish Presence": ("detect_fiendish_presence", 0, "divination", "close", "none"),
"Detect Magic": ("detect_magic", 0, "divination", "close", "none"),
"Detect Poison": ("detect_poison", 0, "divination", "close", "none"),
"Disrupt Undead": ("disrupt_undead", 0, "necromancy", "close", "none"),
"Divine Favor": ("divine_favor", 1, "evocation", "personal", "none"),
"Doom": ("doom", 1, "necromancy", "close", "will"),
"Drunkard's Breath": ("drunkards_breath", 1, "abjuration", "personal", "none"),
"Endure Elements": ("endure_elements", 1, "abjuration", "personal", "none"),
"Enhance Water": ("enhance_water", 2, "abjuration", "personal", "none"),
}
_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 TestBatch1Loading:
"""Each batch-1 spell loads with the expected metadata."""
@pytest.mark.parametrize(
("name", "stem", "level", "school", "range_", "save"),
[
(name, *fields) for name, fields in sorted(_BATCH1.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) == _ORIGINAL_COUNT + 32
assert set(_BATCH1) <= set(reg)
def test_every_effect_validates(self) -> None:
reg = load_spell_registry(_SPELLS_DIR)
for name in _BATCH1:
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
+5 -4
View File
@@ -123,13 +123,13 @@ def _make_engine(
class TestSpellLoading: class TestSpellLoading:
"""Given: data/spells/ directory with 11 JSON spell files """Given: data/spells/ directory with 43 JSON spell files
When: load_spell_registry is called When: load_spell_registry is called
Then: all 11 spells are loaded with correct fields.""" Then: all 43 spells are loaded with correct fields."""
def test_loads_all_10_spells(self) -> None: def test_loads_all_10_spells(self) -> None:
reg = load_spell_registry(_SPELLS_DIR) reg = load_spell_registry(_SPELLS_DIR)
assert len(reg) == 11 assert len(reg) == 43
def test_spell_names_present(self) -> None: def test_spell_names_present(self) -> None:
reg = load_spell_registry(_SPELLS_DIR) reg = load_spell_registry(_SPELLS_DIR)
@@ -139,7 +139,8 @@ class TestSpellLoading:
"Fear", "Bull's Strength", "Acid Arrow", "Fear", "Bull's Strength", "Acid Arrow",
"Summon Monster II", "Summon Monster II",
} }
assert set(reg) == expected assert expected <= set(reg)
assert len(reg) == 43
def test_fireball_fields(self) -> None: def test_fireball_fields(self) -> None:
spell = load_spell(_SPELLS_DIR / "fireball.json") spell = load_spell(_SPELLS_DIR / "fireball.json")