refactoring
This commit is contained in:
parent
6d45ba9c4b
commit
86c2fce87d
|
@ -11,7 +11,8 @@ from rich.logging import RichHandler
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from dnd_item.types import WeaponGenerator, RollTable
|
from dnd_item.types import RollTable
|
||||||
|
from dnd_item.weapons import WeaponGenerator
|
||||||
from dnd_item import five_e
|
from dnd_item import five_e
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
|
@ -16,32 +16,32 @@ metadata:
|
||||||
necrotic: 1.0
|
necrotic: 1.0
|
||||||
radiant: 1.0
|
radiant: 1.0
|
||||||
fire:
|
fire:
|
||||||
- flames, fire
|
- 'flames, fire'
|
||||||
- flaming, burning
|
- 'flaming, burning'
|
||||||
cold:
|
cold:
|
||||||
- ice
|
- 'ice, cold'
|
||||||
- freezing, frosty
|
- 'freezing, frosty'
|
||||||
acid:
|
acid:
|
||||||
- acid
|
- 'acid'
|
||||||
- acidic, caustic
|
- 'acidic, caustic'
|
||||||
thunder:
|
thunder:
|
||||||
- thunder
|
- 'thunder'
|
||||||
- thundering,booming
|
- 'thundering,booming'
|
||||||
psychic:
|
psychic:
|
||||||
- mind
|
- 'mind, screams'
|
||||||
- psychic
|
- 'psychic, screaming'
|
||||||
poison:
|
poison:
|
||||||
- poison
|
- 'poison, toxins, venom'
|
||||||
- poisonous,toxic
|
- 'poisonous, toxic, venomous'
|
||||||
lightning:
|
lightning:
|
||||||
- lightning,sparks,shocks
|
- 'lightning,sparks,shocks'
|
||||||
- lightning,shocking,sparking
|
- 'lightning,shocking,sparking'
|
||||||
force:
|
force:
|
||||||
- force
|
- 'force'
|
||||||
- forceful
|
- 'forceful'
|
||||||
necrotic:
|
necrotic:
|
||||||
- necrosis, darkness, shadows
|
- 'necrosis, darkness, shadows'
|
||||||
- necrotic, dark, unholy
|
- 'necrotic, dark, unholy'
|
||||||
radiant:
|
radiant:
|
||||||
- radiance, shining
|
- 'radiance, shining'
|
||||||
- radiant, holy
|
- 'radiant, holy'
|
||||||
|
|
24
dnd_item/sources/properties_base.yaml
Normal file
24
dnd_item/sources/properties_base.yaml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
metadata:
|
||||||
|
headers:
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
light:
|
||||||
|
- 'A light weapon is small and easy to handle, making it ideal for use when fighting with two weapons.'
|
||||||
|
thrown:
|
||||||
|
- ''
|
||||||
|
heavy:
|
||||||
|
- ''
|
||||||
|
special:
|
||||||
|
- ''
|
||||||
|
two-handed:
|
||||||
|
- ''
|
||||||
|
versatile:
|
||||||
|
- ''
|
||||||
|
finesse:
|
||||||
|
- ''
|
||||||
|
ammunition:
|
||||||
|
- ''
|
||||||
|
reach:
|
||||||
|
- ''
|
||||||
|
loading:
|
||||||
|
- ''
|
|
@ -4,9 +4,19 @@ metadata:
|
||||||
- nouns
|
- nouns
|
||||||
- adjectives
|
- adjectives
|
||||||
- description
|
- description
|
||||||
|
- damage_type
|
||||||
|
- damage
|
||||||
|
- to_hit
|
||||||
|
- override_damage_type
|
||||||
|
- override_damage
|
||||||
- type
|
- type
|
||||||
light:
|
'smiles':
|
||||||
- 'light'
|
- 'smiles, grins, joy, smirks'
|
||||||
- 'light'
|
- 'smiling, grinning, joyfulness, smirking'
|
||||||
- 'A light weapon is small and easy to handle, making it ideal for use when fighting with two weapons.'
|
- 'While this {this.type} is equipped, you are forced to smile.'
|
||||||
- weapon
|
- null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- item
|
||||||
|
|
|
@ -4,5 +4,23 @@ metadata:
|
||||||
- nouns
|
- nouns
|
||||||
- adjectives
|
- adjectives
|
||||||
- description
|
- description
|
||||||
|
- damage_type
|
||||||
|
- damage
|
||||||
|
- to_hit
|
||||||
- type
|
- type
|
||||||
legendary:
|
enchanted:
|
||||||
|
- '{enchantment.nouns}'
|
||||||
|
- '{enchantment.adjectives}'
|
||||||
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
|
- '{enchantment.damage_type}'
|
||||||
|
- 2d6
|
||||||
|
- 0
|
||||||
|
- weapon
|
||||||
|
magical:
|
||||||
|
- 'striking, smacking'
|
||||||
|
- '+3'
|
||||||
|
- This magical weapon grants +3 to attack and damage rolls.
|
||||||
|
- '{damage_type}'
|
||||||
|
- 3
|
||||||
|
- 3
|
||||||
|
- weapon
|
||||||
|
|
|
@ -9,18 +9,18 @@ metadata:
|
||||||
- to_hit
|
- to_hit
|
||||||
- type
|
- type
|
||||||
enchanted:
|
enchanted:
|
||||||
- '{enchanted.nouns}'
|
- '{enchantment.nouns}'
|
||||||
- '{enchanted.adjectives}'
|
- '{enchantment.adjectives}'
|
||||||
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
- '{enchanted.damage_type}'
|
- '{enchantment.damage_type}'
|
||||||
- 1d6
|
- 1d6
|
||||||
- 0
|
- 0
|
||||||
- weapon
|
- weapon
|
||||||
magical:
|
magical:
|
||||||
- magic
|
- 'striking, smacking'
|
||||||
- '+2'
|
- '+2'
|
||||||
- This magical weapon grants +2 to attack and damage rolls.
|
- This magical weapon grants +2 to attack and damage rolls.
|
||||||
- '{base.damage_type}'
|
- '{damage_type}'
|
||||||
- 2
|
- 2
|
||||||
- 2
|
- 2
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -7,20 +7,38 @@ metadata:
|
||||||
- damage_type
|
- damage_type
|
||||||
- damage
|
- damage
|
||||||
- to_hit
|
- to_hit
|
||||||
|
- override_damage_type
|
||||||
|
- override_damage
|
||||||
- type
|
- type
|
||||||
|
'elemental damage':
|
||||||
|
- '{enchantment.nouns}'
|
||||||
|
- '{enchantment.adjectives}'
|
||||||
|
- 'This magical {name} deals {this.damage_type} damage.'
|
||||||
|
- '{enchantment.damage_type}'
|
||||||
|
- 0
|
||||||
|
- 0
|
||||||
|
- '{enchantment.damage_type}'
|
||||||
|
- null
|
||||||
|
- weapon
|
||||||
enchanted:
|
enchanted:
|
||||||
- '{enchanted.nouns}'
|
- '{enchantment.nouns}'
|
||||||
- '{enchanted.adjectives}'
|
- '{enchantment.adjectives}'
|
||||||
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
- '{enchanted.damage_type}'
|
- '{enchantment.damage_type}'
|
||||||
- 1d4
|
- 1d4
|
||||||
- 0
|
- 0
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
- weapon
|
- weapon
|
||||||
magical:
|
magical:
|
||||||
- magic
|
- 'striking, smacking'
|
||||||
- '+1'
|
- '+1'
|
||||||
- This magical weapon grants +1 to attack and damage rolls.
|
- This magical weapon grants +1 to attack and damage rolls.
|
||||||
- '{base.damage_type}'
|
- '{damage_type}'
|
||||||
- 1
|
- 1
|
||||||
- 1
|
- 1
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -9,18 +9,18 @@ metadata:
|
||||||
- to_hit
|
- to_hit
|
||||||
- type
|
- type
|
||||||
enchanted:
|
enchanted:
|
||||||
- '{enchanted.nouns}'
|
- '{enchantment.nouns}'
|
||||||
- '{enchanted.adjectives}'
|
- '{enchantment.adjectives}'
|
||||||
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
- '{enchanted.damage_type}'
|
- '{enchantment.damage_type}'
|
||||||
- 1d8
|
- 1d8
|
||||||
- 0
|
- 0
|
||||||
- weapon
|
- weapon
|
||||||
magical:
|
magical:
|
||||||
- magic
|
- 'striking, smacking'
|
||||||
- '+3'
|
- '+3'
|
||||||
- This magical weapon grants +3 to attack and damage rolls.
|
- This magical weapon grants +3 to attack and damage rolls.
|
||||||
- '{base.damage_type}'
|
- '{damage_type}'
|
||||||
- 3
|
- 3
|
||||||
- 3
|
- 3
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -39,7 +39,8 @@ metadata:
|
||||||
uncommon: 0.05
|
uncommon: 0.05
|
||||||
rare: 0.5
|
rare: 0.5
|
||||||
very rare: 0.5
|
very rare: 0.5
|
||||||
legendary: 0.3
|
#legendary: 0.3
|
||||||
|
legendary: 0.0
|
||||||
common:
|
common:
|
||||||
- 0
|
- 0
|
||||||
uncommon:
|
uncommon:
|
||||||
|
|
|
@ -29,7 +29,7 @@ Blowgun:
|
||||||
- 25/100
|
- 25/100
|
||||||
- ''
|
- ''
|
||||||
- '1000'
|
- '1000'
|
||||||
- ammmunition, loading
|
- ammunition, loading
|
||||||
Club:
|
Club:
|
||||||
- simple
|
- simple
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -139,7 +139,7 @@ Hand crossbow:
|
||||||
- 30/120
|
- 30/120
|
||||||
- ''
|
- ''
|
||||||
- '7500'
|
- '7500'
|
||||||
- ammmunition, light, loading
|
- ammunition, light, loading
|
||||||
Handaxe:
|
Handaxe:
|
||||||
- simple
|
- simple
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -159,7 +159,7 @@ Heavy crossbow:
|
||||||
- 100/400
|
- 100/400
|
||||||
- ''
|
- ''
|
||||||
- '5000'
|
- '5000'
|
||||||
- ammmunition, heavy, loading, two-handed
|
- ammunition, heavy, loading, two-handed
|
||||||
Hooked shortspear:
|
Hooked shortspear:
|
||||||
- martial
|
- martial
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -179,7 +179,7 @@ Hoopak:
|
||||||
- 40/160
|
- 40/160
|
||||||
- ''
|
- ''
|
||||||
- '10'
|
- '10'
|
||||||
- ammmunition, finesse, special, two-handed
|
- ammunition, finesse, special, two-handed
|
||||||
Javelin:
|
Javelin:
|
||||||
- simple
|
- simple
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -209,7 +209,7 @@ Light crossbow:
|
||||||
- 80/320
|
- 80/320
|
||||||
- ''
|
- ''
|
||||||
- '2500'
|
- '2500'
|
||||||
- ammmunition, loading, two-handed
|
- ammunition, loading, two-handed
|
||||||
Light hammer:
|
Light hammer:
|
||||||
- simple
|
- simple
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -229,7 +229,7 @@ Light repeating crossbow:
|
||||||
- 40/160
|
- 40/160
|
||||||
- ''
|
- ''
|
||||||
- ''
|
- ''
|
||||||
- ammmunition, two-handed
|
- ammunition, two-handed
|
||||||
Longbow:
|
Longbow:
|
||||||
- martial
|
- martial
|
||||||
- Ranged
|
- Ranged
|
||||||
|
@ -239,7 +239,7 @@ Longbow:
|
||||||
- 150/600
|
- 150/600
|
||||||
- ''
|
- ''
|
||||||
- '5000'
|
- '5000'
|
||||||
- ammmunition, heavy, two-handed
|
- ammunition, heavy, two-handed
|
||||||
Longsword:
|
Longsword:
|
||||||
- martial
|
- martial
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -339,7 +339,7 @@ Shortbow:
|
||||||
- 80/320
|
- 80/320
|
||||||
- ''
|
- ''
|
||||||
- '2500'
|
- '2500'
|
||||||
- ammmunition, two-handed
|
- ammunition, two-handed
|
||||||
Shortsword:
|
Shortsword:
|
||||||
- martial
|
- martial
|
||||||
- Martial
|
- Martial
|
||||||
|
@ -369,7 +369,7 @@ Sling:
|
||||||
- 30/120
|
- 30/120
|
||||||
- ''
|
- ''
|
||||||
- '10'
|
- '10'
|
||||||
- ammmunition
|
- ammunition
|
||||||
Spear:
|
Spear:
|
||||||
- simple
|
- simple
|
||||||
- Martial
|
- Martial
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import random
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from random_sets.sets import WeightedSet, DataSourceSet
|
from random_sets.sets import WeightedSet, DataSourceSet
|
||||||
|
@ -14,7 +17,8 @@ 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 = {
|
PROPERTIES_BY_RARITY = {
|
||||||
|
'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')),
|
||||||
|
@ -24,39 +28,28 @@ PROPERTIES = {
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AttributeDict:
|
class AttributeDict(Mapping):
|
||||||
_attrs: field(default_factory=dict) = None
|
attributes: field(default_factory=dict)
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
"""
|
"""
|
||||||
Look up attributes in the _attrs dict first, then fall back to the default.
|
Look up attributes in the _attrs dict first, then fall back to the default.
|
||||||
"""
|
"""
|
||||||
if attr in self._attrs:
|
if attr in self.attributes:
|
||||||
return self._attrs[attr]
|
return self.attributes[attr]
|
||||||
return self.__getattribute__(attr)
|
return self.__getattribute__(attr)
|
||||||
|
|
||||||
def __str__(self):
|
def __len__(self):
|
||||||
return self._flatten(self)
|
return len(self.attributes)
|
||||||
|
|
||||||
def __repr__(self):
|
def __getitem__(self, key):
|
||||||
return str(self)
|
return self.attributes[key]
|
||||||
|
|
||||||
def _flatten(self, obj, prefix=None):
|
def __iter__(self):
|
||||||
if prefix == '':
|
return iter(self.attributes)
|
||||||
prefix = f"{self.__class__.__name__}."
|
|
||||||
else:
|
|
||||||
prefix = ''
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
for (k, v) in obj._attrs.items():
|
|
||||||
if type(v) is AttributeDict:
|
|
||||||
lines.append(self._flatten(v, prefix=f"{prefix}{k}"))
|
|
||||||
else:
|
|
||||||
lines.append(f"{prefix}{k} = {v}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, **kwargs: dict):
|
def from_dict(cls, kwargs: dict):
|
||||||
"""
|
"""
|
||||||
Create a new AttributeDict object using keyword arguments. Dicts are
|
Create a new AttributeDict object using keyword arguments. Dicts are
|
||||||
recursively converted to AttributeDict objects; everything else is
|
recursively converted to AttributeDict objects; everything else is
|
||||||
|
@ -64,8 +57,8 @@ class AttributeDict:
|
||||||
"""
|
"""
|
||||||
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(_attrs=attrs)
|
return cls(attributes=attrs)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -73,192 +66,155 @@ class Item(AttributeDict):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
_name: str = None
|
_name: str = None
|
||||||
rarity: str = None
|
|
||||||
_template: str = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
return self._template.format(name=self._name, **self._attrs).format(**self._attrs).title()
|
"""
|
||||||
|
The item's name. This is a handy property for subclassers to override.
|
||||||
@property
|
"""
|
||||||
def properties(self):
|
return self._name
|
||||||
return self._attrs['properties']._attrs
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
txt = []
|
desc = "\n".join([k.title() + ". " + v.description for k, v in self.get('properties', {}).items()])
|
||||||
for k, v in self.properties.items():
|
return desc.format(**self)
|
||||||
txt.append(k.title() + ". " + v.description.format(
|
|
||||||
this=v,
|
|
||||||
name=self.name,
|
|
||||||
**self._attrs,
|
|
||||||
).format(name=self.name, **self._attrs))
|
|
||||||
return "\n".join(txt)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def details(self):
|
|
||||||
"""
|
|
||||||
Format the item attributes as nested bullet lists.
|
|
||||||
"""
|
|
||||||
return "\n".join([
|
|
||||||
f"{self.name} ({self.rarity['rarity']}):",
|
|
||||||
self.description,
|
|
||||||
"",
|
|
||||||
self._flatten(self.base, prefix=None)
|
|
||||||
])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _properties_as_text(self):
|
|
||||||
def attrs_to_lines(item, prefix: str = ''):
|
|
||||||
for (k, v) in item._attrs.items():
|
|
||||||
if type(v) is AttributeDict:
|
|
||||||
yield from attrs_to_lines(v, prefix=f"{k}.")
|
|
||||||
continue
|
|
||||||
yield f" * {prefix}{k}: {v}"
|
|
||||||
return "\n".join(["Properties:"] + sorted(list(attrs_to_lines(self))))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, **kwargs: dict):
|
def from_dict(cls, attrs: dict):
|
||||||
"""
|
"""
|
||||||
Create a new Item object using keyword arguments. Dicts are recursively
|
Create a new Item object using keyword arguments. Dicts are recursively
|
||||||
converted to Item objects; everything else is passed as-is.
|
converted to Item objects; everything else is passed as-is.
|
||||||
|
|
||||||
The "name" and "template" arguments, if supplied, are removed from the
|
|
||||||
keyword arguments and used to populate those attributes directly; all
|
|
||||||
other attributes will be added to the _attrs dict so they can be
|
|
||||||
accessed directly through dotted attribute notation.
|
|
||||||
"""
|
|
||||||
name = kwargs.pop('name') if 'name' in kwargs else '(unnamed)'
|
|
||||||
rarity = kwargs.pop('rarity') if 'rarity' in kwargs else 'common'
|
|
||||||
template = kwargs.pop('template') if 'template' in kwargs else '{name}'
|
|
||||||
attrs = {}
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
attrs[k] = AttributeDict.from_dict(**v)if type(v) is dict else v
|
|
||||||
return cls(_name=name, rarity=rarity, _template=template, _attrs=attrs)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Weapon(Item):
|
|
||||||
"""
|
|
||||||
An Item class representing a weapon with the following attributes:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
# delay processing the 'properties' attribute until after the other
|
||||||
def to_hit(self):
|
# attributes, because they may contain references to those attributes.
|
||||||
bonus_val = 0
|
properties = attrs.pop('properties', None)
|
||||||
bonus_dice = ''
|
|
||||||
for prop in self.properties.values():
|
|
||||||
mod = getattr(prop, 'to_hit', None)
|
|
||||||
if not mod:
|
|
||||||
continue
|
|
||||||
if type(mod) is int:
|
|
||||||
bonus_val += mod
|
|
||||||
elif type(mod) is str:
|
|
||||||
bonus_dice += f"+{mod}"
|
|
||||||
return f"+{bonus_val}{bonus_dice}"
|
|
||||||
|
|
||||||
@property
|
attributes = dict()
|
||||||
def damage_dice(self):
|
|
||||||
dmg = dict()
|
# recursively locate and populate template strings
|
||||||
dmg[self.base.damage_type] = self.base.damage or ''
|
def _format(obj, this=None):
|
||||||
for prop in self.properties.values():
|
|
||||||
mod = getattr(prop, 'damage', None)
|
# enables use of the 'this' keyword to refer to the current context
|
||||||
if not mod:
|
# in a template. Refer to the enchantment sources for an example.
|
||||||
continue
|
if this:
|
||||||
key = str(prop.damage_type).format(**self._attrs).title()
|
this = AttributeDict.from_dict(this)
|
||||||
if key not in dmg:
|
|
||||||
dmg[key] = str(mod)
|
# dicts and lists are descended into
|
||||||
|
if type(obj) is dict:
|
||||||
|
return AttributeDict.from_dict(dict(
|
||||||
|
(key, _format(val, this=obj)) for key, val in obj.items()
|
||||||
|
))
|
||||||
|
if type(obj) is list:
|
||||||
|
return [_format(o, this=this) for o in obj]
|
||||||
|
|
||||||
|
# Strings are formatted wth values from attributes and this. Using
|
||||||
|
# attributes is important here, so that values containing template
|
||||||
|
# strings are processed before they are referenced.
|
||||||
|
if type(obj) is str:
|
||||||
|
return obj.format(**attributes, this=this)
|
||||||
|
|
||||||
|
# Any type other than dict, list, and string is returned unaltered.
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# step through the supplied attributes and format each member.
|
||||||
|
for k, v in attrs.items():
|
||||||
|
if type(v) is dict:
|
||||||
|
attributes[k] = AttributeDict.from_dict(_format(v))
|
||||||
else:
|
else:
|
||||||
dmg[key] += f"+{mod}"
|
attributes[k] = _format(v)
|
||||||
|
if properties:
|
||||||
|
attributes['properties'] = AttributeDict.from_dict(_format(properties))
|
||||||
|
for prop in attributes['properties'].values():
|
||||||
|
overrides = [k for k in prop.attributes.keys() if k.startswith('override_')]
|
||||||
|
for o in overrides:
|
||||||
|
if prop.attributes[o]:
|
||||||
|
attributes[o.replace('override_', '')] = prop.attributes[o]
|
||||||
|
|
||||||
return ' + '.join([f"{v} {k}" for k, v in dmg.items()])
|
# 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,
|
||||||
|
# which may require naming semantics that cannot be resolved at instantiation time.
|
||||||
|
_name = attributes['name']
|
||||||
|
del attributes['name']
|
||||||
|
|
||||||
@property
|
# At this point, attributes is a dictionary with members of multiple
|
||||||
def summary(self):
|
# types, but every dict member has been converted to an AttributeDict,
|
||||||
return f"{self.to_hit} to hit, {self.base.range} ft., {self.base.targets} targets. {self.damage_dice}"
|
# and all template strings in the object have been formatted. Return an
|
||||||
|
# instance of the Item class using these formatted attributes.
|
||||||
@property
|
return cls(
|
||||||
def details(self):
|
_name=_name,
|
||||||
"""
|
attributes=attributes
|
||||||
Format the item attributes as nested bullet lists.
|
)
|
||||||
"""
|
|
||||||
return "\n".join([
|
|
||||||
f"{self.name}",
|
|
||||||
f" * {self.rarity['rarity']} {self.base.category} weapon ({self.base.properties})",
|
|
||||||
f" * {self.summary}",
|
|
||||||
f"\n{self.description}\n" if self.description else "",
|
|
||||||
"----",
|
|
||||||
self._flatten(self.base, prefix=None)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class ItemGenerator:
|
class ItemGenerator:
|
||||||
"""
|
"""
|
||||||
Generate randomized instances of Item objects.
|
|
||||||
|
|
||||||
The main interfaces is the random() method, which will generate one or
|
|
||||||
more random Item instances by selecting random values from the supplied
|
|
||||||
WeightedSets. This allows for fully-controllable frequency distributions.
|
|
||||||
|
|
||||||
You probably want to subclass this class, in order to provide sensible
|
|
||||||
defaults, and control what attributes are available; refer to the
|
|
||||||
subclasses elsewhere in this module.
|
|
||||||
|
|
||||||
The class requires two arguments to instantiate:
|
|
||||||
* templates - a WeightedSet of format strings for item names; and
|
|
||||||
* types - a WeightedSet of item types to be selected from at random.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> ig = ItemGenerator(
|
|
||||||
? templates=WeightedSet("{type.name}", 1.0),
|
|
||||||
? types=WeightedSet(
|
|
||||||
? ({'name': 'ring'}, 1.0),
|
|
||||||
? ({'name': 'hat'}, 1.0),
|
|
||||||
? ),
|
|
||||||
? )
|
|
||||||
>>> ig.random(3).name
|
|
||||||
['hat', 'hat', 'ring']
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create instances of this class. Subclasses may wish to override this.
|
|
||||||
item_class = Item
|
item_class = Item
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, bases: WeightedSet, rarity: WeightedSet, properties_by_rarity: dict):
|
||||||
self,
|
|
||||||
templates: WeightedSet,
|
|
||||||
bases: WeightedSet,
|
|
||||||
rarity: WeightedSet = RARITY,
|
|
||||||
properties: WeightedSet = PROPERTIES,
|
|
||||||
):
|
|
||||||
self.bases = bases
|
self.bases = bases
|
||||||
self.templates = templates
|
|
||||||
self.rarity = rarity
|
self.rarity = rarity
|
||||||
self.properties = properties
|
self.properties_by_rarity = properties_by_rarity
|
||||||
|
|
||||||
def random_attributes(self) -> dict:
|
def _property_count_by_rarity(self, rarity: str) -> int:
|
||||||
"""
|
property_count_by_rarity = {
|
||||||
Select random values from the available attributes. These values will
|
'common': WeightedSet((1, 0.1), (0, 1.0)),
|
||||||
be passed as arguments to the Item constructor.
|
'uncommon': WeightedSet((1, 1.0)),
|
||||||
|
'rare': WeightedSet((1, 1.0), (2, 0.5)),
|
||||||
If you subclass this class and override this method, be sure that
|
'very rare': WeightedSet((1, 0.5), (2, 1.0)),
|
||||||
whatever attributes are referenced in your template strings are
|
'legendary': WeightedSet((2, 1.0), (3, 1.0)),
|
||||||
available as properties here. For example, if you have a subclass with
|
|
||||||
the template:
|
|
||||||
|
|
||||||
WeightedSet("{this.color} {that.thing}", 1.0)
|
|
||||||
|
|
||||||
This method must return a dict that includes both this and that, and
|
|
||||||
each of them must be either Item instances or dictionaries.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'template': self.templates.random(),
|
|
||||||
'base': self.bases.random(),
|
|
||||||
'rarity': self.rarity.random(),
|
|
||||||
'properties': self.properties.random(),
|
|
||||||
}
|
}
|
||||||
|
return min(
|
||||||
|
property_count_by_rarity[rarity].random(),
|
||||||
|
len(self.properties_by_rarity[rarity].members)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_requirements(self, item) -> set:
|
||||||
|
pat = re.compile(r'{([^\.\}]+)')
|
||||||
|
|
||||||
|
def getreqs(obj):
|
||||||
|
if type(obj) is dict:
|
||||||
|
for val in obj.values():
|
||||||
|
yield from getreqs(val)
|
||||||
|
elif type(obj) is list:
|
||||||
|
yield from [getreqs(o) for o in obj]
|
||||||
|
elif type(obj) is str:
|
||||||
|
matches = pat.findall(obj)
|
||||||
|
if matches:
|
||||||
|
yield from matches
|
||||||
|
|
||||||
|
return set(getreqs(item))
|
||||||
|
|
||||||
|
def random_properties(self) -> dict:
|
||||||
|
item = self.bases.random()
|
||||||
|
item['rarity'] = self.rarity.random()
|
||||||
|
|
||||||
|
properties = {}
|
||||||
|
num_properties = self._property_count_by_rarity(item['rarity']['rarity'])
|
||||||
|
while len(properties) != num_properties:
|
||||||
|
thisprop = self.properties_by_rarity[item['rarity']['rarity']].random()
|
||||||
|
properties[thisprop['name']] = thisprop
|
||||||
|
|
||||||
|
# add properties from the base item (versatile, thrown, artifact..)
|
||||||
|
for name in item.pop('properties', '').split(','):
|
||||||
|
name = name.strip()
|
||||||
|
if name:
|
||||||
|
properties[name] = self.properties_by_rarity['base'].source.as_dict()[name]
|
||||||
|
|
||||||
|
item['properties'] = properties
|
||||||
|
|
||||||
|
# look for template strings that reference item attributes which do not yet exist.
|
||||||
|
# Add anything that is missing via a callback.
|
||||||
|
predefined = list(item.keys()) + ['this', '_name']
|
||||||
|
for requirement in [r for r in self.get_requirements(item) if r not in predefined]:
|
||||||
|
try:
|
||||||
|
item[requirement] = getattr(self, f'get_{requirement}')(**item)
|
||||||
|
except AttributeError:
|
||||||
|
logging.error("{item['name']} requires {self.__class__.__name__} to have a get_{requirement}() method.")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
def random(self, count: int = 1, challenge_rating: int = 0) -> list:
|
def random(self, count: int = 1, challenge_rating: int = 0) -> list:
|
||||||
"""
|
"""
|
||||||
|
@ -282,97 +238,10 @@ class ItemGenerator:
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
items.append(self.item_class.from_dict(**self.random_attributes()))
|
items.append(self.item_class.from_dict(self.random_properties()))
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
class WeaponGenerator(ItemGenerator):
|
|
||||||
item_class = Weapon
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
templates: WeightedSet = WeightedSet(('{base.name}', 1.0),),
|
|
||||||
bases: WeightedSet = WEAPON_TYPES,
|
|
||||||
rarity: WeightedSet = RARITY,
|
|
||||||
enchanted: WeightedSet = ENCHANTMENT,
|
|
||||||
properties: WeightedSet = PROPERTIES,
|
|
||||||
):
|
|
||||||
super().__init__(bases=bases, templates=None, rarity=rarity, properties=properties)
|
|
||||||
self.enchanted = enchanted
|
|
||||||
self.property_count_by_rarity = {
|
|
||||||
'common': WeightedSet((0, 1.0)),
|
|
||||||
'uncommon': WeightedSet((1, 1.0)),
|
|
||||||
'rare': WeightedSet((1, 1.0), (2, 0.1)),
|
|
||||||
'very rare': WeightedSet((1, 1.0), (2, 1.0)),
|
|
||||||
'legendary': WeightedSet((2, 1.0), (3, 1.0)),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_template(self, attrs) -> WeightedSet:
|
|
||||||
if not attrs['properties']:
|
|
||||||
return '{base.name}'
|
|
||||||
options = []
|
|
||||||
if attrs['nouns']:
|
|
||||||
options.append(('{base.name} of {nouns}', 1.0))
|
|
||||||
if attrs['adjectives']:
|
|
||||||
options.append(('{adjectives} {base.name}', 1.0))
|
|
||||||
if attrs['nouns'] and attrs['adjectives']:
|
|
||||||
numprops = len(attrs['properties'].keys())
|
|
||||||
if numprops == 1:
|
|
||||||
options.append(('{adjectives} {base.name} of {nouns}', 0.1))
|
|
||||||
elif len(attrs['properties'].items()) > 1:
|
|
||||||
options.append(('{adjectives} {base.name} of {nouns}', 1.0))
|
|
||||||
options.append(('{base.name} of {adjectives} {nouns}', 1.0))
|
|
||||||
return WeightedSet(*options).random()
|
|
||||||
|
|
||||||
def random_attributes(self) -> dict:
|
|
||||||
"""
|
|
||||||
Select a random magical damage type and add it to our properties.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Select a random rarity. This will use the frequency distribution
|
|
||||||
# currently selectedon the rarity data source, which in turn will be
|
|
||||||
# set by self.random(), controllable by the caller.
|
|
||||||
attrs = dict(
|
|
||||||
base=self.bases.random(),
|
|
||||||
rarity=self.rarity.random(),
|
|
||||||
properties=dict(),
|
|
||||||
)
|
|
||||||
attrs['base']['targets'] = 1
|
|
||||||
|
|
||||||
if attrs['base']['category'] == 'Martial':
|
|
||||||
if not attrs['base']['range']:
|
|
||||||
attrs['base']['range'] = '5'
|
|
||||||
|
|
||||||
rarity = attrs['rarity']['rarity']
|
|
||||||
|
|
||||||
numprops = min(
|
|
||||||
self.property_count_by_rarity[rarity].random(),
|
|
||||||
len(self.properties[rarity].members)
|
|
||||||
)
|
|
||||||
|
|
||||||
while len(attrs['properties']) != numprops:
|
|
||||||
prop = self.properties[rarity].random()
|
|
||||||
if prop['name'] in attrs['properties']:
|
|
||||||
continue
|
|
||||||
attrs['properties'][prop['name']] = prop
|
|
||||||
|
|
||||||
# combine multiple property template arguments together
|
|
||||||
attrs['adjectives'] = []
|
|
||||||
attrs['nouns'] = []
|
|
||||||
for prop_name, prop in attrs['properties'].items():
|
|
||||||
attrs['adjectives'].append(prop['adjectives'])
|
|
||||||
attrs['nouns'].append(prop['nouns'])
|
|
||||||
if prop['name'] == 'enchanted':
|
|
||||||
attrs['enchanted'] = self.enchanted.random()
|
|
||||||
attrs['enchanted']['adjectives'] = random.choice(attrs['enchanted']['adjectives'].split(',')).strip()
|
|
||||||
attrs['enchanted']['nouns'] = random.choice(attrs['enchanted']['nouns'].split(',')).strip()
|
|
||||||
|
|
||||||
attrs['template'] = self.get_template(attrs)
|
|
||||||
attrs['adjectives'] = ' '.join(attrs['adjectives'])
|
|
||||||
attrs['nouns'] = ' '.join(attrs['nouns'])
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GeneratorSource:
|
class GeneratorSource:
|
||||||
generator: ItemGenerator
|
generator: ItemGenerator
|
||||||
|
@ -380,7 +249,13 @@ 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.name, item.rarity['rarity'], item.summary, item.base.properties])
|
(
|
||||||
|
item.rarity['sort_order'],
|
||||||
|
[
|
||||||
|
item.name, item.rarity['rarity'],
|
||||||
|
item.summary, ', '.join(item.get('properties', []))
|
||||||
|
]
|
||||||
|
)
|
||||||
for item in self.generator.random(count=count, challenge_rating=self.cr)
|
for item in self.generator.random(count=count, challenge_rating=self.cr)
|
||||||
)
|
)
|
||||||
return [v[1] for v in vals]
|
return [v[1] for v in vals]
|
||||||
|
|
176
dnd_item/weapons.py
Normal file
176
dnd_item/weapons.py
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from dnd_item import types
|
||||||
|
from random_sets.sets import WeightedSet, equal_weights
|
||||||
|
|
||||||
|
|
||||||
|
def random_from_csv(csv: str) -> str:
|
||||||
|
return random.choice(csv.split(',')).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class Weapon(types.Item):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _descriptors(self) -> tuple:
|
||||||
|
"""
|
||||||
|
Collect the nouns and adjectives from the properties of this item.
|
||||||
|
"""
|
||||||
|
nouns = dict()
|
||||||
|
adjectives = dict()
|
||||||
|
if not hasattr(self, 'properties'):
|
||||||
|
return (nouns, adjectives)
|
||||||
|
for prop_name, prop in self.properties.items():
|
||||||
|
if hasattr(prop, 'nouns'):
|
||||||
|
nouns[prop_name] = equal_weights(prop.nouns.split(','), blank=False)
|
||||||
|
if hasattr(prop, 'adjectives'):
|
||||||
|
adjectives[prop_name] = equal_weights(prop.adjectives.split(','), blank=False)
|
||||||
|
return (nouns, adjectives)
|
||||||
|
|
||||||
|
def _name_template(self, with_adjectives: bool, with_nouns: bool) -> str:
|
||||||
|
num_properties = len(self.properties)
|
||||||
|
options = []
|
||||||
|
if with_nouns:
|
||||||
|
options.append(('{name} of {nouns}', 0.5))
|
||||||
|
if with_adjectives:
|
||||||
|
options.append(('{adjectives} {name}', 0.5))
|
||||||
|
if with_nouns and with_adjectives:
|
||||||
|
if num_properties == 1:
|
||||||
|
options.append(('{adjectives} {name} of {nouns}', 1.0))
|
||||||
|
elif num_properties > 1:
|
||||||
|
options.extend([
|
||||||
|
('{adjectives} {name} of {nouns}', 1.0),
|
||||||
|
('{name} of {adjectives} {nouns}', 0.5),
|
||||||
|
])
|
||||||
|
return WeightedSet(*options).random()
|
||||||
|
|
||||||
|
def _random_descriptors(self):
|
||||||
|
"""
|
||||||
|
Select random nouns and adjectives from the object
|
||||||
|
"""
|
||||||
|
random_nouns = []
|
||||||
|
random_adjectives = []
|
||||||
|
|
||||||
|
(nouns, adjectives) = self._descriptors()
|
||||||
|
if not (nouns or adjectives):
|
||||||
|
return (random_nouns, random_adjectives)
|
||||||
|
|
||||||
|
def add_word(key, obj, source):
|
||||||
|
obj.append(source[key].random().strip())
|
||||||
|
|
||||||
|
for prop_name in set(list(nouns.keys()) + list(adjectives.keys())):
|
||||||
|
if prop_name in nouns and prop_name in adjectives:
|
||||||
|
val = random.random()
|
||||||
|
if val <= 0.4:
|
||||||
|
add_word(prop_name, random_nouns, nouns)
|
||||||
|
elif val <= 0.8:
|
||||||
|
add_word(prop_name, random_adjectives, adjectives)
|
||||||
|
else:
|
||||||
|
add_word(prop_name, random_nouns, nouns)
|
||||||
|
add_word(prop_name, random_adjectives, adjectives)
|
||||||
|
elif prop_name in nouns:
|
||||||
|
add_word(prop_name, random_nouns, nouns)
|
||||||
|
elif prop_name in adjectives:
|
||||||
|
add_word(prop_name, random_adjectives, adjectives)
|
||||||
|
|
||||||
|
random_nouns = ' '.join(random_nouns)
|
||||||
|
random_adjectives = ' '.join(random_adjectives)
|
||||||
|
return (random_nouns, random_adjectives)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def name(self) -> str:
|
||||||
|
base_name = super().name
|
||||||
|
(nouns, adjectives) = self._random_descriptors()
|
||||||
|
if not (nouns or adjectives):
|
||||||
|
return base_name
|
||||||
|
|
||||||
|
template = self._name_template(
|
||||||
|
with_adjectives=True if adjectives else False,
|
||||||
|
with_nouns=True if nouns else False,
|
||||||
|
)
|
||||||
|
return template.format(**self, adjectives=adjectives, nouns=nouns, name=base_name).title()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def to_hit(self):
|
||||||
|
bonus_val = 0
|
||||||
|
bonus_dice = ''
|
||||||
|
if not hasattr(self, 'properties'):
|
||||||
|
return ''
|
||||||
|
for prop in self.properties.values():
|
||||||
|
mod = getattr(prop, 'to_hit', None)
|
||||||
|
if not mod:
|
||||||
|
continue
|
||||||
|
if type(mod) is int:
|
||||||
|
bonus_val += mod
|
||||||
|
elif type(mod) is str:
|
||||||
|
bonus_dice += f"+{mod}"
|
||||||
|
return f"+{bonus_val}{bonus_dice}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def damage_dice(self):
|
||||||
|
if not hasattr(self, 'properties'):
|
||||||
|
return ''
|
||||||
|
dmg = {
|
||||||
|
self.damage_type: str(self.damage) or ''
|
||||||
|
}
|
||||||
|
|
||||||
|
for prop in self.properties.values():
|
||||||
|
mod = getattr(prop, 'damage', None)
|
||||||
|
if not mod:
|
||||||
|
continue
|
||||||
|
key = str(prop.damage_type)
|
||||||
|
this_damage = dmg.get(key, '')
|
||||||
|
if this_damage:
|
||||||
|
dmg[key] = f"{this_damage}+{mod}"
|
||||||
|
else:
|
||||||
|
dmg[key] = mod
|
||||||
|
|
||||||
|
return ' + '.join([f"{v} {k}" for k, v in dmg.items()])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def summary(self):
|
||||||
|
return f"{self.to_hit} to hit, {self.range} ft., {self.targets} targets. {self.damage_dice}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def details(self):
|
||||||
|
"""
|
||||||
|
Format the item properties as nested bullet lists.
|
||||||
|
"""
|
||||||
|
props = ', '.join(self.get('properties', dict()).keys())
|
||||||
|
return "\n".join([
|
||||||
|
f"{self.name}",
|
||||||
|
f" * {self.rarity.rarity} {self.category} weapon ({props})",
|
||||||
|
f" * {self.summary}",
|
||||||
|
f"\n{self.description}\n"
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponGenerator(types.ItemGenerator):
|
||||||
|
item_class = Weapon
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bases: WeightedSet = types.WEAPON_TYPES,
|
||||||
|
rarity: WeightedSet = types.RARITY,
|
||||||
|
properties_by_rarity: dict = types.PROPERTIES_BY_RARITY,
|
||||||
|
):
|
||||||
|
super().__init__(bases=bases, rarity=rarity, properties_by_rarity=properties_by_rarity)
|
||||||
|
|
||||||
|
def random_properties(self) -> dict:
|
||||||
|
# add missing base weapon defaults (TODO: update the sources)
|
||||||
|
item = super().random_properties()
|
||||||
|
item['targets'] = 1
|
||||||
|
if item['category'] == 'Martial':
|
||||||
|
if not item['range']:
|
||||||
|
item['range'] = ''
|
||||||
|
return item
|
||||||
|
|
||||||
|
# handlers for extra properties
|
||||||
|
|
||||||
|
def get_enchantment(self, **attrs) -> dict:
|
||||||
|
prop = types.ENCHANTMENT.random()
|
||||||
|
prop['adjectives'] = random_from_csv(prop['adjectives'])
|
||||||
|
prop['nouns'] = random_from_csv(prop['nouns'])
|
||||||
|
return prop
|
Loading…
Reference in New Issue
Block a user