From 49b3a3854375f93967aa2af7e07ebb950fd405a9 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Thu, 5 Oct 2023 14:26:02 -0400 Subject: [PATCH] Refactored spells import, working on level_1 --- service/.env.TEMPLATE | 4 +- service/data/layout.json | 48 + service/data/spells/cantrips.json | 53 +- service/data/spells/level_1.json | 2823 +++++++++++++++++++++++++++++ service/src/db/mod.rs | 4 +- service/src/db/spells/mod.rs | 81 +- service/src/db/spells/model.rs | 7 +- service/src/db/spells/types.rs | 162 +- service/src/main.rs | 7 +- 9 files changed, 3044 insertions(+), 145 deletions(-) create mode 100644 service/data/layout.json diff --git a/service/.env.TEMPLATE b/service/.env.TEMPLATE index 8799caa..6527927 100644 --- a/service/.env.TEMPLATE +++ b/service/.env.TEMPLATE @@ -8,6 +8,4 @@ DATABASE_PORT=5432 SERVICE_HOST=localhost SERVICE_PORT=5000 - -DISCORD_TOKEN= -OPENAI_API_KEY= \ No newline at end of file +DATA_DIR_PATH= \ No newline at end of file diff --git a/service/data/layout.json b/service/data/layout.json new file mode 100644 index 0000000..801cdc8 --- /dev/null +++ b/service/data/layout.json @@ -0,0 +1,48 @@ +{ + "name": "", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "saving_throw": [], + "attack_type": [], + "damage_inflict": [], + "damage_resist": [], + "conditions": [], + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "area": { + "type": "cube", + "size": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false, + "materials_needed": "", + "materials_cost": 0, + "materials_consumed": false + }, + "durations": [ + { + "type": "instantaneous", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ { "source": "", "page": 0 } ], + "tags": [], + "description": { + "entries": [ + + ] + } +} \ No newline at end of file diff --git a/service/data/spells/cantrips.json b/service/data/spells/cantrips.json index 39325e6..fd2ee5a 100644 --- a/service/data/spells/cantrips.json +++ b/service/data/spells/cantrips.json @@ -213,8 +213,7 @@ "entries": [ "You choose nonmagical flame that you can see within range and that fits within a 5-foot cube. You affect it in one of the following ways:", { - "type": "list", - "items": [ + "list": [ "You instantaneously expand the flame 5 feet in one direction, provided that wood or other fuel is present in the new location.", "You instantaneously extinguish the flames within the cube.", "You double or halve the area of bright light and dim light cast by the flame, change its color, or both. The change lasts for 1 hour.", @@ -253,7 +252,7 @@ { "type": "concentration", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["artificer", "druid", "sorcerer", "warlock", "wizard"], @@ -293,7 +292,7 @@ { "type": "concentration", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["artificer", "bard", "sorcerer", "wizard"], @@ -341,8 +340,7 @@ "entries": [ "Whispering to the spirits of nature, you create one of the following effects within range:", { - "type": "list", - "items": [ + "list": [ "You create a tiny, harmless sensory effect that predicts what the weather will be at your location for the next 24 hours. The effect might manifest as a golden orb for clear skies, a cloud for rain, falling snowflakes for snow, and so on. This effect persists for 1 round.", "You instantly make a flower blossom, a seed pod open, or a leaf bud bloom.", "You create an instantaneous, harmless sensory effect, such as falling leaves, a puff of wind, the sound of a small animal, or the faint odor of skunk. The effect must fit in a 5-foot cube.", @@ -490,7 +488,7 @@ { "type": "concentration", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["bard", "sorcerer", "warlock", "wizard"], @@ -611,7 +609,7 @@ { "type": "concentration", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["artificer", "cleric", "druid"], @@ -658,8 +656,7 @@ "entries": [ "You seize the air and compel it to create one of the following effects at a point you can see within range:", { - "type": "list", - "items": [ + "list": [ "One Medium or smaller creature that you choose must succeed on a Strength saving throw or be pushed up to 5 feet away from you.", "You create a small blast of air capable of moving one object that is neither held nor carried and that weighs no more than 5 pounds. The object is pushed up to 10 feet away from you. It isn't pushed with enough force to cause damage.", "You create a harmless sensory effect using air, such as causing leaves to rustle, wind to slam shutters shut, or your clothing to ripple in a breeze." @@ -820,7 +817,7 @@ { "type": "timed", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["artificer", "bard", "sorcerer", "warlock", "wizard"], @@ -861,7 +858,7 @@ { "type": "timed", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["artificer", "druid", "warlock"], @@ -882,7 +879,7 @@ "ritual": false, "casting_time": { "value": 1, - "unit": "minutes" + "unit": "minute" }, "range": { "type": "touch" @@ -1014,7 +1011,7 @@ { "type": "timed", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["bard", "sorcerer", "warlock", "wizard"], @@ -1073,8 +1070,7 @@ "entries": [ "You choose a portion of dirt or stone that you can see within range and that fits within a 5-foot cube. You manipulate it in one of the following ways:", { - "type": "list", - "items": [ + "list": [ "If you target an area of loose earth, you can instantaneously excavate it, move it along the ground, and deposit it up to 5 feet away. This movement doesn't have enough force to cause damage.", "You cause shapes, colors, or both to appear on the dirt or stone, spelling out words, creating images, or shaping patterns. The changes last for 1 hour.", "If the dirt or stone you target is on the ground, you cause it to become difficult terrain. Alternatively, you can cause the ground to become normal terrain if it is already difficult terrain. This change lasts for 1 hour." @@ -1162,8 +1158,7 @@ "entries": [ "This spell is a minor magical trick that novice spellcasters use for practice. You create one of the following magical effects within range:", { - "type": "list", - "items": [ + "list": [ "You create an instantaneous, harmless sensory effect, such as a shower of sparks, a puff of wind, faint musical notes, or an odd odor.", "You instantaneously light or snuff out a candle, a torch, or a small campfire.", "You instantaneously clean or soil an object no larger than 1 cubic foot.", @@ -1402,7 +1397,7 @@ "type": "instantaneous" } ], - "classes": [], + "classes": ["wizard"], "sources": [ { "source": "EGW", "page": 189 } ], @@ -1455,8 +1450,7 @@ "entries": [ "You choose an area of water that you can see within range and that fits within a 5-foot cube. You manipulate it in one of the following ways:", { - "type": "list", - "items": [ + "list": [ "You instantaneously move or otherwise change the flow of the water as you direct, up to 5 feet in any direction. This movement doesn't have enough force to cause damage.", "You cause the water to form into simple shapes and animate at your direction. This change lasts for 1 hour.", "You change the water's color or opacity. The water must be changed in the same way throughout. This change lasts for 1 hour.", @@ -1493,7 +1487,7 @@ { "type": "timed", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["druid"], @@ -1642,7 +1636,7 @@ { "type": "timed", "value": 1, - "unit": "minutes" + "unit": "minute" } ], "classes": ["cleric"], @@ -1654,17 +1648,16 @@ "entries": [ "You manifest a minor wonder, a sign of supernatural power, within range. You create one of the following magical effects within range:", { - "type": "list", - "items": [ - "Your voice booms up to three times as loud as normal for 1 minutes.", - "You cause flames to flicker, brighten, dim, or change color for 1 minutes.", - "You cause harmless tremors in the ground for 1 minutes.", + "list": [ + "Your voice booms up to three times as loud as normal for 1 minute.", + "You cause flames to flicker, brighten, dim, or change color for 1 minute.", + "You cause harmless tremors in the ground for 1 minute.", "You create an instantaneous sound that originates from a point of your choice within range, such as a rumble of thunder, the cry of a raven, or ominous whispers.", "You instantaneously cause an unlocked door or window to fly open or slam shut.", - "You alter the appearance of your eyes for 1 minutes." + "You alter the appearance of your eyes for 1 minute." ] }, - "If you cast this spell multiple times, you can have up to three of its 1-minutes effects active at a time, and you can dismiss such an effect as an action." + "If you cast this spell multiple times, you can have up to three of its 1-minute effects active at a time, and you can dismiss such an effect as an action." ] } }, diff --git a/service/data/spells/level_1.json b/service/data/spells/level_1.json index e69de29..00fa213 100644 --- a/service/data/spells/level_1.json +++ b/service/data/spells/level_1.json @@ -0,0 +1,2823 @@ +[ + { + "name": "Absorb Elements", + "school": "abjuration", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "reaction", + "note": "which you take when you take acid, cold, fire, lightning, or thunder damage" + }, + "damage_inflict": ["acid"], + "range": { + "type": "self" + }, + "components": { + "verbal": false, + "somatic": true, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "round" + } + ], + "classes": ["artificer", "druid", "ranger", "sorcerer", "wizard"], + "sources": [ + { "source": "EEPC", "page": 150 } + ], + "description": { + "entries": [ + "The spell captures some of the incoming energy, lessening its effect on you and storing it for your next melee attack. You have resistance to the triggering damage type until the start of your next turn. Also, the first time you hit with a melee attack on your next turn, the target takes an extra {@damage 1d6} damage of the triggering type, and the spell ends.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, the extra damage increases by {@scaledamage 1d6|1-9|1d6} for each slot level above 1st." + ] + } + }, + { + "name": "Alarm", + "school": "abjuration", + "level": 1, + "ritual": true, + "casting_time": { + "value": 1, + "unit": "minute" + }, + "range": { + "type": "point", + "value": 30, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "a tiny bell and a piece of fine silver wire" + }, + "durations": [ + { + "type": "timed", + "value": 8, + "unit": "hours" + } + ], + "classes": ["artificer", "{@subclass paladin|Oath of the Watchers}", "ranger", "wizard"], + "sources": [ + { "source": "PHB", "page": 211 }, + { "source": "XGE", "page": 155 } + ], + "description": { + "entries": [ + "You set an alarm against unwanted intrusion. Choose a door, a window, or an area within range that is no larger than a {@filter 20-foot cube|spells|cube|phb|p. 204}. Until the spell ends, an alarm alerts you whenever a Tiny or larger creature touches or enters the warded area. When you cast the spell, you can designate creatures that won't set off the alarm. You also choose whether the alarm is mental or audible.", + "A mental alarm alerts you with a ping in your mind if you are within 1 mile of the warded area. This ping awakens you if you are {@condition sleeping}.", + "An audible alarm produces the sound of a hand bell for 10 seconds within 60 feet." + ] + } + }, + { + "name": "Animal Friendship", + "school": "enchantment", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "conditions": ["charmed"], + "range": { + "type": "point", + "value": 30, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "a morsel of food" + }, + "durations": [ + { + "type": "timed", + "value": 24, + "unit": "hours" + } + ], + "classes": ["bard", "druid", "ranger", "{@subclass cleric|Nature Domain}"], + "sources": [ + { "source": "PHB", "page": 212 } + ], + "description": { + "entries": [ + "This spell lets you convince a beast that you mean it no harm. Choose a beast that you can see within range. It must see and hear you. If the beast's Intelligence is 4 or higher, the spell fails. Otherwise, the beast must succeed on a Wisdom saving throw or be {@condition charmed} by you for the spell's duration. If you or one of your companions harms the target, the spell ends.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, you can affect one additional beast for each slot level above 1st." + ] + } + }, + { + "name": "Armor of Agathys", + "school": "abjuration", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "damage_inflict": ["cold"], + "range": { + "type": "self" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "a cup of water" + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "hour" + } + ], + "classes": ["warlock", "{@subclass paladin|Oath of Conquest}"], + "sources": [ + { "source": "PHB", "page": 215 } + ], + "description": { + "entries": [ + "A protective magical force surrounds you, manifesting as a spectral frost that covers you and your gear. You gain {@dice 5} temporary hit points for the duration. If a creature hits you with a melee attack while you have these hit points, the creature takes {@damage 5} cold damage.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, both the temporary hit points and the cold damage increase by {@scaledamage 5|1-9|5} for each slot level above 1st." + ] + } + }, + { + "name": "Arms of Hadar", + "school": "conjuration", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "saving_throw": ["strength"], + "damage_inflict": ["necrotic"], + "range": { + "type": "self" + }, + "area": { + "type": "sphere", + "size": 10, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": false + }, + "durations": [ + { + "type": "instantaneous" + } + ], + "classes": ["warlock"], + "sources": [ + { "source": "PHB", "page": 215 } + ], + "description": { + "entries": [ + "You invoke the power of Hadar, the Dark Hunger. Tendrils of dark energy erupt from you and batter all creatures within 10 feet of you. Each creature in that area must make a Strength saving throw. On a failed save, a target takes {@damage 2d6} necrotic damage and can't take reactions until its next turn. On a successful save, the creature takes half damage, but suffers no other effect.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, the damage increases by {@scaledamage 1d6|1-9|1d6} for each slot level above 1st." + ] + } + }, + { + "name": "Bane", + "school": "enchantment", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "saving_throw": ["charisma"], + "range": { + "type": "point", + "value": 30, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "a drop of blood" + }, + "durations": [ + { + "type": "concentration", + "value": 1, + "unit": "minute" + } + ], + "classes": ["bard", "cleric", "{@subclass paladin|Oath of Vengeance}", "{@subclass warlock|The Undead}"], + "sources": [ + { "source": "PHB", "page": 216 } + ], + "description": { + "entries": [ + "Up to three creatures of your choice that you can see within range must make Charisma saving throws. Whenever a target that fails this saving throw makes an attack roll or a saving throw before the spell ends, the target must roll a {@dice d4} and subtract the number rolled from the attack roll or saving throw.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st." + ] + } + }, + { + "name": "Beast Bond", + "school": "divination", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "touch" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "a bit of fur wrapped in a cloth" + }, + "durations": [ + { + "type": "concentration", + "value": 10, + "unit": "minutes" + } + ], + "classes": ["druid", "ranger"], + "sources": [ + { "source": "EEPC", "page": 150 } + ], + "description": { + "entries": [ + "You establish a telepathic link with one beast you touch that is friendly to you or {@condition charmed} by you. The spell fails if the beast's Intelligence is 4 or higher. Until the spell ends, the link is active while you and the beast are within line of sight of each other. Through the link, the beast can understand your telepathic messages to it, and it can telepathically communicate simple emotions and concepts back to you. While the link is active, the beast gains advantage on attack rolls against any creature within 5 feet of you that you can see." + ] + } + }, + { + "name": "Bless", + "school": "enchantment", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 30, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "a sprinkling of holy water" + }, + "durations": [ + { + "type": "concentration", + "value": 1, + "unit": "minute" + } + ], + "classes": ["cleric", "paladin"], + "sources": [ + { "source": "PHB", "page": 219 } + ], + "description": { + "entries": [ + "You bless up to three creatures of your choice within range. Whenever a target makes an attack roll or a saving throw before the spell ends, the target can roll a {@dice d4} and add the number rolled to the attack roll or saving throw.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st." + ] + } + }, + { + "name": "Burning Hands", + "school": "evocation", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "saving_throw": ["dexterity"], + "damage_inflict": ["fire"], + "range": { + "type": "self" + }, + "area": { + "type": "cone", + "size": 15, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": false + }, + "durations": [ + { + "type": "instantaneous" + } + ], + "classes": ["{@subclass cleric|Light Domain}", "{@subclass druid|Circle of Wildfire}", "sorcerer", "{@subclass warlock|The Fiend}", "{@subclass warlock|The Genie}", "wizard"], + "sources": [ + { "source": "PHB", "page": 220 } + ], + "description": { + "entries": [ + "As you hold your hands with thumbs touching and fingers spread, a thin sheet of flames shoots forth from your outstretched fingertips. Each creature in a {@filter 15-foot cone|spells|cone|phb|p. 204} must make a Dexterity saving throw. A creature takes {@damage 3d6} fire damage on a failed save, or half as much damage on a successful one.", + "The fire ignites any flammable objects in the area that aren't being worn or carried.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, the damage increases by {@scaledamage 1d6|1-9|1d6} for each slot level above 1st." + ] + } + }, + { + "name": "Catapult", + "school": "transmutation", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 60, + "unit": "feet" + }, + "saving_throw": ["dexterity"], + "damage_inflict": ["bludgeoning"], + "components": { + "verbal": false, + "somatic": true, + "material": false + }, + "durations": [ + { + "type": "instantaneous" + } + ], + "classes": ["artificer", "sorcerer", "wizard"], + "sources": [ + { "source": "EEPC", "page": 150 } + ], + "description": { + "entries": [ + "Choose one object weighing 1 to 5 pounds within range that isn't being worn or carried. The object flies in a straight line up to 90 feet in a direction you choose before falling to the ground, stopping early if it impacts against a solid surface. If the object would strike a creature, that creature must make a Dexterity saving throw. On a failed save, the object strikes the target and stops moving. When the object strikes something, the object and what it strikes each take {@damage 3d8} bludgeoning damage.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, the maximum weight of objects that you can target with this spell increases by 5 pounds, and the damage increases by {@scaledamage 1d8|1-9|1d8}, for each slot level above 1st." + ] + } + }, + { + "name": "Cause Fear", + "school": "necromancy", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "saving_throw": ["wisdom"], + "conditions": ["frightened"], + "range": { + "type": "point", + "value": 60, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "concentration", + "value": 1, + "unit": "minute" + } + ], + "classes": ["warlock", "wizard"], + "sources": [ + { "source": "XGE", "page": 151 } + ], + "description": { + "entries": [ + "You awaken the sense of mortality in one creature you can see within range. A construct or an undead is immune to this effect. The target must succeed on a Wisdom saving throw or become {@condition frightened} of you until the spell ends. The {@condition frightened} target can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, you can target one additional creature for each slot level above 1st. The creatures must be within 30 feet of each other when you target them." + ] + } + }, + { + "name": "Ceremony", + "school": "abjuration", + "level": 1, + "ritual": true, + "casting_time": { + "value": 1, + "unit": "hour" + }, + "range": { + "type": "touch" + }, + "components": { + "verbal": true, + "somatic": true, + "material": true, + "materials_needed": "25 gp worth of powdered silver, which the spell consumes", + "materials_consumed": true, + "materials_cost": 2500 + }, + "durations": [ + { + "type": "instantaneous" + } + ], + "classes": ["cleric", "paladin"], + "sources": [ + { "source": "XGE", "page": 151 } + ], + "description": { + "entries": [ + "You perform a special religious ceremony that is infused with magic. When you cast the spell, choose one of the following rites, the target of which must be within 10 feet of you throughout the casting.", + "{@bold Atonement.} You touch one willing creature whose alignment has changed, and you make a DC 20 Wisdom ({@skill Insight}) check. On a successful check, you restore the target to its original alignment.", + "{@bold Bless Water.} You touch one vial of water and cause it to become holy water.", + "{@bold Coming of Age.} You touch one humanoid who is a young adult. For the next 24 hours, whenever the target makes an ability check, it can roll a d4 and add the number rolled to the ability check. A creature can benefit from this rite only once.", + "{@bold Dedication.} You touch one humanoid who wishes to be dedicated to your god's service. For the next 24 hours, whenever the target makes a saving throw, it can roll a d4 and add the number rolled to the save. A creature can benefit from this rite only once.", + "{@bold Funeral Rite.} You touch one corpse, and for the next 7 days, the target can't become undead by any means short of a wish spell.", + "{@bold Wedding.} You touch adult humanoids willing to be bonded together in marriage. For the next 7 days, each target gains a +2 bonus to AC while they are within 30 feet of each other. A creature can benefit from this rite again only if widowed." + ] + } + }, + { + "name": "Chaos Bolt", + "school": "evocation", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "attack_type": "ranged", + "damage_inflict": ["acid", "cold", "fire", "force", "lightning", "poison", "psychic", "thunder"], + "range": { + "type": "point", + "value": 120, + "unit": "feet" + }, + "components": { + "verbal": true, + "somatic": true, + "material": false + }, + "durations": [ + { + "type": "instantaneous" + } + ], + "classes": ["sorcerer"], + "sources": [ + { "source": "XGE", "page": 151 } + ], + "description": { + "entries": [ + "You hurl an undulating, warbling mass of chaotic energy at one creature in range. Make a ranged spell attack against the target. On a hit, the target takes {@damage 2d8} + {@damage 1d6} damage. Choose one of the d8s. The number rolled on that die determines the attack's damage type, as shown below.", + { + "table": { + "headers": ["d8", "Damage Type"], + "rows": [ + ["1", "Acid"], + ["2", "Cold"], + ["3", "Fire"], + ["4", "Force"], + ["5", "Lightning"], + ["6", "Poison"], + ["7", "Psychic"], + ["8", "Thunder"] + ] + } + }, + "If you roll the same number on both d8s, the chaotic energy leaps from the target to a different creature of your choice within 30 feet of it. Make a new attack roll against the new target, and make a new damage roll, which could cause the chaotic energy to leap again.", + "A creature can be targeted only once by each casting of this spell.", + "{@bold At Higher Levels.} When you cast this spell using a spell slot of 2nd level or higher, each target takes {@scaledamage 1d6|1-9|1d6} extra damage of the type rolled for each slot level above 1st." + ] + } + }, + { + "name": "Charm Person", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Chromatic Orb", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Color Spray", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Command", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Compelled Duel", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Comprehend Languages", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Create or Destroy Water", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Cure Wounds", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Detect Evil and Good", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Detect Magic", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Detect Poison and Disease", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Disguise Self", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Dissonant Whispers", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Divine Favor", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Earth Tremor", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Ensnaring Strike", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Entangle", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Expeditious Retreat", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Faerie Fire", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "False Life", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Feather Fall", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Find Familiar", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Fog Cloud", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Frost Fingers", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Gift of Alacrity", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Goodberry", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Grease", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Guiding Bolt", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Hail of Thorns", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Healing Word", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Hellish Rebuke", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Heroism", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Hex", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Hunter’s Mark", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Ice Knife", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Identify", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Illusory Script", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Inflict Wounds", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Jump", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Longstrider", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Mage Armor", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Magic Missile", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Magnify Gravity", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Protection from Evil and Good", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Purify Food and Drink", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Ray of Sickness", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Sanctuary", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Searing Smite", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Shield", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Shield of Faith", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Silent Image", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Silvery Barbs", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Sleep", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Snare", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Speak with Animals", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Tasha’s Caustic Brew", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Tasha’s Hideous Laughter", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Tenser’s Floating Disk", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Thunderous Smite", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Thunderwave", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Unseen Servant", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Witch Bolt", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Wrathful Smite", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + }, + { + "name": "Zephyr Strike", + "school": "", + "level": 1, + "ritual": false, + "casting_time": { + "value": 1, + "unit": "action" + }, + "range": { + "type": "point", + "value": 1, + "unit": "feet" + }, + "components": { + "verbal": false, + "somatic": false, + "material": false + }, + "durations": [ + { + "type": "timed", + "value": 1, + "unit": "minute" + } + ], + "classes": [], + "sources": [ + { "source": "", "page": 0 } + ], + "description": { + "entries": [ + + ] + } + } +] \ No newline at end of file diff --git a/service/src/db/mod.rs b/service/src/db/mod.rs index 8325a8c..4b69032 100644 --- a/service/src/db/mod.rs +++ b/service/src/db/mod.rs @@ -51,6 +51,6 @@ pub fn connection() -> Result { .map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e))) } -pub fn load_data() { - spells::load_data(); +pub fn load_data(data_dir_path: &str) { + spells::load_data(data_dir_path); } diff --git a/service/src/db/spells/mod.rs b/service/src/db/spells/mod.rs index 27ba8cf..4ab082a 100644 --- a/service/src/db/spells/mod.rs +++ b/service/src/db/spells/mod.rs @@ -2,46 +2,57 @@ mod model; mod routes; mod types; +use std::{fs::{metadata, File, read_dir}, path::Path, io::BufReader}; + +use log::{warn, trace}; pub use model::*; pub use types::*; pub use routes::init_routes; -pub fn load_data() { - let root_path = std::env::current_dir().unwrap(); - let files = [ - "cantrips.json", "level_1.json", "level_2.json", "level_3.json", "level_4.json", "level_5.json", "level_6.json", "level_7.json", "level_8.json", "level_9.json" - ]; - let mut spells: Vec = vec![]; - for file in files { - let mut data_path = std::path::PathBuf::from(&root_path); - data_path.push(format!("data/spells/{}", file)); - let path = data_path.to_str().unwrap(); - match std::fs::read_to_string(path) { - Ok(data) => { - log::debug!("Loading spells from {}", path); - match serde_json::from_str::(&data) { - Ok(json) => { - match serde_json::from_value::>(json) { - Ok(mut new_spells) => spells.append(&mut new_spells), - Err(err) => log::error!("Failed to parse spells data: {}", err) +pub fn load_data(data_dir_path: &str) { + if Path::new(data_dir_path).exists() { + let meta = metadata(data_dir_path).unwrap(); + if meta.is_dir() { + let spells_dir_path = format!("{}/spells", data_dir_path); + if Path::new(&spells_dir_path).exists() { + let meta = metadata(&spells_dir_path).unwrap(); + if meta.is_dir() { + for entry in read_dir(&spells_dir_path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + let result: Result, serde_json::Error> = serde_json::from_reader(reader); + match result { + Ok(spells) => { + for spell in spells { + let mut filters = QueryFilters::default(); + filters.by_name = Some(spell.name.clone()); + match QuerySpell::get_all(&filters, 100, 1) { + Ok(spells) => { + if spells.len() > 0 { + trace!("Spell '{}' already exists", spell.name); + continue; + } + }, + Err(err) => { + warn!("Error checking if spell '{}' exists: {}", spell.name, err); + continue; + } + }; + let spell = InsertSpell::insert(spell.into()).unwrap(); + trace!("Inserted spell: {}", spell.name); + } + }, + Err(err) => warn!("Error reading spells from file: {}", err) + }; } - }, - Err(err) => log::error!("Failed to parse spells data to value: {}", err) - }; - }, - Err(err) => log::error!("Failed to read from {}: {}", file, err) - }; - } - let count = QuerySpell::get_count(&QueryFilters::default()).unwrap(); - if count >= spells.len() as i64 { - log::warn!("Spell data is already loaded"); - return; - } - for spell in spells { - let spell_name = spell.name.clone(); - match InsertSpell::insert(spell.into()) { - Ok(_) => {}, - Err(err) => log::error!("Failed to insert '{}' spell: {}", spell_name, err) + } + } + } } + } else { + warn!("Data path '{}' does not exist, no data imported", data_dir_path); } } diff --git a/service/src/db/spells/model.rs b/service/src/db/spells/model.rs index f1317ff..027aaf6 100644 --- a/service/src/db/spells/model.rs +++ b/service/src/db/spells/model.rs @@ -4,7 +4,7 @@ use siren::ServiceError; use crate::db::{schema::spells::{self}, classes::AbilityType, conditions::ConditionType}; -use super::{SchoolType, CastingTime, CastingType, SpellAttackType, SpellDamageType, Range, Area, Components, Duration, Source, Description, DurationType}; +use super::{SchoolType, CastingTime, SpellAttackType, SpellDamageType, Range, Area, Components, Duration, Source, Description, DurationType, Effect}; #[derive(Queryable, QueryableByName, Serialize, Deserialize)] #[diesel(table_name = spells)] @@ -198,6 +198,8 @@ pub struct Spell { pub ritual: bool, pub casting_time: CastingTime, #[serde(skip_serializing_if = "Option::is_none")] + pub effect: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub saving_throw: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub attack_type: Option, @@ -232,7 +234,8 @@ impl From for Spell { school: SchoolType::Abjuration, level: 0, ritual: false, - casting_time: CastingTime { value: 0, casting_type: CastingType::Action }, + casting_time: CastingTime { value: 0, casting_type: "".to_string(), note: None }, + effect: None, saving_throw: None, attack_type: None, damage_inflict: None, diff --git a/service/src/db/spells/types.rs b/service/src/db/spells/types.rs index f82c4c3..eaeb94e 100644 --- a/service/src/db/spells/types.rs +++ b/service/src/db/spells/types.rs @@ -58,50 +58,10 @@ impl FromStr for SchoolType { pub struct CastingTime { pub value: i32, #[serde(rename = "unit")] - pub casting_type: CastingType + pub casting_type: String, + pub note: Option } -#[derive(Debug, Serialize, Deserialize)] -pub enum CastingType { - #[serde(rename = "action")] - Action, - #[serde(rename = "bonus")] - BonusAction, - #[serde(rename = "reaction")] - Reaction, - #[serde(rename = "minutes")] - Minutes, - #[serde(rename = "hours")] - Hours -} - -// impl CastingType { -// pub fn to_string(&self) -> String { -// match self { -// CastingType::Action => "action".to_string(), -// CastingType::BonusAction => "bonus".to_string(), -// CastingType::Reaction => "reaction".to_string(), -// CastingType::Minutes => "minutes".to_string(), -// CastingType::Hours => "hours".to_string() -// } -// } -// } - -// impl FromStr for CastingType { -// type Err = (); - -// fn from_str(s: &str) -> Result { -// match s { -// "action" => Ok(CastingType::Action), -// "bonus" => Ok(CastingType::BonusAction), -// "reaction" => Ok(CastingType::Reaction), -// "minutes" => Ok(CastingType::Minutes), -// "hours" => Ok(CastingType::Hours), -// _ => Err(()) -// } -// } -// } - #[derive(Debug, Serialize, Deserialize)] pub enum SpellAttackType { #[serde(rename = "melee")] @@ -303,8 +263,15 @@ pub struct Description { #[derive(Debug)] pub struct Entry { - pub entry_type: String, - pub items: Vec + pub text: Option>, + pub list: Option>, + pub table: Option +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EntryTable { + pub headers: Vec, + pub rows: Vec> } impl<'de> Deserialize<'de> for Entry { @@ -312,36 +279,82 @@ impl<'de> Deserialize<'de> for Entry { let value = serde_json::Value::deserialize(deserializer)?; match value { serde_json::Value::String(s) => Ok(Entry { - entry_type: "string".to_string(), - items: vec![s] + text: Some(vec![s]), + list: None, + table: None, }), serde_json::Value::Object(o) => { - let entry_type = match o.get("type") { - Some(t) => match t.as_str() { - Some(s) => s.to_string(), - None => return Err(serde::de::Error::custom("Invalid entry type")) - }, - None => return Err(serde::de::Error::custom("Missing entry type")) - }; - let items = match o.get("items") { + let list = match o.get("list") { Some(i) => match i.as_array() { Some(a) => { - let mut items = Vec::new(); + let mut list = Vec::new(); for item in a { match item.as_str() { - Some(s) => items.push(s.to_string()), - None => return Err(serde::de::Error::custom("Invalid entry item")) + Some(s) => list.push(s.to_string()), + None => return Err(serde::de::Error::custom("Invalid entry list item")) } } - items + Some(list) }, - None => return Err(serde::de::Error::custom("Invalid entry items")) + None => return Err(serde::de::Error::custom("Invalid entry list items")) }, - None => return Err(serde::de::Error::custom("Missing entry items")) + None => None + }; + let table = match o.get("table") { + Some(t) => match t.as_object() { + Some(o) => { + let mut headers = Vec::new(); + let mut rows = Vec::new(); + match o.get("headers") { + Some(c) => match c.as_array() { + Some(a) => { + for item in a { + match item.as_str() { + Some(s) => headers.push(s.to_string()), + None => return Err(serde::de::Error::custom("Invalid entry table header")) + } + } + }, + None => return Err(serde::de::Error::custom("Invalid entry table headers")) + }, + None => return Err(serde::de::Error::custom("Missing entry table headers")) + }; + match o.get("rows") { + Some(r) => match r.as_array() { + Some(a) => { + for row in a { + match row.as_array() { + Some(a) => { + let mut row = Vec::new(); + for item in a { + match item.as_str() { + Some(s) => row.push(s.to_string()), + None => return Err(serde::de::Error::custom("Invalid entry table row item")) + } + } + rows.push(row); + }, + None => return Err(serde::de::Error::custom("Invalid entry table row")) + } + } + }, + None => return Err(serde::de::Error::custom("Invalid entry table rows")) + }, + None => return Err(serde::de::Error::custom("Missing entry table rows")) + }; + Some(EntryTable { + headers, + rows + }) + }, + None => return Err(serde::de::Error::custom("Invalid entry table")) + }, + None => None }; Ok(Entry { - entry_type, - items + text: None, + list, + table }) }, _ => Err(serde::de::Error::custom("Invalid entry")) @@ -351,15 +364,17 @@ impl<'de> Deserialize<'de> for Entry { impl Serialize for Entry { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer { - match self.entry_type.as_str() { - "string" => serializer.serialize_str(&self.items[0]), - _ => { - let mut map = serializer.serialize_map(Some(2))?; - map.serialize_entry("type", &self.entry_type)?; - map.serialize_entry("items", &self.items)?; - map.end() - } + let mut map = serializer.serialize_map(Some(1))?; + if let Some(text) = &self.text { + map.serialize_entry("text", text)?; } + if let Some(list) = &self.list { + map.serialize_entry("list", list)?; + } + if let Some(table) = &self.table { + map.serialize_entry("table", table)?; + } + map.end() } } @@ -374,4 +389,9 @@ pub struct Components { pub materials_cost: Option, #[serde(skip_serializing_if = "Option::is_none")] pub materials_consumed: Option -} \ No newline at end of file +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Effect { + pub effect_type: Option +} diff --git a/service/src/main.rs b/service/src/main.rs index bae3589..53e0402 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -8,7 +8,7 @@ use actix_cors::Cors; use actix_web::{HttpServer, App}; use dotenv::dotenv; -use log::{error, info}; +use log::{error, info, warn}; mod db; @@ -17,7 +17,10 @@ async fn main() -> std::io::Result<()> { dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info")); db::init(); - db::load_data(); + match env::var("DATA_DIR_PATH") { + Ok(data_dir_path) => db::load_data(&data_dir_path), + Err(err) => warn!("Unable to load initial database data: {}", err) + }; let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string());