Add feature roadmap and implementation plan for D&D spell extraction and validation upgrades
This commit is contained in:
25
internal/modules/dnd/spells/catalog/SOURCES.md
Normal file
25
internal/modules/dnd/spells/catalog/SOURCES.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# D&D 5e 2014 SRD spell catalog sources
|
||||
|
||||
The canonical catalog in `assets/dnd_5e_2014_srd_spells.json` was consolidated
|
||||
from the eight class spell lists in the System Reference Document 5.1. Repeated
|
||||
class-list entries were combined into one spell record while retaining spell
|
||||
level and class membership. The consolidation produced 319 unique spells and
|
||||
779 class memberships.
|
||||
|
||||
Source: [System Reference Document 5.1](https://dnd.wizards.com/resources/systems-reference-document)
|
||||
|
||||
License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
Required attribution:
|
||||
|
||||
> This work includes material taken from the System Reference Document 5.1
|
||||
> (“SRD 5.1”) by Wizards of the Coast LLC and available at
|
||||
> https://dnd.wizards.com/resources/systems-reference-document. The SRD 5.1 is
|
||||
> licensed under the Creative Commons Attribution 4.0 International License
|
||||
> available at https://creativecommons.org/licenses/by/4.0/legalcode.
|
||||
|
||||
The catalog contains names, spell levels, and class-list membership only. It
|
||||
does not reproduce spell descriptions. Canonical spell names retain the
|
||||
punctuation used by the source lists. The package lookup normalizes common
|
||||
apostrophe variants, case, and repeated whitespace without changing those
|
||||
display names.
|
||||
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"id": "dnd-5e-2014-srd-spells",
|
||||
"ruleset": "dnd-5e-2014",
|
||||
"source": {
|
||||
"title": "System Reference Document 5.1",
|
||||
"version": "5.1",
|
||||
"url": "https://dnd.wizards.com/resources/systems-reference-document",
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"spells": [
|
||||
{"name":"Acid Arrow","level":2,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Acid Splash","level":0,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Aid","level":2,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Alarm","level":1,"classes":["ranger","wizard"],"aliases":[]},
|
||||
{"name":"Alter Self","level":2,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Animal Friendship","level":1,"classes":["bard","druid","ranger"],"aliases":[]},
|
||||
{"name":"Animal Messenger","level":2,"classes":["bard","druid","ranger"],"aliases":[]},
|
||||
{"name":"Animal Shapes","level":8,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Animate Dead","level":3,"classes":["cleric","wizard"],"aliases":[]},
|
||||
{"name":"Animate Objects","level":5,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Antilife Shell","level":5,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Antimagic Field","level":8,"classes":["cleric","wizard"],"aliases":[]},
|
||||
{"name":"Antipathy/Sympathy","level":8,"classes":["druid","wizard"],"aliases":[]},
|
||||
{"name":"Arcane Eye","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Arcane Hand","level":5,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Arcane Lock","level":2,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Arcane Sword","level":7,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Arcanist’s Magic Aura","level":2,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Astral Projection","level":9,"classes":["cleric","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Augury","level":2,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Awaken","level":5,"classes":["bard","druid"],"aliases":[]},
|
||||
{"name":"Bane","level":1,"classes":["bard","cleric"],"aliases":[]},
|
||||
{"name":"Banishment","level":4,"classes":["cleric","paladin","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Barkskin","level":2,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Beacon of Hope","level":3,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Bestow Curse","level":3,"classes":["bard","cleric","wizard"],"aliases":[]},
|
||||
{"name":"Black Tentacles","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Blade Barrier","level":6,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Bless","level":1,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Blight","level":4,"classes":["druid","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Blindness/Deafness","level":2,"classes":["bard","cleric","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Blink","level":3,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Blur","level":2,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Branding Smite","level":2,"classes":["paladin"],"aliases":[]},
|
||||
{"name":"Burning Hands","level":1,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Call Lightning","level":3,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Calm Emotions","level":2,"classes":["bard","cleric"],"aliases":[]},
|
||||
{"name":"Chain Lightning","level":6,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Charm Person","level":1,"classes":["bard","druid","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Chill Touch","level":0,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Circle of Death","level":6,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Clairvoyance","level":3,"classes":["bard","cleric","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Clone","level":8,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Cloudkill","level":5,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Color Spray","level":1,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Command","level":1,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Commune","level":5,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Commune with Nature","level":5,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Comprehend Languages","level":1,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Compulsion","level":4,"classes":["bard"],"aliases":[]},
|
||||
{"name":"Cone of Cold","level":5,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Confusion","level":4,"classes":["bard","druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Conjure Animals","level":3,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Conjure Celestial","level":7,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Conjure Elemental","level":5,"classes":["druid","wizard"],"aliases":[]},
|
||||
{"name":"Conjure Fey","level":6,"classes":["druid","warlock"],"aliases":[]},
|
||||
{"name":"Conjure Minor Elementals","level":4,"classes":["druid","wizard"],"aliases":[]},
|
||||
{"name":"Conjure Woodland Beings","level":4,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Contact Other Plane","level":5,"classes":["warlock","wizard"],"aliases":[]},
|
||||
{"name":"Contagion","level":5,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Contingency","level":6,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Continual Flame","level":2,"classes":["cleric","wizard"],"aliases":[]},
|
||||
{"name":"Control Water","level":4,"classes":["cleric","druid","wizard"],"aliases":[]},
|
||||
{"name":"Control Weather","level":8,"classes":["cleric","druid","wizard"],"aliases":[]},
|
||||
{"name":"Counterspell","level":3,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Create Food and Water","level":3,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Create Undead","level":6,"classes":["cleric","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Create or Destroy Water","level":1,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Creation","level":5,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Cure Wounds","level":1,"classes":["bard","cleric","druid","paladin","ranger"],"aliases":[]},
|
||||
{"name":"Dancing Lights","level":0,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Darkness","level":2,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Darkvision","level":2,"classes":["druid","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Daylight","level":3,"classes":["cleric","druid","paladin","ranger","sorcerer"],"aliases":[]},
|
||||
{"name":"Death Ward","level":4,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Delayed Blast Fireball","level":7,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Demiplane","level":8,"classes":["warlock","wizard"],"aliases":[]},
|
||||
{"name":"Detect Evil and Good","level":1,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Detect Magic","level":1,"classes":["bard","cleric","druid","paladin","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Detect Poison and Disease","level":1,"classes":["cleric","druid","paladin","ranger"],"aliases":[]},
|
||||
{"name":"Detect Thoughts","level":2,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Dimension Door","level":4,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Disguise Self","level":1,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Disintegrate","level":6,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Dispel Evil and Good","level":5,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Dispel Magic","level":3,"classes":["bard","cleric","druid","paladin","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Divination","level":4,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Divine Favor","level":1,"classes":["paladin"],"aliases":[]},
|
||||
{"name":"Divine Word","level":7,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Dominate Beast","level":4,"classes":["druid","sorcerer"],"aliases":[]},
|
||||
{"name":"Dominate Monster","level":8,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Dominate Person","level":5,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Dream","level":5,"classes":["bard","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Druidcraft","level":0,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Earthquake","level":8,"classes":["cleric","druid","sorcerer"],"aliases":[]},
|
||||
{"name":"Eldritch Blast","level":0,"classes":["warlock"],"aliases":[]},
|
||||
{"name":"Enhance Ability","level":2,"classes":["bard","cleric","druid","sorcerer"],"aliases":[]},
|
||||
{"name":"Enlarge/Reduce","level":2,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Entangle","level":1,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Enthrall","level":2,"classes":["bard","warlock"],"aliases":[]},
|
||||
{"name":"Etherealness","level":7,"classes":["bard","cleric","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Expeditious Retreat","level":1,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Eyebite","level":6,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Fabricate","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Faerie Fire","level":1,"classes":["bard","druid"],"aliases":[]},
|
||||
{"name":"Faithful Hound","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"False Life","level":1,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Fear","level":3,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Feather Fall","level":1,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Feeblemind","level":8,"classes":["bard","druid","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Find Familiar","level":1,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Find Steed","level":2,"classes":["paladin"],"aliases":[]},
|
||||
{"name":"Find Traps","level":2,"classes":["cleric","druid","ranger"],"aliases":[]},
|
||||
{"name":"Find the Path","level":6,"classes":["bard","cleric","druid"],"aliases":[]},
|
||||
{"name":"Finger of Death","level":7,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Fire Bolt","level":0,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Fire Shield","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Fire Storm","level":7,"classes":["cleric","druid","sorcerer"],"aliases":[]},
|
||||
{"name":"Fireball","level":3,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Flame Blade","level":2,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Flame Strike","level":5,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Flaming Sphere","level":2,"classes":["druid","wizard"],"aliases":[]},
|
||||
{"name":"Flesh to Stone","level":6,"classes":["warlock","wizard"],"aliases":[]},
|
||||
{"name":"Floating Disk","level":1,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Fly","level":3,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Fog Cloud","level":1,"classes":["druid","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Forbiddance","level":6,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Forcecage","level":7,"classes":["bard","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Foresight","level":9,"classes":["bard","druid","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Freedom of Movement","level":4,"classes":["bard","cleric","druid","ranger"],"aliases":[]},
|
||||
{"name":"Freezing Sphere","level":6,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Gaseous Form","level":3,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Gate","level":9,"classes":["cleric","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Geas","level":5,"classes":["bard","cleric","druid","paladin","wizard"],"aliases":[]},
|
||||
{"name":"Gentle Repose","level":2,"classes":["cleric","wizard"],"aliases":[]},
|
||||
{"name":"Giant Insect","level":4,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Glibness","level":8,"classes":["bard","warlock"],"aliases":[]},
|
||||
{"name":"Globe of Invulnerability","level":6,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Glyph of Warding","level":3,"classes":["bard","cleric","wizard"],"aliases":[]},
|
||||
{"name":"Goodberry","level":1,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Grease","level":1,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Greater Invisibility","level":4,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Greater Restoration","level":5,"classes":["bard","cleric","druid"],"aliases":[]},
|
||||
{"name":"Guardian of Faith","level":4,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Guards and Wards","level":6,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Guidance","level":0,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Guiding Bolt","level":1,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Gust of Wind","level":2,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Hallow","level":5,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Hallucinatory Terrain","level":4,"classes":["bard","druid","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Harm","level":6,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Haste","level":3,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Heal","level":6,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Healing Word","level":1,"classes":["bard","cleric","druid"],"aliases":[]},
|
||||
{"name":"Heat Metal","level":2,"classes":["bard","druid"],"aliases":[]},
|
||||
{"name":"Hellish Rebuke","level":1,"classes":["warlock"],"aliases":[]},
|
||||
{"name":"Heroes’ Feast","level":6,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Heroism","level":1,"classes":["bard","paladin"],"aliases":[]},
|
||||
{"name":"Hideous Laughter","level":1,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Hold Monster","level":5,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Hold Person","level":2,"classes":["bard","cleric","druid","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Holy Aura","level":8,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Hunter’s Mark","level":1,"classes":["ranger"],"aliases":[]},
|
||||
{"name":"Hypnotic Pattern","level":3,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Ice Storm","level":4,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Identify","level":1,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Illusory Script","level":1,"classes":["bard","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Imprisonment","level":9,"classes":["warlock","wizard"],"aliases":[]},
|
||||
{"name":"Incendiary Cloud","level":8,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Inflict Wounds","level":1,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Insect Plague","level":5,"classes":["cleric","druid","sorcerer"],"aliases":[]},
|
||||
{"name":"Instant Summons","level":6,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Invisibility","level":2,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Irresistible Dance","level":6,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Jump","level":1,"classes":["druid","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Knock","level":2,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Legend Lore","level":5,"classes":["bard","cleric","wizard"],"aliases":[]},
|
||||
{"name":"Lesser Restoration","level":2,"classes":["bard","cleric","druid","paladin","ranger"],"aliases":[]},
|
||||
{"name":"Levitate","level":2,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Light","level":0,"classes":["bard","cleric","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Lightning Bolt","level":3,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Locate Animals or Plants","level":2,"classes":["bard","druid","ranger"],"aliases":[]},
|
||||
{"name":"Locate Creature","level":4,"classes":["bard","cleric","druid","paladin","ranger","wizard"],"aliases":[]},
|
||||
{"name":"Locate Object","level":2,"classes":["bard","cleric","druid","paladin","ranger","wizard"],"aliases":[]},
|
||||
{"name":"Longstrider","level":1,"classes":["bard","druid","ranger","wizard"],"aliases":[]},
|
||||
{"name":"Mage Armor","level":1,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Mage Hand","level":0,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Magic Circle","level":3,"classes":["cleric","paladin","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Magic Jar","level":6,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Magic Missile","level":1,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Magic Mouth","level":2,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Magic Weapon","level":2,"classes":["paladin","wizard"],"aliases":[]},
|
||||
{"name":"Magnificent Mansion","level":7,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Major Image","level":3,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Mass Cure Wounds","level":5,"classes":["bard","cleric","druid"],"aliases":[]},
|
||||
{"name":"Mass Heal","level":9,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Mass Healing Word","level":3,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Mass Suggestion","level":6,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Maze","level":8,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Meld into Stone","level":3,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Mending","level":0,"classes":["bard","cleric","druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Message","level":0,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Meteor Swarm","level":9,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Mind Blank","level":8,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Minor Illusion","level":0,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Mirage Arcane","level":7,"classes":["bard","druid","wizard"],"aliases":[]},
|
||||
{"name":"Mirror Image","level":2,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Mislead","level":5,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Misty Step","level":2,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Modify Memory","level":5,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Moonbeam","level":2,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Move Earth","level":6,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Nondetection","level":3,"classes":["bard","ranger","wizard"],"aliases":[]},
|
||||
{"name":"Pass without Trace","level":2,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Passwall","level":5,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Phantasmal Killer","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Phantom Steed","level":3,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Planar Ally","level":6,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Planar Binding","level":5,"classes":["bard","cleric","druid","wizard"],"aliases":[]},
|
||||
{"name":"Plane Shift","level":7,"classes":["cleric","druid","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Plant Growth","level":3,"classes":["bard","druid","ranger"],"aliases":[]},
|
||||
{"name":"Poison Spray","level":0,"classes":["druid","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Polymorph","level":4,"classes":["bard","druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Power Word Kill","level":9,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Power Word Stun","level":8,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Prayer of Healing","level":2,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Prestidigitation","level":0,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Prismatic Spray","level":7,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Prismatic Wall","level":9,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Private Sanctum","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Produce Flame","level":0,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Programmed Illusion","level":6,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Project Image","level":7,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Protection from Energy","level":3,"classes":["cleric","druid","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Protection from Evil and Good","level":1,"classes":["cleric","paladin","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Protection from Poison","level":2,"classes":["cleric","druid","paladin","ranger"],"aliases":[]},
|
||||
{"name":"Purify Food and Drink","level":1,"classes":["cleric","druid","paladin"],"aliases":[]},
|
||||
{"name":"Raise Dead","level":5,"classes":["bard","cleric","paladin"],"aliases":[]},
|
||||
{"name":"Ray of Enfeeblement","level":2,"classes":["warlock","wizard"],"aliases":[]},
|
||||
{"name":"Ray of Frost","level":0,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Regenerate","level":7,"classes":["bard","cleric","druid"],"aliases":[]},
|
||||
{"name":"Reincarnate","level":5,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Remove Curse","level":3,"classes":["cleric","paladin","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Resilient Sphere","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Resistance","level":0,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"Resurrection","level":7,"classes":["bard","cleric"],"aliases":[]},
|
||||
{"name":"Reverse Gravity","level":7,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Revivify","level":3,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Rope Trick","level":2,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Sacred Flame","level":0,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Sanctuary","level":1,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Scorching Ray","level":2,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Scrying","level":5,"classes":["bard","cleric","druid","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Secret Chest","level":4,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"See Invisibility","level":2,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Seeming","level":5,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Sending","level":3,"classes":["bard","cleric","wizard"],"aliases":[]},
|
||||
{"name":"Sequester","level":7,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Shapechange","level":9,"classes":["druid","wizard"],"aliases":[]},
|
||||
{"name":"Shatter","level":2,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Shield","level":1,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Shield of Faith","level":1,"classes":["cleric","paladin"],"aliases":[]},
|
||||
{"name":"Shillelagh","level":0,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Shocking Grasp","level":0,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Silence","level":2,"classes":["bard","cleric","ranger"],"aliases":[]},
|
||||
{"name":"Silent Image","level":1,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Simulacrum","level":7,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Sleep","level":1,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Sleet Storm","level":3,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Slow","level":3,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Spare the Dying","level":0,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Speak with Animals","level":1,"classes":["bard","druid","ranger"],"aliases":[]},
|
||||
{"name":"Speak with Dead","level":3,"classes":["bard","cleric"],"aliases":[]},
|
||||
{"name":"Speak with Plants","level":3,"classes":["bard","druid","ranger"],"aliases":[]},
|
||||
{"name":"Spider Climb","level":2,"classes":["sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Spike Growth","level":2,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Spirit Guardians","level":3,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Spiritual Weapon","level":2,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Stinking Cloud","level":3,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Stone Shape","level":4,"classes":["cleric","druid","wizard"],"aliases":[]},
|
||||
{"name":"Stoneskin","level":4,"classes":["druid","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Storm of Vengeance","level":9,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Suggestion","level":2,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Sunbeam","level":6,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Sunburst","level":8,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Symbol","level":7,"classes":["bard","cleric","wizard"],"aliases":[]},
|
||||
{"name":"Telekinesis","level":5,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Telepathic Bond","level":5,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Teleport","level":7,"classes":["bard","cleric","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Teleportation Circle","level":5,"classes":["bard","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Thaumaturgy","level":0,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Thunderwave","level":1,"classes":["bard","druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Time Stop","level":9,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Tiny Hut","level":3,"classes":["bard","wizard"],"aliases":[]},
|
||||
{"name":"Tongues","level":3,"classes":["bard","cleric","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Transport via Plants","level":6,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Tree Stride","level":5,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"True Polymorph","level":9,"classes":["bard","warlock","wizard"],"aliases":[]},
|
||||
{"name":"True Resurrection","level":9,"classes":["cleric","druid"],"aliases":[]},
|
||||
{"name":"True Seeing","level":6,"classes":["bard","cleric","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"True Strike","level":0,"classes":["bard","sorcerer","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Unseen Servant","level":1,"classes":["bard","warlock","wizard"],"aliases":[]},
|
||||
{"name":"Vampiric Touch","level":3,"classes":["warlock","wizard"],"aliases":[]},
|
||||
{"name":"Vicious Mockery","level":0,"classes":["bard"],"aliases":[]},
|
||||
{"name":"Wall of Fire","level":4,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Wall of Force","level":5,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Wall of Ice","level":6,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Wall of Stone","level":5,"classes":["druid","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Wall of Thorns","level":6,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Warding Bond","level":2,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Water Breathing","level":3,"classes":["druid","ranger","sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Water Walk","level":3,"classes":["cleric","druid","ranger","sorcerer"],"aliases":[]},
|
||||
{"name":"Web","level":2,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Weird","level":9,"classes":["wizard"],"aliases":[]},
|
||||
{"name":"Wind Walk","level":6,"classes":["druid"],"aliases":[]},
|
||||
{"name":"Wind Wall","level":3,"classes":["druid","ranger"],"aliases":[]},
|
||||
{"name":"Wish","level":9,"classes":["sorcerer","wizard"],"aliases":[]},
|
||||
{"name":"Word of Recall","level":6,"classes":["cleric"],"aliases":[]},
|
||||
{"name":"Zone of Truth","level":2,"classes":["bard","cleric","paladin"],"aliases":[]}
|
||||
]
|
||||
}
|
||||
214
internal/modules/dnd/spells/catalog/catalog.go
Normal file
214
internal/modules/dnd/spells/catalog/catalog.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// Package catalog provides immutable D&D spell reference catalogs.
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
SRD5E2014ID = "dnd-5e-2014-srd-spells"
|
||||
SRD5E2014Ruleset = "dnd-5e-2014"
|
||||
)
|
||||
|
||||
//go:embed assets/dnd_5e_2014_srd_spells.json
|
||||
var catalogAssets embed.FS
|
||||
|
||||
type Source struct {
|
||||
Title string `json:"title"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type Spell struct {
|
||||
Name string `json:"name"`
|
||||
Level int `json:"level"`
|
||||
Classes []string `json:"classes"`
|
||||
Aliases []string `json:"aliases"`
|
||||
}
|
||||
|
||||
type document struct {
|
||||
ID string `json:"id"`
|
||||
Ruleset string `json:"ruleset"`
|
||||
Source Source `json:"source"`
|
||||
Spells []Spell `json:"spells"`
|
||||
}
|
||||
|
||||
type Catalog struct {
|
||||
id string
|
||||
ruleset string
|
||||
source Source
|
||||
spells []Spell
|
||||
lookup map[string]int
|
||||
}
|
||||
|
||||
var (
|
||||
loadSRD5E2014Once sync.Once
|
||||
loadedSRD5E2014 Catalog
|
||||
loadSRD5E2014Err error
|
||||
)
|
||||
|
||||
// LoadSRD5E2014 returns the embedded SRD 5.1 spell catalog. Returned values do
|
||||
// not expose mutable catalog storage.
|
||||
func LoadSRD5E2014() (Catalog, error) {
|
||||
loadSRD5E2014Once.Do(func() {
|
||||
loadedSRD5E2014, loadSRD5E2014Err = load("assets/dnd_5e_2014_srd_spells.json")
|
||||
})
|
||||
return loadedSRD5E2014, loadSRD5E2014Err
|
||||
}
|
||||
|
||||
func (c Catalog) ID() string { return c.id }
|
||||
func (c Catalog) Ruleset() string { return c.ruleset }
|
||||
func (c Catalog) Source() Source { return c.source }
|
||||
|
||||
func (c Catalog) Spells() []Spell {
|
||||
return cloneSpells(c.spells)
|
||||
}
|
||||
|
||||
// Lookup matches canonical names and declared aliases after case, whitespace,
|
||||
// and common apostrophe variants are normalized.
|
||||
func (c Catalog) Lookup(name string) (Spell, bool) {
|
||||
index, ok := c.lookup[lookupKey(name)]
|
||||
if !ok {
|
||||
return Spell{}, false
|
||||
}
|
||||
return cloneSpell(c.spells[index]), true
|
||||
}
|
||||
|
||||
func load(path string) (Catalog, error) {
|
||||
raw, err := catalogAssets.ReadFile(path)
|
||||
if err != nil {
|
||||
return Catalog{}, fmt.Errorf("read spell catalog: %w", err)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.DisallowUnknownFields()
|
||||
var doc document
|
||||
if err := decoder.Decode(&doc); err != nil {
|
||||
return Catalog{}, fmt.Errorf("decode spell catalog: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
return Catalog{}, fmt.Errorf("decode spell catalog: multiple JSON values")
|
||||
}
|
||||
return newCatalog(doc)
|
||||
}
|
||||
|
||||
func newCatalog(doc document) (Catalog, error) {
|
||||
if doc.ID != SRD5E2014ID {
|
||||
return Catalog{}, fmt.Errorf("spell catalog id %q does not match %q", doc.ID, SRD5E2014ID)
|
||||
}
|
||||
if doc.Ruleset != SRD5E2014Ruleset {
|
||||
return Catalog{}, fmt.Errorf("spell catalog ruleset %q does not match %q", doc.Ruleset, SRD5E2014Ruleset)
|
||||
}
|
||||
if strings.TrimSpace(doc.Source.Title) == "" || strings.TrimSpace(doc.Source.Version) == "" ||
|
||||
strings.TrimSpace(doc.Source.URL) == "" || strings.TrimSpace(doc.Source.License) == "" {
|
||||
return Catalog{}, fmt.Errorf("spell catalog source metadata must be complete")
|
||||
}
|
||||
if len(doc.Spells) == 0 {
|
||||
return Catalog{}, fmt.Errorf("spell catalog spells must not be empty")
|
||||
}
|
||||
|
||||
allowedClasses := map[string]struct{}{
|
||||
"bard": {}, "cleric": {}, "druid": {}, "paladin": {},
|
||||
"ranger": {}, "sorcerer": {}, "warlock": {}, "wizard": {},
|
||||
}
|
||||
lookup := make(map[string]int, len(doc.Spells))
|
||||
for index := range doc.Spells {
|
||||
spell := &doc.Spells[index]
|
||||
if spell.Name != strings.TrimSpace(spell.Name) || spell.Name == "" {
|
||||
return Catalog{}, fmt.Errorf("spell[%d] name must be non-empty and trimmed", index)
|
||||
}
|
||||
if index > 0 && doc.Spells[index-1].Name >= spell.Name {
|
||||
return Catalog{}, fmt.Errorf("spell catalog names must be unique and sorted")
|
||||
}
|
||||
if spell.Level < 0 || spell.Level > 9 {
|
||||
return Catalog{}, fmt.Errorf("spell %q level must be between 0 and 9", spell.Name)
|
||||
}
|
||||
if len(spell.Classes) == 0 {
|
||||
return Catalog{}, fmt.Errorf("spell %q classes must not be empty", spell.Name)
|
||||
}
|
||||
if err := validateSortedStrings(spell.Classes, func(value string) bool {
|
||||
_, ok := allowedClasses[value]
|
||||
return ok
|
||||
}); err != nil {
|
||||
return Catalog{}, fmt.Errorf("spell %q classes: %w", spell.Name, err)
|
||||
}
|
||||
if spell.Aliases == nil {
|
||||
return Catalog{}, fmt.Errorf("spell %q aliases must be present", spell.Name)
|
||||
}
|
||||
if err := validateSortedStrings(spell.Aliases, func(value string) bool {
|
||||
return value == strings.TrimSpace(value) && value != ""
|
||||
}); err != nil {
|
||||
return Catalog{}, fmt.Errorf("spell %q aliases: %w", spell.Name, err)
|
||||
}
|
||||
for _, candidate := range append([]string{spell.Name}, spell.Aliases...) {
|
||||
key := lookupKey(candidate)
|
||||
if key == "" {
|
||||
return Catalog{}, fmt.Errorf("spell %q has an empty lookup key", spell.Name)
|
||||
}
|
||||
if previous, exists := lookup[key]; exists {
|
||||
return Catalog{}, fmt.Errorf("spell %q lookup key conflicts with %q", spell.Name, doc.Spells[previous].Name)
|
||||
}
|
||||
lookup[key] = index
|
||||
}
|
||||
}
|
||||
|
||||
return Catalog{
|
||||
id: doc.ID,
|
||||
ruleset: doc.Ruleset,
|
||||
source: doc.Source,
|
||||
spells: cloneSpells(doc.Spells),
|
||||
lookup: lookup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateSortedStrings(values []string, valid func(string) bool) error {
|
||||
if !sort.StringsAreSorted(values) {
|
||||
return fmt.Errorf("values must be sorted")
|
||||
}
|
||||
for index, value := range values {
|
||||
if !valid(value) {
|
||||
return fmt.Errorf("value %q is not supported", value)
|
||||
}
|
||||
if index > 0 && values[index-1] == value {
|
||||
return fmt.Errorf("value %q is duplicated", value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var apostropheVariants = strings.NewReplacer(
|
||||
"‘", "'",
|
||||
"’", "'",
|
||||
"ʼ", "'",
|
||||
"'", "'",
|
||||
)
|
||||
|
||||
func lookupKey(value string) string {
|
||||
value = apostropheVariants.Replace(value)
|
||||
return strings.ToLower(strings.Join(strings.Fields(value), " "))
|
||||
}
|
||||
|
||||
func cloneSpells(values []Spell) []Spell {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Spell, len(values))
|
||||
for index, value := range values {
|
||||
out[index] = cloneSpell(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneSpell(value Spell) Spell {
|
||||
value.Classes = append([]string(nil), value.Classes...)
|
||||
value.Aliases = append([]string(nil), value.Aliases...)
|
||||
return value
|
||||
}
|
||||
62
internal/modules/dnd/spells/catalog/catalog_test.go
Normal file
62
internal/modules/dnd/spells/catalog/catalog_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadSRD5E2014Catalog(t *testing.T) {
|
||||
catalog, err := LoadSRD5E2014()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSRD5E2014() error = %v", err)
|
||||
}
|
||||
if catalog.ID() != SRD5E2014ID || catalog.Ruleset() != SRD5E2014Ruleset {
|
||||
t.Fatalf("catalog identity = %q/%q", catalog.ID(), catalog.Ruleset())
|
||||
}
|
||||
if source := catalog.Source(); source.Version != "5.1" || source.License != "CC-BY-4.0" || source.URL == "" {
|
||||
t.Fatalf("catalog source = %#v", source)
|
||||
}
|
||||
spells := catalog.Spells()
|
||||
if len(spells) != 319 {
|
||||
t.Fatalf("spell count = %d, want 319", len(spells))
|
||||
}
|
||||
memberships := 0
|
||||
for _, spell := range spells {
|
||||
memberships += len(spell.Classes)
|
||||
}
|
||||
if memberships != 779 {
|
||||
t.Fatalf("class memberships = %d, want 779", memberships)
|
||||
}
|
||||
|
||||
cureWounds, ok := catalog.Lookup("cure wounds")
|
||||
if !ok || cureWounds.Name != "Cure Wounds" || cureWounds.Level != 1 ||
|
||||
!reflect.DeepEqual(cureWounds.Classes, []string{"bard", "cleric", "druid", "paladin", "ranger"}) {
|
||||
t.Fatalf("Cure Wounds lookup = %#v, present=%t", cureWounds, ok)
|
||||
}
|
||||
if huntersMark, ok := catalog.Lookup(" HUNTER'S MARK "); !ok || huntersMark.Name != "Hunter’s Mark" {
|
||||
t.Fatalf("Hunter's Mark lookup = %#v, present=%t", huntersMark, ok)
|
||||
}
|
||||
if _, ok := catalog.Lookup("Definitely Not A Spell"); ok {
|
||||
t.Fatal("unknown spell lookup succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogResultsDoNotExposeMutableStorage(t *testing.T) {
|
||||
catalog, err := LoadSRD5E2014()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
spells := catalog.Spells()
|
||||
spells[0].Name = "changed"
|
||||
spells[0].Classes[0] = "changed"
|
||||
|
||||
acidArrow, ok := catalog.Lookup("Acid Arrow")
|
||||
if !ok || acidArrow.Name != "Acid Arrow" || !reflect.DeepEqual(acidArrow.Classes, []string{"wizard"}) {
|
||||
t.Fatalf("catalog mutated through Spells(): %#v, present=%t", acidArrow, ok)
|
||||
}
|
||||
acidArrow.Classes[0] = "changed"
|
||||
again, ok := catalog.Lookup("Acid Arrow")
|
||||
if !ok || !reflect.DeepEqual(again.Classes, []string{"wizard"}) {
|
||||
t.Fatalf("catalog mutated through Lookup(): %#v, present=%t", again, ok)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user