Adding scrolls

This commit introduces a random spells scroll generator
This commit is contained in:
evilchili 2024-01-17 18:30:53 -08:00
parent f5814bd1ef
commit 9c19373120
6 changed files with 3771 additions and 7 deletions

View File

@ -12,6 +12,7 @@ from rich.table import Table
from dnd_item import five_e from dnd_item import five_e
from dnd_item.types import RollTable from dnd_item.types import RollTable
from dnd_item.weapons import WeaponGenerator from dnd_item.weapons import WeaponGenerator
from dnd_item.scrolls import ScrollGenerator
app = typer.Typer() app = typer.Typer()
app_state = {} app_state = {}
@ -46,6 +47,13 @@ def weapon(count: int = typer.Option(1, help="The number of weapons to generate.
console.print(weapon.details) console.print(weapon.details)
@app.command()
def scroll(count: int = typer.Option(1, help="The number of weapons to generate.")):
console = Console()
for scroll in ScrollGenerator().random(count=count, challenge_rating=app_state["cr"]):
console.print(scroll.details)
@app.command("roll-table") @app.command("roll-table")
def table( def table(
die: int = typer.Option(20, help="The size of the die for which to create a table"), die: int = typer.Option(20, help="The size of the die for which to create a table"),
@ -84,5 +92,5 @@ def table(
@app.command() @app.command()
def convert(): def convert():
src = five_e.weapons() src = five_e.spells()
print(src.as_yaml) print(src.as_yaml)

View File

@ -1,4 +1,5 @@
import json import json
import re
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@ -28,6 +29,30 @@ PROPERTIES = {
"R": "reach", "R": "reach",
} }
LEVEL = [
'cantrip',
'first',
'second',
'third',
'fourth',
'fifth',
'sixth',
'seventh',
'eighth',
'ninth',
]
SCHOOL = {
'A': 'Abjuration',
'C': 'Conjuration',
'D': 'Divination',
'E': 'Enchantment',
'I': 'Illusion',
'N': 'Necromancy',
'T': 'Transmutation',
'V': 'Evocation',
}
class Weapons(DataSource): class Weapons(DataSource):
""" """
@ -83,7 +108,89 @@ class Weapons(DataSource):
return yaml.dump({"metadata": self.metadata}) + yaml.dump(dict(self.data)) return yaml.dump({"metadata": self.metadata}) + yaml.dump(dict(self.data))
class Spells(DataSource):
def read_source(self) -> None:
src = json.load(self.source)['spell']
self.data = defaultdict(list)
headers = [
"Level",
"Name",
"School",
"Range",
"Duration",
"Damage Die",
"Damage Type",
"Material Cost",
]
dmg_die = re.compile(r'\d+d\d+')
for spell in sorted(src, key=lambda x: int(x['level'])):
distance = ""
if spell["range"]["type"] == "special":
distance = "special"
elif "amount" in spell["range"]["distance"]:
distance = f"{spell['range']['distance']['amount']} {spell['range']['distance']['type']}"
else:
distance = spell["range"]["distance"]["type"]
dmgdice = ""
dmgtype = ""
if 'damageInflict' in spell:
try:
dmgdice = dmg_die.findall(str(spell["entries"]))[0]
except IndexError:
pass
dmgtype = ','.join(spell['damageInflict'])
duration = ""
dur = spell["duration"][0]
if dur["type"] == "timed":
s_or_blank = 's' if dur['duration']['amount'] > 1 else ''
duration = f"{dur['duration']['amount']} {dur['duration']['type']}{s_or_blank}"
else:
duration = dur["type"]
cost = ""
try:
cost = spell['components']['m']['text']
except (KeyError, TypeError):
cost = ""
self.data[LEVEL[spell["level"]]].append(
{
spell["name"].title(): [
SCHOOL[spell["school"]],
str(distance),
str(duration),
dmgdice,
dmgtype,
cost
]
}
)
self.metadata = {"headers": headers}
@property
def as_yaml(self) -> str:
return yaml.dump({"metadata": self.metadata}) + yaml.dump(dict(self.data))
def weapons(source_path: str = "items-base.json") -> dict: def weapons(source_path: str = "items-base.json") -> dict:
with open(sources / Path(source_path)) as filehandle: with open(sources / Path(source_path)) as filehandle:
ds = Weapons(source=filehandle) ds = Weapons(source=filehandle)
return ds return ds
def spells() -> dict:
spells = []
for source in ['phb', 'ftd', 'scc', 'xge', 'tce']:
source_path = sources / Path(f"spells-{source}.json")
with open(source_path) as filehandle:
spells.append(Spells(source=filehandle))
for i in range(1, len(spells)):
for level, spell_list in spells[i].data.items():
spells[0].data[level] += spell_list
return spells[0]

51
dnd_item/scrolls.py Normal file
View File

@ -0,0 +1,51 @@
from dnd_item import types
from rolltable.tables import spells
class Scroll(types.Item):
"""
A magic scroll.
"""
@property
def name(self):
return 'Scroll of {spell.name}'.format(**self).title()
@property
def summary(self):
if self.spell.level == 'cantrip':
return f"{self.name} ({self.spell.school} {self.spell.level})"
else:
return f"{self.name} ({self.spell.level} level {self.spell.school})"
details = summary
class ScrollGenerator(types.ItemGenerator):
item_class = Scroll
def random_properties(self, rarity: str = '') -> dict:
"""
Every scroll must have a randomly-selected spell.
"""
item = super().random_properties(rarity)
# maps maximum spell level to a rarity level (0=common...)
frequencies = {
'common': 'first',
'uncommon': 'third',
'rare': 'fifth',
'very rare': 'seventh',
'legendary': 'ninth'
}
# Create a rolltable of spells at an appropriate level with one row
spells.reset()
spells.die = 1
spells.datasources[0].set_frequency(frequencies[item['rarity']['rarity']])
# add the spell to the item as a dictionary
keys = [h.lower().replace(' ', '_') for h in spells.headers]
item['spell'] = dict(zip(*[keys, spells.rows[1][1:]]))
return item

3594
dnd_item/sources/spells.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import logging import logging
import random
import re import re
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -285,7 +286,7 @@ class ItemGenerator:
(dict(name='{type} stick', type='silver'), 0.5), (dict(name='{type} stick', type='silver'), 0.5),
(dict(name='{type} stick', type='glass'), 0.1), (dict(name='{type} stick', type='glass'), 0.1),
), ),
rarity.types.RARITY, rarity=types.RARITY,
properties_by_rarity=types.PROPERTIES_BY_RARITY, properties_by_rarity=types.PROPERTIES_BY_RARITY,
) )
@ -317,9 +318,9 @@ class ItemGenerator:
# override this with a subclass of Item. # override this with a subclass of Item.
item_class = Item item_class = Item
def __init__(self, bases: WeightedSet, rarity: WeightedSet, properties_by_rarity: dict): def __init__(self, bases: WeightedSet = None, rarity: WeightedSet = None, properties_by_rarity: dict = None):
self.bases = bases self.bases = bases or WeightedSet((dict(name=self.__class__.__name__), 1.0))
self.rarity = rarity self.rarity = rarity or RARITY
self.properties_by_rarity = properties_by_rarity self.properties_by_rarity = properties_by_rarity
def _property_count_by_rarity(self, rarity: str) -> int: def _property_count_by_rarity(self, rarity: str) -> int:
@ -329,6 +330,9 @@ class ItemGenerator:
2 or 3 properties. This is the primary method by which Items of greater 2 or 3 properties. This is the primary method by which Items of greater
rarity become more valuable and wondrous, justifying their rarity. rarity become more valuable and wondrous, justifying their rarity.
""" """
if not self.properties_by_rarity:
return 0
property_count_by_rarity = { property_count_by_rarity = {
"common": WeightedSet((1, 0.1), (0, 1.0)), "common": WeightedSet((1, 0.1), (0, 1.0)),
"uncommon": WeightedSet((1, 1.0)), "uncommon": WeightedSet((1, 1.0)),
@ -407,7 +411,7 @@ class ItemGenerator:
if rarity: if rarity:
item["rarity"] = self.rarity.source.as_dict()[rarity] item["rarity"] = self.rarity.source.as_dict()[rarity]
else: else:
item["rarity"] = self.rarity.random() item['rarity'] = self.rarity.random()
# select a number of properties appropriate to the rarity # select a number of properties appropriate to the rarity
num_properties = self._property_count_by_rarity(item["rarity"]["rarity"]) num_properties = self._property_count_by_rarity(item["rarity"]["rarity"])

View File

@ -271,7 +271,7 @@ class WeaponGenerator(types.ItemGenerator):
): ):
super().__init__(bases=bases, rarity=rarity, properties_by_rarity=properties_by_rarity) super().__init__(bases=bases, rarity=rarity, properties_by_rarity=properties_by_rarity)
def random_properties(self) -> dict: def random_properties(self, rarity: str = '') -> dict:
# add missing base weapon defaults # add missing base weapon defaults
# TODO: update the sources then delete this method # TODO: update the sources then delete this method
item = super().random_properties() item = super().random_properties()