Code Monkey home page Code Monkey logo

legendlore's Introduction

legendlore

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.

Vim usage

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.

REPL usage

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 *

Usage

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

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.

Predicates: tl;dr

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.

Composite predicates

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`

Using custom predicates

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.

Format options

Currently, monster and spell objects have three format options.

'oneline'

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)
Class abbreviations

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

'full'

#TODO: write an example for this

'pointform'

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.

Fields

Any field that is present in an item of a given type can be used as a keyword argument to the where method.

Spell fields

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)'`

Monster fields

Honestly I just can't be arsed to document this right now. Just check the __dict__ field.

repltools

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.

Required tools for installation

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

Installation

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."

legendlore's People

Stargazers

 avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.