From 543da6a931ac388cdb34ef8f8995ae303ad04eb7 Mon Sep 17 00:00:00 2001 From: ctan Date: Thu, 10 Sep 2026 19:12:57 +0200 Subject: [PATCH] data(spells): add batch 2 of PC spell list (32 spells) --- README.md | 2 +- data/spells/enhanced_diplomacy.json | 10 ++ data/spells/face_of_the_devourer.json | 10 ++ data/spells/flurry_of_snowballs.json | 24 ++++ data/spells/forbid_action.json | 17 +++ data/spells/grasp.json | 10 ++ data/spells/guidance.json | 10 ++ data/spells/heroism.json | 21 +++ data/spells/hex_vulnerability.json | 10 ++ data/spells/hidden_diplomacy.json | 10 ++ data/spells/hide_from_undead.json | 10 ++ data/spells/ice_slick.json | 30 ++++ data/spells/ill_omen.json | 10 ++ data/spells/inflict_light_wounds.json | 17 +++ data/spells/ironskin.json | 18 +++ data/spells/light.json | 10 ++ data/spells/mage_hand.json | 10 ++ data/spells/make_whole.json | 10 ++ data/spells/masterwork_transformation.json | 10 ++ data/spells/mending.json | 10 ++ data/spells/message.json | 10 ++ data/spells/mirror_image.json | 17 +++ data/spells/oath_of_anonymity.json | 10 ++ data/spells/obscuring_mist.json | 24 ++++ data/spells/open_close.json | 10 ++ data/spells/owls_wisdom.json | 10 ++ data/spells/prestidigitation.json | 10 ++ data/spells/protection_from_evil.json | 10 ++ .../spells/protection_from_evil_communal.json | 10 ++ data/spells/protection_from_law.json | 10 ++ data/spells/protection_from_law_communal.json | 10 ++ data/spells/purify_food_and_drink.json | 10 ++ data/spells/putrefy_food_and_drink.json | 10 ++ tests/test_runner_spells.py | 4 +- tests/test_spell_registry_batch1.py | 3 +- tests/test_spell_registry_batch2.py | 130 ++++++++++++++++++ tests/test_spells.py | 8 +- 37 files changed, 547 insertions(+), 8 deletions(-) create mode 100644 data/spells/enhanced_diplomacy.json create mode 100644 data/spells/face_of_the_devourer.json create mode 100644 data/spells/flurry_of_snowballs.json create mode 100644 data/spells/forbid_action.json create mode 100644 data/spells/grasp.json create mode 100644 data/spells/guidance.json create mode 100644 data/spells/heroism.json create mode 100644 data/spells/hex_vulnerability.json create mode 100644 data/spells/hidden_diplomacy.json create mode 100644 data/spells/hide_from_undead.json create mode 100644 data/spells/ice_slick.json create mode 100644 data/spells/ill_omen.json create mode 100644 data/spells/inflict_light_wounds.json create mode 100644 data/spells/ironskin.json create mode 100644 data/spells/light.json create mode 100644 data/spells/mage_hand.json create mode 100644 data/spells/make_whole.json create mode 100644 data/spells/masterwork_transformation.json create mode 100644 data/spells/mending.json create mode 100644 data/spells/message.json create mode 100644 data/spells/mirror_image.json create mode 100644 data/spells/oath_of_anonymity.json create mode 100644 data/spells/obscuring_mist.json create mode 100644 data/spells/open_close.json create mode 100644 data/spells/owls_wisdom.json create mode 100644 data/spells/prestidigitation.json create mode 100644 data/spells/protection_from_evil.json create mode 100644 data/spells/protection_from_evil_communal.json create mode 100644 data/spells/protection_from_law.json create mode 100644 data/spells/protection_from_law_communal.json create mode 100644 data/spells/purify_food_and_drink.json create mode 100644 data/spells/putrefy_food_and_drink.json create mode 100644 tests/test_spell_registry_batch2.py diff --git a/README.md b/README.md index 1456a84..76ed020 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/`, 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, diff --git a/data/spells/enhanced_diplomacy.json b/data/spells/enhanced_diplomacy.json new file mode 100644 index 0000000..c7c7ca4 --- /dev/null +++ b/data/spells/enhanced_diplomacy.json @@ -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." +} diff --git a/data/spells/face_of_the_devourer.json b/data/spells/face_of_the_devourer.json new file mode 100644 index 0000000..6044aaf --- /dev/null +++ b/data/spells/face_of_the_devourer.json @@ -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." +} diff --git a/data/spells/flurry_of_snowballs.json b/data/spells/flurry_of_snowballs.json new file mode 100644 index 0000000..89b8621 --- /dev/null +++ b/data/spells/flurry_of_snowballs.json @@ -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." +} diff --git a/data/spells/forbid_action.json b/data/spells/forbid_action.json new file mode 100644 index 0000000..903c135 --- /dev/null +++ b/data/spells/forbid_action.json @@ -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." +} diff --git a/data/spells/grasp.json b/data/spells/grasp.json new file mode 100644 index 0000000..75b2079 --- /dev/null +++ b/data/spells/grasp.json @@ -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." +} diff --git a/data/spells/guidance.json b/data/spells/guidance.json new file mode 100644 index 0000000..230aad1 --- /dev/null +++ b/data/spells/guidance.json @@ -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." +} diff --git a/data/spells/heroism.json b/data/spells/heroism.json new file mode 100644 index 0000000..a1693d8 --- /dev/null +++ b/data/spells/heroism.json @@ -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)." +} diff --git a/data/spells/hex_vulnerability.json b/data/spells/hex_vulnerability.json new file mode 100644 index 0000000..2826e17 --- /dev/null +++ b/data/spells/hex_vulnerability.json @@ -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." +} diff --git a/data/spells/hidden_diplomacy.json b/data/spells/hidden_diplomacy.json new file mode 100644 index 0000000..e74d507 --- /dev/null +++ b/data/spells/hidden_diplomacy.json @@ -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." +} diff --git a/data/spells/hide_from_undead.json b/data/spells/hide_from_undead.json new file mode 100644 index 0000000..cfea54d --- /dev/null +++ b/data/spells/hide_from_undead.json @@ -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." +} diff --git a/data/spells/ice_slick.json b/data/spells/ice_slick.json new file mode 100644 index 0000000..3b004bd --- /dev/null +++ b/data/spells/ice_slick.json @@ -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." +} diff --git a/data/spells/ill_omen.json b/data/spells/ill_omen.json new file mode 100644 index 0000000..208a3ac --- /dev/null +++ b/data/spells/ill_omen.json @@ -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." +} diff --git a/data/spells/inflict_light_wounds.json b/data/spells/inflict_light_wounds.json new file mode 100644 index 0000000..463ca91 --- /dev/null +++ b/data/spells/inflict_light_wounds.json @@ -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." +} diff --git a/data/spells/ironskin.json b/data/spells/ironskin.json new file mode 100644 index 0000000..86d1128 --- /dev/null +++ b/data/spells/ironskin.json @@ -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." +} diff --git a/data/spells/light.json b/data/spells/light.json new file mode 100644 index 0000000..353a09c --- /dev/null +++ b/data/spells/light.json @@ -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." +} diff --git a/data/spells/mage_hand.json b/data/spells/mage_hand.json new file mode 100644 index 0000000..c1723ea --- /dev/null +++ b/data/spells/mage_hand.json @@ -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." +} diff --git a/data/spells/make_whole.json b/data/spells/make_whole.json new file mode 100644 index 0000000..6893eb9 --- /dev/null +++ b/data/spells/make_whole.json @@ -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." +} diff --git a/data/spells/masterwork_transformation.json b/data/spells/masterwork_transformation.json new file mode 100644 index 0000000..b221356 --- /dev/null +++ b/data/spells/masterwork_transformation.json @@ -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." +} diff --git a/data/spells/mending.json b/data/spells/mending.json new file mode 100644 index 0000000..c286e6b --- /dev/null +++ b/data/spells/mending.json @@ -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." +} diff --git a/data/spells/message.json b/data/spells/message.json new file mode 100644 index 0000000..127f9a4 --- /dev/null +++ b/data/spells/message.json @@ -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." +} diff --git a/data/spells/mirror_image.json b/data/spells/mirror_image.json new file mode 100644 index 0000000..1602825 --- /dev/null +++ b/data/spells/mirror_image.json @@ -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." +} diff --git a/data/spells/oath_of_anonymity.json b/data/spells/oath_of_anonymity.json new file mode 100644 index 0000000..7f54321 --- /dev/null +++ b/data/spells/oath_of_anonymity.json @@ -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." +} diff --git a/data/spells/obscuring_mist.json b/data/spells/obscuring_mist.json new file mode 100644 index 0000000..9965645 --- /dev/null +++ b/data/spells/obscuring_mist.json @@ -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." +} diff --git a/data/spells/open_close.json b/data/spells/open_close.json new file mode 100644 index 0000000..ae7d4f3 --- /dev/null +++ b/data/spells/open_close.json @@ -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." +} diff --git a/data/spells/owls_wisdom.json b/data/spells/owls_wisdom.json new file mode 100644 index 0000000..3223684 --- /dev/null +++ b/data/spells/owls_wisdom.json @@ -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." +} diff --git a/data/spells/prestidigitation.json b/data/spells/prestidigitation.json new file mode 100644 index 0000000..6baca5f --- /dev/null +++ b/data/spells/prestidigitation.json @@ -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." +} diff --git a/data/spells/protection_from_evil.json b/data/spells/protection_from_evil.json new file mode 100644 index 0000000..5f26b89 --- /dev/null +++ b/data/spells/protection_from_evil.json @@ -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." +} diff --git a/data/spells/protection_from_evil_communal.json b/data/spells/protection_from_evil_communal.json new file mode 100644 index 0000000..0182edc --- /dev/null +++ b/data/spells/protection_from_evil_communal.json @@ -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." +} diff --git a/data/spells/protection_from_law.json b/data/spells/protection_from_law.json new file mode 100644 index 0000000..fecb172 --- /dev/null +++ b/data/spells/protection_from_law.json @@ -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." +} diff --git a/data/spells/protection_from_law_communal.json b/data/spells/protection_from_law_communal.json new file mode 100644 index 0000000..dd06bda --- /dev/null +++ b/data/spells/protection_from_law_communal.json @@ -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." +} diff --git a/data/spells/purify_food_and_drink.json b/data/spells/purify_food_and_drink.json new file mode 100644 index 0000000..c97bdbb --- /dev/null +++ b/data/spells/purify_food_and_drink.json @@ -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." +} diff --git a/data/spells/putrefy_food_and_drink.json b/data/spells/putrefy_food_and_drink.json new file mode 100644 index 0000000..95a4407 --- /dev/null +++ b/data/spells/putrefy_food_and_drink.json @@ -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." +} diff --git a/tests/test_runner_spells.py b/tests/test_runner_spells.py index 5e5dfcc..d684255 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 = 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 diff --git a/tests/test_spell_registry_batch1.py b/tests/test_spell_registry_batch1.py index 2a9f4b6..02b3c6f 100644 --- a/tests/test_spell_registry_batch1.py +++ b/tests/test_spell_registry_batch1.py @@ -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: diff --git a/tests/test_spell_registry_batch2.py b/tests/test_spell_registry_batch2.py new file mode 100644 index 0000000..1073de8 --- /dev/null +++ b/tests/test_spell_registry_batch2.py @@ -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 diff --git a/tests/test_spells.py b/tests/test_spells.py index 73481f7..30c3c8e 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 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")