Adding scrolls
This commit introduces a random spells scroll generator
This commit is contained in:
parent
f5814bd1ef
commit
9c19373120
|
@ -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)
|
||||||
|
|
|
@ -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
51
dnd_item/scrolls.py
Normal 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
3594
dnd_item/sources/spells.yaml
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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"])
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user