automatic formatting

This commit is contained in:
evilchili 2023-12-29 19:24:26 -08:00
parent 51ecf83357
commit 5b4c26ecf2
4 changed files with 186 additions and 207 deletions

View File

@ -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:

View File

@ -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

View File

@ -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 = []

View File

@ -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