automatic formatting
This commit is contained in:
parent
51ecf83357
commit
5b4c26ecf2
|
@ -1,33 +1,31 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.logging import RichHandler
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.logging import RichHandler
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
|
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 import five_e
|
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
app_state = {}
|
app_state = {}
|
||||||
|
|
||||||
|
|
||||||
class OUTPUT_FORMATS(Enum):
|
class OUTPUT_FORMATS(Enum):
|
||||||
text = 'text'
|
text = "text"
|
||||||
yaml = 'yaml'
|
yaml = "yaml"
|
||||||
markdown = 'markdown'
|
markdown = "markdown"
|
||||||
|
|
||||||
|
|
||||||
@app.callback()
|
@app.callback()
|
||||||
def main(
|
def main(
|
||||||
cr: int = typer.Option(default=None, help='The Challenge Rating to use when determining rarity.'),
|
cr: int = typer.Option(default=None, help="The Challenge Rating to use when determining rarity."),
|
||||||
):
|
):
|
||||||
debug = os.getenv("FANITEM_DEBUG", None)
|
debug = os.getenv("FANITEM_DEBUG", None)
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
@ -35,38 +33,32 @@ def main(
|
||||||
level=logging.DEBUG if debug else logging.INFO,
|
level=logging.DEBUG if debug else logging.INFO,
|
||||||
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])],
|
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])],
|
||||||
)
|
)
|
||||||
logging.getLogger('markdown_it').setLevel(logging.ERROR)
|
logging.getLogger("markdown_it").setLevel(logging.ERROR)
|
||||||
|
|
||||||
app_state['cr'] = cr or 0
|
app_state["cr"] = cr or 0
|
||||||
app_state['data'] = Path(__file__).parent / Path("sources")
|
app_state["data"] = Path(__file__).parent / Path("sources")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def weapon(count: int = typer.Option(1, help="The number of weapons to generate.")):
|
def weapon(count: int = typer.Option(1, help="The number of weapons to generate.")):
|
||||||
console = Console()
|
console = Console()
|
||||||
for weapon in WeaponGenerator().random(count=count, challenge_rating=app_state['cr']):
|
for weapon in WeaponGenerator().random(count=count, challenge_rating=app_state["cr"]):
|
||||||
console.print(weapon.details)
|
console.print(weapon.details)
|
||||||
|
|
||||||
|
|
||||||
@app.command("roll-table")
|
@app.command("roll-table")
|
||||||
def table(
|
def table(
|
||||||
die: int = typer.Option(
|
die: int = typer.Option(20, help="The size of the die for which to create a table"),
|
||||||
20,
|
|
||||||
help='The size of the die for which to create a table'),
|
|
||||||
hide_rolls: bool = typer.Option(
|
hide_rolls: bool = typer.Option(
|
||||||
False,
|
False,
|
||||||
help='If True, do not show the Roll column.',
|
help="If True, do not show the Roll column.",
|
||||||
),
|
),
|
||||||
collapsed: bool = typer.Option(
|
collapsed: bool = typer.Option(True, help="If True, collapse multiple die values with the same option."),
|
||||||
True,
|
width: int = typer.Option(180, help="Width of the table."),
|
||||||
help='If True, collapse multiple die values with the same option.'),
|
|
||||||
width: int = typer.Option(
|
|
||||||
180,
|
|
||||||
help='Width of the table.'),
|
|
||||||
output: OUTPUT_FORMATS = typer.Option(
|
output: OUTPUT_FORMATS = typer.Option(
|
||||||
'text',
|
"text",
|
||||||
help='The output format to use.',
|
help="The output format to use.",
|
||||||
)
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
CLI for creating roll tables of randomly-generated items.
|
CLI for creating roll tables of randomly-generated items.
|
||||||
|
@ -75,7 +67,7 @@ def table(
|
||||||
sources=[WeaponGenerator],
|
sources=[WeaponGenerator],
|
||||||
die=die,
|
die=die,
|
||||||
hide_rolls=hide_rolls,
|
hide_rolls=hide_rolls,
|
||||||
challenge_rating=app_state['cr'],
|
challenge_rating=app_state["cr"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == OUTPUT_FORMATS.yaml:
|
if output == OUTPUT_FORMATS.yaml:
|
||||||
|
|
|
@ -1,46 +1,31 @@
|
||||||
import json
|
import json
|
||||||
import yaml
|
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from random_sets.datasources import DataSource
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from random_sets.datasources import DataSource
|
||||||
|
|
||||||
sources = Path(__file__).parent / Path("sources")
|
sources = Path(__file__).parent / Path("sources")
|
||||||
|
|
||||||
RARITY = {
|
RARITY = {"unknown": "common", "none": "common", "": ""}
|
||||||
'unknown': 'common',
|
|
||||||
'none': 'common',
|
|
||||||
'': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
TYPE = {
|
TYPE = {"M": "martial", "R": "ranged", "": ""}
|
||||||
'M': 'martial',
|
|
||||||
'R': 'ranged',
|
|
||||||
'': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
DAMAGE = {
|
DAMAGE = {"S": "Slashing", "P": "Piercing", "B": "Bludgeoning", "": ""}
|
||||||
'S': 'Slashing',
|
|
||||||
'P': 'Piercing',
|
|
||||||
'B': 'Bludgeoning',
|
|
||||||
'': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
PROPERTIES = {
|
PROPERTIES = {
|
||||||
'F': 'finesse',
|
"F": "finesse",
|
||||||
'AF': 'firearm',
|
"AF": "firearm",
|
||||||
'A': 'ammmunition',
|
"A": "ammmunition",
|
||||||
'T': 'thrown',
|
"T": "thrown",
|
||||||
'L': 'light',
|
"L": "light",
|
||||||
'2H': 'two-handed',
|
"2H": "two-handed",
|
||||||
'V': 'versatile',
|
"V": "versatile",
|
||||||
'RLD': 'reload',
|
"RLD": "reload",
|
||||||
'LD': 'loading',
|
"LD": "loading",
|
||||||
'S': 'special',
|
"S": "special",
|
||||||
'H': 'heavy',
|
"H": "heavy",
|
||||||
'R': 'reach',
|
"R": "reach",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,42 +34,56 @@ class Weapons(DataSource):
|
||||||
A rolltables data source backed by a 5e.tools json data file. used to
|
A rolltables data source backed by a 5e.tools json data file. used to
|
||||||
convert the 5e.tools data to the yaml format consumed by dnd-rolltables.
|
convert the 5e.tools data to the yaml format consumed by dnd-rolltables.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def read_source(self) -> None:
|
def read_source(self) -> None:
|
||||||
src = json.load(self.source)['baseitem']
|
src = json.load(self.source)["baseitem"]
|
||||||
self.data = defaultdict(list)
|
self.data = defaultdict(list)
|
||||||
headers = ['Rarity', 'Name', 'Category', 'Type', 'Weight', 'Damage Type',
|
headers = [
|
||||||
'Damage Dice', 'Range', 'Reload', 'Value', 'Properties']
|
"Rarity",
|
||||||
|
"Name",
|
||||||
|
"Category",
|
||||||
|
"Type",
|
||||||
|
"Weight",
|
||||||
|
"Damage Type",
|
||||||
|
"Damage Dice",
|
||||||
|
"Range",
|
||||||
|
"Reload",
|
||||||
|
"Value",
|
||||||
|
"Properties",
|
||||||
|
]
|
||||||
|
|
||||||
for item in src:
|
for item in src:
|
||||||
if not item.get('weapon', False):
|
if not item.get("weapon", False):
|
||||||
continue
|
continue
|
||||||
if item.get('age', False):
|
if item.get("age", False):
|
||||||
continue
|
continue
|
||||||
rarity = RARITY.get(item['rarity'], 'Common').capitalize()
|
rarity = RARITY.get(item["rarity"], "Common").capitalize()
|
||||||
itype = TYPE.get(item['type'], '_unknown').capitalize()
|
itype = TYPE.get(item["type"], "_unknown").capitalize()
|
||||||
properties = ', '.join([PROPERTIES[p] for p in item.get('property', [])])
|
properties = ", ".join([PROPERTIES[p] for p in item.get("property", [])])
|
||||||
|
|
||||||
self.data[rarity].append({
|
self.data[rarity].append(
|
||||||
item['name'].capitalize(): [
|
{
|
||||||
item['weaponCategory'],
|
item["name"].capitalize(): [
|
||||||
itype,
|
item["weaponCategory"],
|
||||||
str(item.get('weight', 0)),
|
itype,
|
||||||
DAMAGE.get(item.get('dmgType', '')),
|
str(item.get("weight", 0)),
|
||||||
item.get('dmg1', None),
|
DAMAGE.get(item.get("dmgType", "")),
|
||||||
item.get('range', None),
|
item.get("dmg1", None),
|
||||||
str(item.get('reload', '')),
|
item.get("range", None),
|
||||||
str(item.get('value', '')),
|
str(item.get("reload", "")),
|
||||||
properties,
|
str(item.get("value", "")),
|
||||||
]
|
properties,
|
||||||
})
|
]
|
||||||
self.metadata = {'headers': headers}
|
}
|
||||||
|
)
|
||||||
|
self.metadata = {"headers": headers}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_yaml(self) -> str:
|
def as_yaml(self) -> str:
|
||||||
return yaml.dump({'metadata': self.metadata}) + yaml.dump(dict(self.data))
|
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
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from pathlib import Path
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
from random_sets.sets import WeightedSet, DataSourceSet
|
|
||||||
|
|
||||||
import rolltable.types
|
import rolltable.types
|
||||||
|
from random_sets.sets import DataSourceSet, WeightedSet
|
||||||
|
|
||||||
# Create DataSourceSets, which are WeightedSets populated with DataSource
|
# Create DataSourceSets, which are WeightedSets populated with DataSource
|
||||||
# objects generated from yaml data files. These are used to supply default
|
# objects generated from yaml data files. These are used to supply default
|
||||||
# values to item generators; see below.
|
# values to item generators; see below.
|
||||||
sources = Path(__file__).parent / Path("sources")
|
sources = Path(__file__).parent / Path("sources")
|
||||||
ENCHANTMENT = DataSourceSet(sources / Path('magic_damage_types.yaml'))
|
ENCHANTMENT = DataSourceSet(sources / Path("magic_damage_types.yaml"))
|
||||||
WEAPON_TYPES = DataSourceSet(sources / Path('weapons.yaml'))
|
WEAPON_TYPES = DataSourceSet(sources / Path("weapons.yaml"))
|
||||||
RARITY = DataSourceSet(sources / Path('rarity.yaml'))
|
RARITY = DataSourceSet(sources / Path("rarity.yaml"))
|
||||||
PROPERTIES_BY_RARITY = {
|
PROPERTIES_BY_RARITY = {
|
||||||
'base': DataSourceSet(sources / Path('properties_base.yaml')),
|
"base": DataSourceSet(sources / Path("properties_base.yaml")),
|
||||||
'common': DataSourceSet(sources / Path('properties_common.yaml')),
|
"common": DataSourceSet(sources / Path("properties_common.yaml")),
|
||||||
'uncommon': DataSourceSet(sources / Path('properties_uncommon.yaml')),
|
"uncommon": DataSourceSet(sources / Path("properties_uncommon.yaml")),
|
||||||
'rare': DataSourceSet(sources / Path('properties_rare.yaml')),
|
"rare": DataSourceSet(sources / Path("properties_rare.yaml")),
|
||||||
'very rare': DataSourceSet(sources / Path('properties_very_rare.yaml')),
|
"very rare": DataSourceSet(sources / Path("properties_very_rare.yaml")),
|
||||||
'legendary': DataSourceSet(sources / Path('properties_legendary.yaml')),
|
"legendary": DataSourceSet(sources / Path("properties_legendary.yaml")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,14 +54,14 @@ class AttributeDict(Mapping):
|
||||||
"""
|
"""
|
||||||
attrs = {}
|
attrs = {}
|
||||||
for k, v in sorted(kwargs.items()):
|
for k, v in sorted(kwargs.items()):
|
||||||
attrs[k] = AttributeDict.from_dict(v)if type(v) is dict else v
|
attrs[k] = AttributeDict.from_dict(v) if type(v) is dict else v
|
||||||
return cls(attributes=attrs)
|
return cls(attributes=attrs)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Item(AttributeDict):
|
class Item(AttributeDict):
|
||||||
"""
|
""" """
|
||||||
"""
|
|
||||||
_name: str = None
|
_name: str = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -76,7 +73,7 @@ class Item(AttributeDict):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
desc = "\n".join([k.title() + ". " + v.description for k, v in self.get('properties', {}).items()])
|
desc = "\n".join([k.title() + ". " + v.description for k, v in self.get("properties", {}).items()])
|
||||||
return desc.format(**self)
|
return desc.format(**self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -88,13 +85,12 @@ class Item(AttributeDict):
|
||||||
|
|
||||||
# delay processing the 'properties' attribute until after the other
|
# delay processing the 'properties' attribute until after the other
|
||||||
# attributes, because they may contain references to those attributes.
|
# attributes, because they may contain references to those attributes.
|
||||||
properties = attrs.pop('properties', None)
|
properties = attrs.pop("properties", None)
|
||||||
|
|
||||||
attributes = dict()
|
attributes = dict()
|
||||||
|
|
||||||
# recursively locate and populate template strings
|
# recursively locate and populate template strings
|
||||||
def _format(obj, this=None):
|
def _format(obj, this=None):
|
||||||
|
|
||||||
# enables use of the 'this' keyword to refer to the current context
|
# enables use of the 'this' keyword to refer to the current context
|
||||||
# in a template. Refer to the enchantment sources for an example.
|
# in a template. Refer to the enchantment sources for an example.
|
||||||
if this:
|
if this:
|
||||||
|
@ -102,9 +98,7 @@ class Item(AttributeDict):
|
||||||
|
|
||||||
# dicts and lists are descended into
|
# dicts and lists are descended into
|
||||||
if type(obj) is dict:
|
if type(obj) is dict:
|
||||||
return AttributeDict.from_dict(dict(
|
return AttributeDict.from_dict(dict((key, _format(val, this=obj)) for key, val in obj.items()))
|
||||||
(key, _format(val, this=obj)) for key, val in obj.items()
|
|
||||||
))
|
|
||||||
if type(obj) is list:
|
if type(obj) is list:
|
||||||
return [_format(o, this=this) for o in obj]
|
return [_format(o, this=this) for o in obj]
|
||||||
|
|
||||||
|
@ -124,32 +118,29 @@ class Item(AttributeDict):
|
||||||
else:
|
else:
|
||||||
attributes[k] = _format(v)
|
attributes[k] = _format(v)
|
||||||
if properties:
|
if properties:
|
||||||
attributes['properties'] = AttributeDict.from_dict(_format(properties))
|
attributes["properties"] = AttributeDict.from_dict(_format(properties))
|
||||||
for prop in attributes['properties'].values():
|
for prop in attributes["properties"].values():
|
||||||
overrides = [k for k in prop.attributes.keys() if k.startswith('override_')]
|
overrides = [k for k in prop.attributes.keys() if k.startswith("override_")]
|
||||||
for o in overrides:
|
for o in overrides:
|
||||||
if prop.attributes[o]:
|
if prop.attributes[o]:
|
||||||
attributes[o.replace('override_', '')] = prop.attributes[o]
|
attributes[o.replace("override_", "")] = prop.attributes[o]
|
||||||
|
|
||||||
# store the item name as the _name attribute; it is accessable directly, or
|
# store the item name as the _name attribute; it is accessable directly, or
|
||||||
# via the name property. This makes overriding the name convenient for subclassers,
|
# via the name property. This makes overriding the name convenient for subclassers,
|
||||||
# which may require naming semantics that cannot be resolved at instantiation time.
|
# which may require naming semantics that cannot be resolved at instantiation time.
|
||||||
_name = attributes['name']
|
_name = attributes["name"]
|
||||||
del attributes['name']
|
del attributes["name"]
|
||||||
|
|
||||||
# At this point, attributes is a dictionary with members of multiple
|
# At this point, attributes is a dictionary with members of multiple
|
||||||
# types, but every dict member has been converted to an AttributeDict,
|
# types, but every dict member has been converted to an AttributeDict,
|
||||||
# and all template strings in the object have been formatted. Return an
|
# and all template strings in the object have been formatted. Return an
|
||||||
# instance of the Item class using these formatted attributes.
|
# instance of the Item class using these formatted attributes.
|
||||||
return cls(
|
return cls(_name=_name, attributes=attributes)
|
||||||
_name=_name,
|
|
||||||
attributes=attributes
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ItemGenerator:
|
class ItemGenerator:
|
||||||
"""
|
""" """
|
||||||
"""
|
|
||||||
item_class = Item
|
item_class = Item
|
||||||
|
|
||||||
def __init__(self, bases: WeightedSet, rarity: WeightedSet, properties_by_rarity: dict):
|
def __init__(self, bases: WeightedSet, rarity: WeightedSet, properties_by_rarity: dict):
|
||||||
|
@ -159,19 +150,16 @@ class ItemGenerator:
|
||||||
|
|
||||||
def _property_count_by_rarity(self, rarity: str) -> int:
|
def _property_count_by_rarity(self, rarity: str) -> int:
|
||||||
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)),
|
||||||
'rare': WeightedSet((1, 1.0), (2, 0.5)),
|
"rare": WeightedSet((1, 1.0), (2, 0.5)),
|
||||||
'very rare': WeightedSet((1, 0.5), (2, 1.0)),
|
"very rare": WeightedSet((1, 0.5), (2, 1.0)),
|
||||||
'legendary': WeightedSet((2, 1.0), (3, 1.0)),
|
"legendary": WeightedSet((2, 1.0), (3, 1.0)),
|
||||||
}
|
}
|
||||||
return min(
|
return min(property_count_by_rarity[rarity].random(), len(self.properties_by_rarity[rarity].members))
|
||||||
property_count_by_rarity[rarity].random(),
|
|
||||||
len(self.properties_by_rarity[rarity].members)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_requirements(self, item) -> set:
|
def get_requirements(self, item) -> set:
|
||||||
pat = re.compile(r'{([^\.\}]+)')
|
pat = re.compile(r"{([^\.\}]+)")
|
||||||
|
|
||||||
def getreqs(obj):
|
def getreqs(obj):
|
||||||
if type(obj) is dict:
|
if type(obj) is dict:
|
||||||
|
@ -188,28 +176,28 @@ class ItemGenerator:
|
||||||
|
|
||||||
def random_properties(self) -> dict:
|
def random_properties(self) -> dict:
|
||||||
item = self.bases.random()
|
item = self.bases.random()
|
||||||
item['rarity'] = self.rarity.random()
|
item["rarity"] = self.rarity.random()
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
num_properties = self._property_count_by_rarity(item['rarity']['rarity'])
|
num_properties = self._property_count_by_rarity(item["rarity"]["rarity"])
|
||||||
while len(properties) != num_properties:
|
while len(properties) != num_properties:
|
||||||
thisprop = self.properties_by_rarity[item['rarity']['rarity']].random()
|
thisprop = self.properties_by_rarity[item["rarity"]["rarity"]].random()
|
||||||
properties[thisprop['name']] = thisprop
|
properties[thisprop["name"]] = thisprop
|
||||||
|
|
||||||
# add properties from the base item (versatile, thrown, artifact..)
|
# add properties from the base item (versatile, thrown, artifact..)
|
||||||
for name in item.pop('properties', '').split(','):
|
for name in item.pop("properties", "").split(","):
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
if name:
|
if name:
|
||||||
properties[name] = self.properties_by_rarity['base'].source.as_dict()[name]
|
properties[name] = self.properties_by_rarity["base"].source.as_dict()[name]
|
||||||
|
|
||||||
item['properties'] = properties
|
item["properties"] = properties
|
||||||
|
|
||||||
# look for template strings that reference item attributes which do not yet exist.
|
# look for template strings that reference item attributes which do not yet exist.
|
||||||
# Add anything that is missing via a callback.
|
# Add anything that is missing via a callback.
|
||||||
predefined = list(item.keys()) + ['this', '_name']
|
predefined = list(item.keys()) + ["this", "_name"]
|
||||||
for requirement in [r for r in self.get_requirements(item) if r not in predefined]:
|
for requirement in [r for r in self.get_requirements(item) if r not in predefined]:
|
||||||
try:
|
try:
|
||||||
item[requirement] = getattr(self, f'get_{requirement}')(**item)
|
item[requirement] = getattr(self, f"get_{requirement}")(**item)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.error("{item['name']} requires {self.__class__.__name__} to have a get_{requirement}() method.")
|
logging.error("{item['name']} requires {self.__class__.__name__} to have a get_{requirement}() method.")
|
||||||
raise
|
raise
|
||||||
|
@ -225,15 +213,15 @@ class ItemGenerator:
|
||||||
# select the appropriate frequency distributionnb ased on the specified
|
# select the appropriate frequency distributionnb ased on the specified
|
||||||
# challenge rating. By default, all rarities are weighted equally.
|
# challenge rating. By default, all rarities are weighted equally.
|
||||||
if challenge_rating in range(1, 5):
|
if challenge_rating in range(1, 5):
|
||||||
frequency = '1-4'
|
frequency = "1-4"
|
||||||
elif challenge_rating in range(5, 11):
|
elif challenge_rating in range(5, 11):
|
||||||
frequency = '5-10'
|
frequency = "5-10"
|
||||||
elif challenge_rating in range(11, 17):
|
elif challenge_rating in range(11, 17):
|
||||||
frequency = '11-16'
|
frequency = "11-16"
|
||||||
elif challenge_rating >= 17:
|
elif challenge_rating >= 17:
|
||||||
frequency = '17'
|
frequency = "17"
|
||||||
else:
|
else:
|
||||||
frequency = 'default'
|
frequency = "default"
|
||||||
self.rarity.set_frequency(frequency)
|
self.rarity.set_frequency(frequency)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
@ -250,14 +238,8 @@ class GeneratorSource:
|
||||||
def random_values(self, count: int = 1) -> list:
|
def random_values(self, count: int = 1) -> list:
|
||||||
vals = sorted(
|
vals = sorted(
|
||||||
(
|
(
|
||||||
item.rarity['sort_order'],
|
item.rarity["sort_order"],
|
||||||
[
|
[item.name, item.rarity["rarity"], item.summary, ", ".join(item.get("properties", [])), item.id],
|
||||||
item.name,
|
|
||||||
item.rarity['rarity'],
|
|
||||||
item.summary,
|
|
||||||
', '.join(item.get('properties', [])),
|
|
||||||
item.id
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
for item in self.generator.random(count=count, challenge_rating=self.cr)
|
for item in self.generator.random(count=count, challenge_rating=self.cr)
|
||||||
)
|
)
|
||||||
|
@ -275,7 +257,7 @@ class RollTable(rolltable.types.RollTable):
|
||||||
self._cr = challenge_rating
|
self._cr = challenge_rating
|
||||||
super().__init__(
|
super().__init__(
|
||||||
sources=sources,
|
sources=sources,
|
||||||
frequency='default',
|
frequency="default",
|
||||||
die=die,
|
die=die,
|
||||||
hide_rolls=hide_rolls,
|
hide_rolls=hide_rolls,
|
||||||
)
|
)
|
||||||
|
@ -286,10 +268,10 @@ class RollTable(rolltable.types.RollTable):
|
||||||
self._data.append(GeneratorSource(generator=src(), cr=self._cr))
|
self._data.append(GeneratorSource(generator=src(), cr=self._cr))
|
||||||
|
|
||||||
self._headers = [
|
self._headers = [
|
||||||
'Name',
|
"Name",
|
||||||
'Rarity',
|
"Rarity",
|
||||||
'Summary',
|
"Summary",
|
||||||
'Properties',
|
"Properties",
|
||||||
'ID',
|
"ID",
|
||||||
]
|
]
|
||||||
self._header_excludes = []
|
self._header_excludes = []
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import random
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import random
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from dnd_item import types
|
|
||||||
from random_sets.sets import WeightedSet, equal_weights
|
from random_sets.sets import WeightedSet, equal_weights
|
||||||
|
|
||||||
|
from dnd_item import types
|
||||||
|
|
||||||
|
|
||||||
def random_from_csv(csv: str) -> str:
|
def random_from_csv(csv: str) -> str:
|
||||||
return random.choice(csv.split(',')).strip()
|
return random.choice(csv.split(",")).strip()
|
||||||
|
|
||||||
|
|
||||||
class Weapon(types.Item):
|
class Weapon(types.Item):
|
||||||
"""
|
""" """
|
||||||
"""
|
|
||||||
|
|
||||||
def _descriptors(self) -> tuple:
|
def _descriptors(self) -> tuple:
|
||||||
"""
|
"""
|
||||||
|
@ -22,30 +21,32 @@ class Weapon(types.Item):
|
||||||
"""
|
"""
|
||||||
nouns = dict()
|
nouns = dict()
|
||||||
adjectives = dict()
|
adjectives = dict()
|
||||||
if not hasattr(self, 'properties'):
|
if not hasattr(self, "properties"):
|
||||||
return (nouns, adjectives)
|
return (nouns, adjectives)
|
||||||
for prop_name, prop in self.properties.items():
|
for prop_name, prop in self.properties.items():
|
||||||
if hasattr(prop, 'nouns'):
|
if hasattr(prop, "nouns"):
|
||||||
nouns[prop_name] = equal_weights(prop.nouns.split(','), blank=False)
|
nouns[prop_name] = equal_weights(prop.nouns.split(","), blank=False)
|
||||||
if hasattr(prop, 'adjectives'):
|
if hasattr(prop, "adjectives"):
|
||||||
adjectives[prop_name] = equal_weights(prop.adjectives.split(','), blank=False)
|
adjectives[prop_name] = equal_weights(prop.adjectives.split(","), blank=False)
|
||||||
return (nouns, adjectives)
|
return (nouns, adjectives)
|
||||||
|
|
||||||
def _name_template(self, with_adjectives: bool, with_nouns: bool) -> str:
|
def _name_template(self, with_adjectives: bool, with_nouns: bool) -> str:
|
||||||
num_properties = len(self.properties)
|
num_properties = len(self.properties)
|
||||||
options = []
|
options = []
|
||||||
if with_nouns and not with_adjectives:
|
if with_nouns and not with_adjectives:
|
||||||
options.append(('{name} of {nouns}', 0.5))
|
options.append(("{name} of {nouns}", 0.5))
|
||||||
if with_adjectives and not with_nouns:
|
if with_adjectives and not with_nouns:
|
||||||
options.append(('{adjectives} {name}', 0.5))
|
options.append(("{adjectives} {name}", 0.5))
|
||||||
if with_nouns and with_adjectives:
|
if with_nouns and with_adjectives:
|
||||||
if num_properties == 1:
|
if num_properties == 1:
|
||||||
options.append(('{adjectives} {name} of {nouns}', 1.0))
|
options.append(("{adjectives} {name} of {nouns}", 1.0))
|
||||||
elif num_properties > 1:
|
elif num_properties > 1:
|
||||||
options.extend([
|
options.extend(
|
||||||
('{adjectives} {name} of {nouns}', 1.0),
|
[
|
||||||
('{name} of {adjectives} {nouns}', 0.5),
|
("{adjectives} {name} of {nouns}", 1.0),
|
||||||
])
|
("{name} of {adjectives} {nouns}", 0.5),
|
||||||
|
]
|
||||||
|
)
|
||||||
return WeightedSet(*options).random()
|
return WeightedSet(*options).random()
|
||||||
|
|
||||||
def _random_descriptors(self):
|
def _random_descriptors(self):
|
||||||
|
@ -64,7 +65,6 @@ class Weapon(types.Item):
|
||||||
|
|
||||||
seen_nouns = dict()
|
seen_nouns = dict()
|
||||||
for prop_name in set(list(nouns.keys()) + list(adjectives.keys())):
|
for prop_name in set(list(nouns.keys()) + list(adjectives.keys())):
|
||||||
|
|
||||||
if prop_name in nouns and prop_name not in adjectives:
|
if prop_name in nouns and prop_name not in adjectives:
|
||||||
if prop_name not in seen_nouns:
|
if prop_name not in seen_nouns:
|
||||||
add_word(prop_name, random_nouns, nouns)
|
add_word(prop_name, random_nouns, nouns)
|
||||||
|
@ -89,8 +89,8 @@ class Weapon(types.Item):
|
||||||
add_word(prop_name, random_nouns, nouns)
|
add_word(prop_name, random_nouns, nouns)
|
||||||
add_word(prop_name, random_adjectives, adjectives)
|
add_word(prop_name, random_adjectives, adjectives)
|
||||||
|
|
||||||
random_nouns = ' and '.join(random_nouns)
|
random_nouns = " and ".join(random_nouns)
|
||||||
random_adjectives = ' '.join(random_adjectives)
|
random_adjectives = " ".join(random_adjectives)
|
||||||
return (random_nouns, random_adjectives)
|
return (random_nouns, random_adjectives)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
@ -109,11 +109,11 @@ class Weapon(types.Item):
|
||||||
@property
|
@property
|
||||||
def to_hit(self):
|
def to_hit(self):
|
||||||
bonus_val = 0
|
bonus_val = 0
|
||||||
bonus_dice = ''
|
bonus_dice = ""
|
||||||
if not hasattr(self, 'properties'):
|
if not hasattr(self, "properties"):
|
||||||
return ''
|
return ""
|
||||||
for prop in self.properties.values():
|
for prop in self.properties.values():
|
||||||
mod = getattr(prop, 'to_hit', None)
|
mod = getattr(prop, "to_hit", None)
|
||||||
if not mod:
|
if not mod:
|
||||||
continue
|
continue
|
||||||
if type(mod) is int:
|
if type(mod) is int:
|
||||||
|
@ -124,24 +124,22 @@ class Weapon(types.Item):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def damage_dice(self):
|
def damage_dice(self):
|
||||||
if not hasattr(self, 'properties'):
|
if not hasattr(self, "properties"):
|
||||||
return ''
|
return ""
|
||||||
dmg = {
|
dmg = {self.damage_type: str(self.damage) or ""}
|
||||||
self.damage_type: str(self.damage) or ''
|
|
||||||
}
|
|
||||||
|
|
||||||
for prop in self.properties.values():
|
for prop in self.properties.values():
|
||||||
mod = getattr(prop, 'damage', None)
|
mod = getattr(prop, "damage", None)
|
||||||
if not mod:
|
if not mod:
|
||||||
continue
|
continue
|
||||||
key = str(prop.damage_type)
|
key = str(prop.damage_type)
|
||||||
this_damage = dmg.get(key, '')
|
this_damage = dmg.get(key, "")
|
||||||
if this_damage:
|
if this_damage:
|
||||||
dmg[key] = f"{this_damage}+{mod}"
|
dmg[key] = f"{this_damage}+{mod}"
|
||||||
else:
|
else:
|
||||||
dmg[key] = mod
|
dmg[key] = mod
|
||||||
|
|
||||||
return ' + '.join([f"{v} {k}" for k, v in dmg.items()])
|
return " + ".join([f"{v} {k}" for k, v in dmg.items()])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self):
|
def summary(self):
|
||||||
|
@ -152,20 +150,28 @@ class Weapon(types.Item):
|
||||||
"""
|
"""
|
||||||
Format the item properties as nested bullet lists.
|
Format the item properties as nested bullet lists.
|
||||||
"""
|
"""
|
||||||
props = ', '.join(self.get('properties', dict()).keys())
|
props = ", ".join(self.get("properties", dict()).keys())
|
||||||
return "\n".join([
|
return "\n".join(
|
||||||
f"{self.name}",
|
[
|
||||||
f" * {self.rarity.rarity} {self.category} weapon ({props})",
|
f"{self.name}",
|
||||||
f" * {self.summary}",
|
f" * {self.rarity.rarity} {self.category} weapon ({props})",
|
||||||
f"\n{self.description}\n"
|
f" * {self.summary}",
|
||||||
])
|
f"\n{self.description}\n",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
sha1bytes = hashlib.sha1(''.join([
|
sha1bytes = hashlib.sha1(
|
||||||
self._name, self.to_hit, self.damage_dice,
|
"".join(
|
||||||
]).encode())
|
[
|
||||||
return base64.urlsafe_b64encode(sha1bytes.digest()).decode('ascii')[:10]
|
self._name,
|
||||||
|
self.to_hit,
|
||||||
|
self.damage_dice,
|
||||||
|
]
|
||||||
|
).encode()
|
||||||
|
)
|
||||||
|
return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
|
||||||
|
|
||||||
|
|
||||||
class WeaponGenerator(types.ItemGenerator):
|
class WeaponGenerator(types.ItemGenerator):
|
||||||
|
@ -182,16 +188,16 @@ class WeaponGenerator(types.ItemGenerator):
|
||||||
def random_properties(self) -> dict:
|
def random_properties(self) -> dict:
|
||||||
# add missing base weapon defaults (TODO: update the sources)
|
# add missing base weapon defaults (TODO: update the sources)
|
||||||
item = super().random_properties()
|
item = super().random_properties()
|
||||||
item['targets'] = 1
|
item["targets"] = 1
|
||||||
if item['category'] == 'Martial':
|
if item["category"] == "Martial":
|
||||||
if not item['range']:
|
if not item["range"]:
|
||||||
item['range'] = ''
|
item["range"] = ""
|
||||||
return item
|
return item
|
||||||
|
|
||||||
# handlers for extra properties
|
# handlers for extra properties
|
||||||
|
|
||||||
def get_enchantment(self, **attrs) -> dict:
|
def get_enchantment(self, **attrs) -> dict:
|
||||||
prop = types.ENCHANTMENT.random()
|
prop = types.ENCHANTMENT.random()
|
||||||
prop['adjectives'] = random_from_csv(prop['adjectives'])
|
prop["adjectives"] = random_from_csv(prop["adjectives"])
|
||||||
prop['nouns'] = random_from_csv(prop['nouns'])
|
prop["nouns"] = random_from_csv(prop["nouns"])
|
||||||
return prop
|
return prop
|
||||||
|
|
Loading…
Reference in New Issue
Block a user