diff --git a/README.md b/README.md index 201e321..1456a84 100644 --- a/README.md +++ b/README.md @@ -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/`, 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, conditions effectives (`conditions.py`), manœuvres de combat, dons (55 noms résolus, `abilities.py` + chargeur) et capacités de classe (channel energy, diff --git a/data/spells/abundant_ammunition.json b/data/spells/abundant_ammunition.json new file mode 100644 index 0000000..f07bebb --- /dev/null +++ b/data/spells/abundant_ammunition.json @@ -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)." +} diff --git a/data/spells/acid_splash.json b/data/spells/acid_splash.json new file mode 100644 index 0000000..d3dd8f2 --- /dev/null +++ b/data/spells/acid_splash.json @@ -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)." +} diff --git a/data/spells/aid.json b/data/spells/aid.json new file mode 100644 index 0000000..e03fc1a --- /dev/null +++ b/data/spells/aid.json @@ -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." +} diff --git a/data/spells/air_bubble.json b/data/spells/air_bubble.json new file mode 100644 index 0000000..cd67e91 --- /dev/null +++ b/data/spells/air_bubble.json @@ -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." +} diff --git a/data/spells/alarm.json b/data/spells/alarm.json new file mode 100644 index 0000000..704eb8d --- /dev/null +++ b/data/spells/alarm.json @@ -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." +} diff --git a/data/spells/arcane_mark.json b/data/spells/arcane_mark.json new file mode 100644 index 0000000..a76eb77 --- /dev/null +++ b/data/spells/arcane_mark.json @@ -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." +} diff --git a/data/spells/bane.json b/data/spells/bane.json new file mode 100644 index 0000000..39f15e7 --- /dev/null +++ b/data/spells/bane.json @@ -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." +} diff --git a/data/spells/bears_endurance.json b/data/spells/bears_endurance.json new file mode 100644 index 0000000..64e4f90 --- /dev/null +++ b/data/spells/bears_endurance.json @@ -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." +} diff --git a/data/spells/bladed_dash.json b/data/spells/bladed_dash.json new file mode 100644 index 0000000..d69cdf9 --- /dev/null +++ b/data/spells/bladed_dash.json @@ -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." +} diff --git a/data/spells/bleed.json b/data/spells/bleed.json new file mode 100644 index 0000000..4e17a1f --- /dev/null +++ b/data/spells/bleed.json @@ -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." +} diff --git a/data/spells/bless.json b/data/spells/bless.json new file mode 100644 index 0000000..33e54e6 --- /dev/null +++ b/data/spells/bless.json @@ -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." +} diff --git a/data/spells/blindness_deafness.json b/data/spells/blindness_deafness.json new file mode 100644 index 0000000..fcdfcba --- /dev/null +++ b/data/spells/blindness_deafness.json @@ -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." +} diff --git a/data/spells/charm_person.json b/data/spells/charm_person.json new file mode 100644 index 0000000..19e3e48 --- /dev/null +++ b/data/spells/charm_person.json @@ -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." +} diff --git a/data/spells/color_spray.json b/data/spells/color_spray.json new file mode 100644 index 0000000..16cd31d --- /dev/null +++ b/data/spells/color_spray.json @@ -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." +} diff --git a/data/spells/comprehend_languages.json b/data/spells/comprehend_languages.json new file mode 100644 index 0000000..f07d716 --- /dev/null +++ b/data/spells/comprehend_languages.json @@ -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." +} diff --git a/data/spells/create_alcool.json b/data/spells/create_alcool.json new file mode 100644 index 0000000..54e0b1f --- /dev/null +++ b/data/spells/create_alcool.json @@ -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." +} diff --git a/data/spells/create_water.json b/data/spells/create_water.json new file mode 100644 index 0000000..03aa9d8 --- /dev/null +++ b/data/spells/create_water.json @@ -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." +} diff --git a/data/spells/dancing_lights.json b/data/spells/dancing_lights.json new file mode 100644 index 0000000..7521913 --- /dev/null +++ b/data/spells/dancing_lights.json @@ -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." +} diff --git a/data/spells/darkness.json b/data/spells/darkness.json new file mode 100644 index 0000000..bf21c1a --- /dev/null +++ b/data/spells/darkness.json @@ -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." +} diff --git a/data/spells/daze.json b/data/spells/daze.json new file mode 100644 index 0000000..52c4ddb --- /dev/null +++ b/data/spells/daze.json @@ -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." +} diff --git a/data/spells/deja_vu.json b/data/spells/deja_vu.json new file mode 100644 index 0000000..97e4936 --- /dev/null +++ b/data/spells/deja_vu.json @@ -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." +} diff --git a/data/spells/delay_disease.json b/data/spells/delay_disease.json new file mode 100644 index 0000000..6d9ae75 --- /dev/null +++ b/data/spells/delay_disease.json @@ -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)." +} diff --git a/data/spells/delay_poison.json b/data/spells/delay_poison.json new file mode 100644 index 0000000..bb1f45f --- /dev/null +++ b/data/spells/delay_poison.json @@ -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)." +} diff --git a/data/spells/detect_fiendish_presence.json b/data/spells/detect_fiendish_presence.json new file mode 100644 index 0000000..0b09c44 --- /dev/null +++ b/data/spells/detect_fiendish_presence.json @@ -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." +} diff --git a/data/spells/detect_magic.json b/data/spells/detect_magic.json new file mode 100644 index 0000000..6c471ba --- /dev/null +++ b/data/spells/detect_magic.json @@ -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." +} diff --git a/data/spells/detect_poison.json b/data/spells/detect_poison.json new file mode 100644 index 0000000..ff5767f --- /dev/null +++ b/data/spells/detect_poison.json @@ -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." +} diff --git a/data/spells/disrupt_undead.json b/data/spells/disrupt_undead.json new file mode 100644 index 0000000..1720b46 --- /dev/null +++ b/data/spells/disrupt_undead.json @@ -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." +} diff --git a/data/spells/divine_favor.json b/data/spells/divine_favor.json new file mode 100644 index 0000000..dd99461 --- /dev/null +++ b/data/spells/divine_favor.json @@ -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." +} diff --git a/data/spells/doom.json b/data/spells/doom.json new file mode 100644 index 0000000..9f3227f --- /dev/null +++ b/data/spells/doom.json @@ -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." +} diff --git a/data/spells/drunkards_breath.json b/data/spells/drunkards_breath.json new file mode 100644 index 0000000..bdef68e --- /dev/null +++ b/data/spells/drunkards_breath.json @@ -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." +} diff --git a/data/spells/endure_elements.json b/data/spells/endure_elements.json new file mode 100644 index 0000000..03aaff1 --- /dev/null +++ b/data/spells/endure_elements.json @@ -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." +} diff --git a/data/spells/enhance_water.json b/data/spells/enhance_water.json new file mode 100644 index 0000000..6d53fe9 --- /dev/null +++ b/data/spells/enhance_water.json @@ -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." +} diff --git a/tests/test_runner_spells.py b/tests/test_runner_spells.py index 03d0b66..5e5dfcc 100644 --- a/tests/test_runner_spells.py +++ b/tests/test_runner_spells.py @@ -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 = 11 +_NUM_SPELLS = 43 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) assert len(registry) == _NUM_SPELLS diff --git a/tests/test_spell_registry_batch1.py b/tests/test_spell_registry_batch1.py new file mode 100644 index 0000000..2a9f4b6 --- /dev/null +++ b/tests/test_spell_registry_batch1.py @@ -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 diff --git a/tests/test_spells.py b/tests/test_spells.py index 723316c..73481f7 100644 --- a/tests/test_spells.py +++ b/tests/test_spells.py @@ -123,13 +123,13 @@ def _make_engine( 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 - 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: reg = load_spell_registry(_SPELLS_DIR) - assert len(reg) == 11 + assert len(reg) == 43 def test_spell_names_present(self) -> None: reg = load_spell_registry(_SPELLS_DIR) @@ -139,7 +139,8 @@ class TestSpellLoading: "Fear", "Bull's Strength", "Acid Arrow", "Summon Monster II", } - assert set(reg) == expected + assert expected <= set(reg) + assert len(reg) == 43 def test_fireball_fields(self) -> None: spell = load_spell(_SPELLS_DIR / "fireball.json")