Python API to run queries on and format data from XML data for D&D 5th ed.
XML data is from FightClub5eXML, installed by legendlore
as a submodule.
Currently, only the spells and monsters aspects of the database are properly handled.
Note that some documentation in this README file may be out-of-date and inaccurate.
In addition to the primary function of legendlore
—providing information in a REPL session, detailed below—it is also possible to access some functionality via the vim-legendlore
pack. See that pack's README for details.
legendlore
is particularly useful in a powerful REPL like iPython. To setup convenient access, it can be useful to issue the following command in the REPL:
>>> from legendlore.repltools import *
One of the most common lookups is made convenient via the repl.sp
function, which provides convenient access to spell information in the author's preferred format:
In [176]: sp('eldritch blast')
* Eldritch Blast A/120'/I (0:Wl)
" A beam of crackling energy streaks toward a creature within range. Make a ranged spell attack against the target. On a hit, the target takes 1d10 force damage.
" The spell creates more than one beam when you reach higher levels: two beams at 5th level, three beams at 11th level, and four beams at 17th level. you can direct the beams at the same target or at different ones. Make a separate attack roll for each beam.
More specific information can be gleaned using the API. A basic query can give us a list of all level 9 spells in the game:
>>> s.where(level=9).print()
Astral Projection 1h/10'/S [V/S/M@!1100!gp] (9:C+Wl+Wz)
Blade of Disaster B/60'/C<=1m [V/S] (9:S+Wl+Wz)
Foresight 1m/T/8h [V/S/M] (9:Bd+D+Wl+Wz)
Gate A/60'/C<=1m [V/S/M@5000gp] (9:C+S+Wz)
Imprisonment 1m/30'/UD [V/S/M@500gp] (9:Wl+Wz)
Invulnerability A/S/C<=10m [V/S/M@!500!gp] (9:Wz)
Mass Heal A/60'/I [V/S] (9:C)
Mass Polymorph A/120'/C<=1h [V/S/M] (9:Bd+S+Wz)
Meteor Swarm A/1mi/I [V/S] (9:S+Wz)
Power Word Heal A/T/I [V/S] (9:Bd)
Power Word Kill A/60'/I [V] (9:Bd+S+Wl+Wz)
Prismatic Wall A/60'/10m [V/S] (9:Wz)
Psychic Scream A/90'/I [S] (9:Bd+S+Wl+Wz)
Ravenous Void A/1000'/C<=1m [V/S/M] (9:WzG)
Shapechange A/S/C<=1h [V/S/M@1500gp] (9:D+Wz)
Storm of Vengeance A/Sight/C<=1m [V/S] (9:D)
Time Ravage A/90'/I [V/S/M@!5000!gp] (9:WzC)
Time Stop A/S/I [V] (9:S+Wz)
True Polymorph A/30'/C<=1h [V/S/M] (9:Bd+Wl+Wz)
True Resurrection 1h/T/I [V/S/M@!25000!gp] (9:C+D)
Weird A/120'/C<=1m [V/S] (9:Wz)
Wish A/S/I [V] (9:S+WlGe+Wz)
The s
object is imported from legendlore.repl
; it's a list-like object (with some added methods) that contains all of the spells in the game. It's an instance of the legendlore.Spells
class.
It is also possible to call collection objects directly. This is equivalent to calling their where
method. To list all level 4 Bard spells, sorted by level:
>>> s(level=4, classes=p.contains('Bard')).print()
Charm Monster A/30'/1h [V/S] (4:Bd+D+S+Wl+Wz)
Compulsion A/30'/C<=1m [V/S] (4:Bd+CO+PG)
Confusion A/90'/C<=1m [V/S/M] (4:Bd+CK+D+DS+PO+S+Wz)
Dimension Door A/500'/I [V] (4:Bd+CTr+PV+RaFW+S+Wl+Wz)
Freedom of Movement A/T/1h [V/S/M] (4:A+Bd+C+CW+D+DL+PD+PG+Ra+SCS)
Greater Invisibility A/T/C<=1m [V/S] (4:AArm+Bd+CTw+DL+RaGS+S+WlA+WlGe+Wz)
Hallucinatory Terrain 10m/300'/24h [V/S/M] (4:Bd+D+DL+Wl+Wz)
Locate Creature A/S/C<=1h [V/S/M] (4:Bd+C+CO+D+DL+P+Ra+Wz)
Phantasmal Killer A/120'/C<=1m [V/S] (4:Bd+WlGe+WlH+Wz)
Polymorph A/60'/C<=1h [V/S/M] (4:Bd+CTr+D+S+Wz)
This last example uses predicates.
Predicates are generated by calling one of the factory functions in legendlore.predicates
.
The closure returned by a predicate factory is a function of the form f(attr, obj)
.
When the closure is called, it compares the value supplied to the factory with the 'attr' attribute of the object 'obj'.
If the object does not have that attribute, or if the comparison is false, a False
value is returned.
Otherwise, a the closure returns True
.
The Spells.where
method runs this comparison on all the spells in that collection,
and returns a list of the ones for which the predicate evaluated to True
.
In the previous example, the contains
predicate checks whether 'Bard'
is one of the items in the list of strings
stored in the 'classes'
attribute of obj
.
All you need to know to use predicates is that they are function calls
passed as the value of keyword arguments to the where
methods
of Spells
and Monsters
collections.
Here we have another example using predicates. This code gives us a printout of all Ranger spells of 2nd or 3rd level:
>>> s.where( classes=p.contains('Ranger'),
... level=p.and_(p.gte(2), p.lte(3)) ).sorted('level').print()
Aid A/30'/8h [V/S/M] (2:A+Bd+C+CPe+P+Ra+SCS)
Animal Messenger (rit.) A/30'/24h [V/S/M] (2:Bd+D+Ra)
Barkskin A/T/C<=1h [V/S/M] (2:CN+D+DL+Ra)
Beast Sense (rit.) A/T/C<=1h [S] (2:BbTW+D+Ra)
Cordon of Arrows A/5'/8h [V/S/M] (2:Ra)
Darkvision A/T/8h [V/S/M] (2:A+D+Ra+S+Wz)
Enhance Ability A/T/C<=1h [V/S/M] (2:A+Bd+C+D+PG+Ra+S+Wz)
Find Traps A/120'/I [V/S] (2:C+D+Ra)
Gust of Wind A/S(60'line)/C<=1m [V/S/M] (2:CTm+D+Ra+S+WlFa+WlGe+Wz)
Healing Spirit B/60'/C<=1m [V/S] (2:D+Ra)
Lesser Restoration A/T/I [V/S] (2:A+Bd+C+CLf+D+P+PD+Ra+SCS+WlC)
Locate Animals or Plants (rit.) A/S/I [V/S/M] (2:Bd+D+Ra)
Locate Object A/S/C<=10m [V/S/M] (2:Bd+C+D+P+Ra+Wz)
Magic Weapon B/T/C<=1h [V/S] (2:A+CA+CF+CW+P+PG+Ra+S+Wz)
Pass without Trace A/S/C<=1h [V/S/M] (2:CTr+D+DL+Ra)
Protection from Poison A/T/1h [V/S] (2:A+C+D+P+Ra)
Silence (rit.) A/120'/C<=10m [V/S] (2:Bd+C+DL+Ra+WlFa+WlU)
Spike Growth A/150'/C<=10m [V/S/M] (2:CN+D+DL+Ra+WlGe)
Summon Beast A/90'/C<=1h [V/S/M@200gp] (2:D+Ra)
Conjure Animals A/60'/C<=1h [V/S] (3:D+Ra)
Conjure Barrage A/S(60'cone)/I [V/S/M] (3:ABS+Ra)
Daylight A/60'/1h [V/S] (3:C+CLt+D+DL+P+Ra+S+WlC)
Elemental Weapon A/T/C<=1h [V/S] (3:A+CF+D+P+Ra+WlH)
Flame Arrows A/T/C<=1h [V/S] (3:A+D+Ra+S+Wz)
Lightning Arrow B/S/C<=1m [V/S] (3:Ra)
Meld into Stone (rit.) A/T/8h [V/S] (3:C+D+DL+Ra+WlGe)
Nondetection A/T/8h [V/S/M@!25!gp] (3:Bd+CK+PW+Ra+Wz)
Plant Growth A/8h/150'/I [V/S] (3:Bd+CN+D+DL+DW+PA+Ra+WlA)
Protection from Energy A/T/C<=1h [V/S] (3:A+C+CF+D+DL+PA+PG+PV+Ra+S+SCS+Wz)
Revivify A/T/I [V/S/M@!300!gp] (3:A+C+CG+CLf+D+DW+P+Ra+WlC)
Speak with Plants A/S(30'r)/10m [V/S] (3:Bd+D+Ra)
Summon Fey A/90'/C<=1h [V/S/M@300gp] (3:D+Ra+Wl+Wz)
Water Breathing (rit.) A/30'/24h [V/S/M] (3:A+D+DL+Ra+S+Wz)
Water Walk (rit.) A/30'/1h [V/S/M] (3:A+C+D+DL+Ra+S)
Wind Wall A/120'/C<=1m [V/S/M] (3:AArt+CN+D+Ra+WlGe)
The and_
predicate is simply a predicate that checks the values of two or more other predicates.
Ever wanted to know which spells are available to sorcerers but not wizards?
>>> s.where(classes=p.and_(p.contains('Sorcerer'), p.not_(p.contains('Wizard')))).print()
Chaos Bolt A/120'/I [V/S] (1:S)
Daylight A/60'/1h [V/S] (3:C+CLt+D+DL+P+Ra+S+WlC)
Water Walk (rit.) A/30'/1h [V/S/M] (3:A+C+D+DL+Ra+S)
Dominate Beast A/60'/C<=1m [V/S] (4:CN+D+PCo+S+WlA+WlGOO)
Insect Plague A/300'/C<=10m [V/S/M] (5:C+CN+CTm+D+DL+RaS+S)
Fire Storm A/150'/I [V/S] (7:C+D+S)
Earthquake A/500'/C<=1m [V/S/M] (8:C+D+S)
If a string is passed to the or_
or not_
composite predicates, it is mapped to the eq
predicate. Spells available via the Spirits Bard's 6th-level Spirit Session feature at level 6:
s(school=p.or_('Divination', 'Necromancy'), level=p.lte(3))
Current predicates are:
- `eq`: equivalent to Python's `==` operator
- `lt`, `lte`, `gt`, `gte`: numeric comparisons equivalent to `<`, `<=`, etc.
- `key`: checks for the presence of a key in dictionary fields
- `in_`: equivalent to `in` operator
- `and_`: `True` if all of the passed predicates return `True`
- `or_`: `True` if at least one of the passed predicates returns `True`
You can also pass your own predicates to where
. What's that race that starts with "aa" again?
>>> m.where(name=lambda attr, obj: getattr(obj, attr, '').startswith('aa')).print() #TODO: fix this example!
Monster({'name': Aarakocra, 'type': humanoid (aarakocra)})
Predicates need to be type-safe—in other words, they should safely return False
if the attribute is not present in an object.
Currently, monster and spell objects have three format options.
The default 'oneline'
format generates a single-line summary of the object's vital statistics. EG
>>> s.where(name='Magic Missile').print()
Magic Missile A/120'/I [V/S] (1:AArm+CA+S+Wz)
>>> s.where(name='Magic Missile').print('oneline')
Magic Missile A/120'/I [V/S] (1:AArm+CA+S+Wz)
The 'oneline'
format uses two- to three-letter abbreviations to summarize the classes of casters with access to the spell.
These are as follows:
#TODO
#TODO: write an example for this
The 'pointform'
format yields a header summary (identical to the 'oneline'
output)
followed by indented lines containing the rest of the information for the item.
For spells, the indented lines simply contain the spell text.
>>> s.where(name='Magic Missile').print('pointform')
- Magic Missile A/120'/I [V/S] (1:AArm+CA+S+Wz)
- Evocation (PHB#257)
- You create three glowing darts of magical force. Each dart hits a creature of your choice that you can see within range. A dart deals 1d4 + 1 force damage to its target. The darts all strike simultaneously, and you can direct them to hit one creature or several.
- At Higher Levels:
- When you cast this spell using a spell slot of 2nd level or higher, the spell creates one more dart for each slot level above 1st.
Any field that is present in an item of a given type can be used as a keyword argument to the where
method.
The following fields occur in one or more spell items:
- `'name'`: String containing the properly capitalized name of the spell
- `'roll'`: String containing the dice to be rolled as part of casting, if applicable
- this information is not presented in spell formatting (though it is invariably also present in the spell text)
- `'classes'`: List of strings, each containing the name of a character class or class subtype
- e.g. 'Wizard', 'Rogue (Arcane Trickster')
- subtypes are always written according to f'{class} ({subclass})' where both `class` and `subclass` are properly capitalized
- `'level'`: Integer between 0 and 9
- `'time'`: String containing the spell's casting time, e.g. `"1 minute"`
- `'school'`: String. `"Enchantment"`, `"Evocation"`, etc.
- `'text'`: Multiline string containing the spell description
- `'ritual'`: Boolean; `True` if the spell can be cast as a ritual
- `'duration'`: String similar to `'time'`.
- `'range'`: String; contains values like `'Self'`, `'Touch'`, `'30 feet'`, etc.
- `'components'`: currently a String, e.g. `'V, S, M (diamonds worth 300 gp, which the spell consumes)'`
Honestly I just can't be arsed to document this right now. Just check the __dict__
field.
legendlore
also includes a repltools
module. Currently this is just being used in ipython via
%run -i repltools.py
from the legendlore
directory. This command adds a number of objects to the repl namespace.
Currently, most of this module consists of convenience functions and objects
for queries on moon druid wildshaping.
The beasts
function as well as the moonws
data structure serve this purpose.
There are also a couple of functions to assist with moon druid multiclass queries:
bearbarian_ac
and drunk_ac
(druid/monk == drunk).
These can be used as per the following ipython snippet:
In [173]: [f'AC:{bearbarian_ac(c)} DEX:{c.dex} CON:{c.con} HP:{c.hp} {c}'
...: for c in moonws[2].sorted(key=bearbarian_ac, reverse=True) ][:20]
Out[162]:
['AC:16 DEX:17 CON:16 HP:34 Monster(Crag Cat: L unaligned beast, 1.0CR 34HP/4d10+12 13AC (walk 40))',
'AC:15 DEX:15 CON:15 HP:37 Monster(Dire Wolf: L unaligned beast, 1.0CR 37HP/5d10+10 14AC (walk 50))',
'AC:15 DEX:16 CON:14 HP:30 Monster(Steeder Female: L unaligned beast, 1.0CR 30HP/4d10+8 14AC (walk 30, climb 30))',
'AC:15 DEX:16 CON:14 HP:19 Monster(Clawfoot: M Unaligned beast, 1.0CR 19HP/3d8+6 13AC (walk 40))',
'AC:14 DEX:16 CON:13 HP:11 Monster(Giant Wolf Spider: M unaligned beast, 1/4CR 11HP/2d8+2 13AC (walk 40, climb 40))',
'AC:14 DEX:18 CON:10 HP:9 Monster(Fastieth: M Unaligned beast, 1/4CR 9HP/2d8 14AC (walk 50))',
'AC:14 DEX:14 CON:14 HP:19 Monster(Ape: M unaligned beast, 1/2CR 19HP/3d8+6 12AC (walk 30, climb 30))',
'AC:14 DEX:14 CON:11 HP:16 Monster(Jaculi: L unaligned beast, 1/2CR 16HP/3d10 14AC (walk 30, climb 20))',
'AC:14 DEX:14 CON:14 HP:45 Monster(Giant Hyena: L unaligned beast, 1.0CR 45HP/6d10+12 12AC (walk 50))',
'AC:14 DEX:16 CON:12 HP:26 Monster(Giant Spider: L unaligned beast, 1.0CR 26HP/4d10+4 14AC (walk 30, climb 30))',
'AC:14 DEX:15 CON:13 HP:26 Monster(Lion: L unaligned beast, 1.0CR 26HP/4d10+4 12AC (walk 50))',
'AC:14 DEX:15 CON:14 HP:37 Monster(Tiger: L unaligned beast, 1.0CR 37HP/5d10+10 12AC (walk 40))',
'AC:14 DEX:15 CON:14 HP:26 Monster(Deinonychus: M unaligned beast, 1.0CR 26HP/4d8+8 13AC (walk 40))',
'AC:13 DEX:16 CON:11 HP:4 Monster(Deer: M unaligned beast, 0.0CR 4HP/1d8 13AC (walk 50))',
'AC:13 DEX:10 CON:12 HP:4 Monster(Giant Fire Beetle: S unaligned beast, 0.0CR 4HP/1d6+1 13AC (walk 30))',
'AC:13 DEX:15 CON:11 HP:3 Monster(Jackal: S unaligned beast, 0.0CR 3HP/1d6 12AC (walk 40))',
'AC:13 DEX:16 CON:10 HP:3 Monster(Almiraj: S unaligned beast, 0.0CR 3HP/1d6 13AC (walk 50))',
'AC:13 DEX:15 CON:11 HP:7 Monster(Giant Rat: S unaligned beast, 1/8CR 7HP/2d6 12AC (walk 30))',
'AC:13 DEX:15 CON:11 HP:7 Monster(Giant Rat (Diseased): S unaligned beast, 1/8CR 7HP/2d6 12AC (walk 30))',
'AC:13 DEX:16 CON:10 HP:9 Monster(Giant Weasel: M unaligned beast, 1/8CR 9HP/2d8 13AC (walk 40))']
As documentation for this module is less likely to be updated than that of others, it is best to check the source file repltools.py
for information on its contents.
xsltproc
is required by FightClub5eXML to build the XML data. On Ubuntu, this can be installed via
$ sudo apt install xsltproc
You'll also need to have the simpleeval
python module installed. To install with pip
:
$ pip3 install simpleeval
Once you've got the above tools installed (plus any I forgot about), you're ready to install the repo.
To install the repo, change directory to one that's in your PYTHONPATH and run
$ git clone --recursive https://github.com/intuited/legendlore
Assuming the submodule gets set up correctly, at this point you just need to build the actual XML file with a simple make
command:
$ cd legendlore
$ make
Once this is done, you should be able to use the module. Test with ipython:
$ PYTHONPATH=.. ipython3
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.13.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from legendlore.repltools import *
sp(
In [2]: sp('legend lore')
* Legend Lore 10m/S/I [V/S/M@200+!250!gp] (5:Bd+C+CK+WlU+Wz)
" Divination (PHB#254); Material components: incense worth at least 250 gp, which the spell consumes, and four ivory strips worth at least 50 gp each
" Name or describe a person, place, or object. The spell brings to your mind a brief summary of the significant lore about the thing you named. The lore might consist of current tales, forgotten stories, or even secret lore that has never been widely known. If the thing you named isn't of legendary importance, you gain no information. The more information you already have about the thing, the more precise and detailed the information you receive is.
" The information you learn is accurate but might be couched in figurative language. For example, if you have a mysterious magic axe on hand, the spell might yield this information: "Woe to the evildoer whose hand touches the axe, for even the haft slices the hand of the evil ones. Only a true Child of Stone, lover and beloved of Moradin, may awaken the true powers of the axe, and only with the sacred word Rudnogg on the lips."