first working commit of random items
This commit is contained in:
parent
84c955fb62
commit
6d45ba9c4b
|
@ -11,7 +11,7 @@ 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, MagicWeaponGenerator, RollTable
|
from dnd_item.types import WeaponGenerator, RollTable
|
||||||
from dnd_item import five_e
|
from dnd_item import five_e
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
@ -24,7 +24,6 @@ class OUTPUT_FORMATS(Enum):
|
||||||
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.'),
|
||||||
|
@ -48,12 +47,6 @@ def weapon(count: int = typer.Option(1, help="The number of weapons to generate.
|
||||||
console.print(weapon.details)
|
console.print(weapon.details)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
def magic_weapon(count: int = typer.Option(1, help="The number of weapons to generate.")):
|
|
||||||
console = Console()
|
|
||||||
for weapon in MagicWeaponGenerator().random(count=count, challenge_rating=app_state['cr']):
|
|
||||||
console.print(weapon.details)
|
|
||||||
|
|
||||||
@app.command("roll-table")
|
@app.command("roll-table")
|
||||||
def table(
|
def table(
|
||||||
die: int = typer.Option(
|
die: int = typer.Option(
|
||||||
|
@ -78,7 +71,7 @@ def table(
|
||||||
CLI for creating roll tables of randomly-generated items.
|
CLI for creating roll tables of randomly-generated items.
|
||||||
"""
|
"""
|
||||||
rt = RollTable(
|
rt = RollTable(
|
||||||
sources=[MagicWeaponGenerator],
|
sources=[WeaponGenerator],
|
||||||
die=die,
|
die=die,
|
||||||
hide_rolls=hide_rolls,
|
hide_rolls=hide_rolls,
|
||||||
challenge_rating=app_state['cr'],
|
challenge_rating=app_state['cr'],
|
||||||
|
@ -96,7 +89,6 @@ def table(
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def convert():
|
def convert():
|
||||||
src = five_e.weapons()
|
src = five_e.weapons()
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
metadata:
|
|
||||||
headers:
|
|
||||||
- rarity
|
|
||||||
- name
|
|
||||||
- nouns
|
|
||||||
- adjectives
|
|
||||||
- description
|
|
||||||
- type
|
|
||||||
|
|
||||||
common:
|
|
||||||
uncommon:
|
|
||||||
rare:
|
|
||||||
'+2':
|
|
||||||
- magic
|
|
||||||
- '+2'
|
|
||||||
- This magical weapon grants +2 to attack and damage rolls.
|
|
||||||
- weapon
|
|
||||||
very rare:
|
|
||||||
'+3':
|
|
||||||
- magic
|
|
||||||
- '+3'
|
|
||||||
- This magical weapon grants +3 to attack and damage rolls.
|
|
||||||
- weapon
|
|
||||||
legendary:
|
|
|
@ -8,5 +8,5 @@ metadata:
|
||||||
light:
|
light:
|
||||||
- 'light'
|
- 'light'
|
||||||
- 'light'
|
- 'light'
|
||||||
- 'Weapons with the light property...'
|
- 'A light weapon is small and easy to handle, making it ideal for use when fighting with two weapons.'
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -4,17 +4,23 @@ metadata:
|
||||||
- nouns
|
- nouns
|
||||||
- adjectives
|
- adjectives
|
||||||
- description
|
- description
|
||||||
- damage_modifier
|
- damage_type
|
||||||
|
- damage
|
||||||
|
- to_hit
|
||||||
- type
|
- type
|
||||||
element:
|
enchanted:
|
||||||
- '{element.nouns}'
|
- '{enchanted.nouns}'
|
||||||
- '{element.adjectives}'
|
- '{enchanted.adjectives}'
|
||||||
- 'Attacks made with the {name} do an extra {this.damage_modifier} {element.damage_type} damage.'
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
- d6
|
- '{enchanted.damage_type}'
|
||||||
|
- 1d6
|
||||||
|
- 0
|
||||||
- weapon
|
- weapon
|
||||||
'+2':
|
magical:
|
||||||
- magic
|
- magic
|
||||||
- '+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}'
|
||||||
|
- 2
|
||||||
- 2
|
- 2
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -4,17 +4,23 @@ metadata:
|
||||||
- nouns
|
- nouns
|
||||||
- adjectives
|
- adjectives
|
||||||
- description
|
- description
|
||||||
- damage_modifier
|
- damage_type
|
||||||
|
- damage
|
||||||
|
- to_hit
|
||||||
- type
|
- type
|
||||||
element:
|
enchanted:
|
||||||
- '{element.nouns}'
|
- '{enchanted.nouns}'
|
||||||
- '{element.adjectives}'
|
- '{enchanted.adjectives}'
|
||||||
- 'Attacks made with the {name} do an extra {this.damage_modifier} {element.damage_type} damage.'
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
- d4
|
- '{enchanted.damage_type}'
|
||||||
|
- 1d4
|
||||||
|
- 0
|
||||||
- weapon
|
- weapon
|
||||||
'+1':
|
magical:
|
||||||
- magic
|
- magic
|
||||||
- '+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}'
|
||||||
|
- 1
|
||||||
- 1
|
- 1
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -4,17 +4,23 @@ metadata:
|
||||||
- nouns
|
- nouns
|
||||||
- adjectives
|
- adjectives
|
||||||
- description
|
- description
|
||||||
- damage modifier
|
- damage_type
|
||||||
|
- damage
|
||||||
|
- to_hit
|
||||||
- type
|
- type
|
||||||
element:
|
enchanted:
|
||||||
- '{element.nouns}'
|
- '{enchanted.nouns}'
|
||||||
- '{element.adjectives}'
|
- '{enchanted.adjectives}'
|
||||||
- 'Attacks made with the {name} do an extra {this.damage_modifier} {element.damage_type} damage.'
|
- 'Attacks made with this magical weapon do an extra {this.damage} {this.damage_type} damage.'
|
||||||
- d8
|
- '{enchanted.damage_type}'
|
||||||
|
- 1d8
|
||||||
|
- 0
|
||||||
- weapon
|
- weapon
|
||||||
'+3':
|
magical:
|
||||||
- magic
|
- magic
|
||||||
- '+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}'
|
||||||
|
- 3
|
||||||
- 3
|
- 3
|
||||||
- weapon
|
- weapon
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
metadata:
|
metadata:
|
||||||
headers:
|
headers:
|
||||||
- rarity
|
- rarity
|
||||||
|
- sort_order
|
||||||
frequencies:
|
frequencies:
|
||||||
'default':
|
'default':
|
||||||
common: 1.0
|
common: 1.0
|
||||||
|
@ -40,7 +41,12 @@ metadata:
|
||||||
very rare: 0.5
|
very rare: 0.5
|
||||||
legendary: 0.3
|
legendary: 0.3
|
||||||
common:
|
common:
|
||||||
|
- 0
|
||||||
uncommon:
|
uncommon:
|
||||||
|
- 1
|
||||||
rare:
|
rare:
|
||||||
|
- 2
|
||||||
very rare:
|
very rare:
|
||||||
|
- 3
|
||||||
legendary:
|
legendary:
|
||||||
|
- 4
|
||||||
|
|
|
@ -4,8 +4,8 @@ metadata:
|
||||||
- category
|
- category
|
||||||
- type
|
- type
|
||||||
- weight
|
- weight
|
||||||
|
- damage_type
|
||||||
- damage
|
- damage
|
||||||
- die
|
|
||||||
- range
|
- range
|
||||||
- reload
|
- reload
|
||||||
- value
|
- value
|
||||||
|
@ -16,7 +16,7 @@ Battleaxe:
|
||||||
- '4'
|
- '4'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1000'
|
- '1000'
|
||||||
- versatile
|
- versatile
|
||||||
|
@ -36,7 +36,7 @@ Club:
|
||||||
- '2'
|
- '2'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 1d4
|
- 1d4
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '10'
|
- '10'
|
||||||
- light
|
- light
|
||||||
|
@ -66,7 +66,7 @@ Double-bladed scimitar:
|
||||||
- '6'
|
- '6'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 2d4
|
- 2d4
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '10000'
|
- '10000'
|
||||||
- special, two-handed
|
- special, two-handed
|
||||||
|
@ -76,7 +76,7 @@ Flail:
|
||||||
- '2'
|
- '2'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1000'
|
- '1000'
|
||||||
- ''
|
- ''
|
||||||
|
@ -86,7 +86,7 @@ Glaive:
|
||||||
- '6'
|
- '6'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d10
|
- 1d10
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '2000'
|
- '2000'
|
||||||
- heavy, reach, two-handed
|
- heavy, reach, two-handed
|
||||||
|
@ -96,7 +96,7 @@ Greataxe:
|
||||||
- '7'
|
- '7'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d12
|
- 1d12
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '3000'
|
- '3000'
|
||||||
- heavy, two-handed
|
- heavy, two-handed
|
||||||
|
@ -106,7 +106,7 @@ Greatclub:
|
||||||
- '10'
|
- '10'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '20'
|
- '20'
|
||||||
- two-handed
|
- two-handed
|
||||||
|
@ -116,7 +116,7 @@ Greatsword:
|
||||||
- '6'
|
- '6'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 2d6
|
- 2d6
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '5000'
|
- '5000'
|
||||||
- heavy, two-handed
|
- heavy, two-handed
|
||||||
|
@ -126,7 +126,7 @@ Halberd:
|
||||||
- '6'
|
- '6'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d10
|
- 1d10
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '2000'
|
- '2000'
|
||||||
- heavy, reach, two-handed
|
- heavy, reach, two-handed
|
||||||
|
@ -166,7 +166,7 @@ Hooked shortspear:
|
||||||
- '2'
|
- '2'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d4
|
- 1d4
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- ''
|
- ''
|
||||||
- light
|
- light
|
||||||
|
@ -196,7 +196,7 @@ Lance:
|
||||||
- '6'
|
- '6'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d12
|
- 1d12
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1000'
|
- '1000'
|
||||||
- reach, special
|
- reach, special
|
||||||
|
@ -246,7 +246,7 @@ Longsword:
|
||||||
- '3'
|
- '3'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1500'
|
- '1500'
|
||||||
- versatile
|
- versatile
|
||||||
|
@ -256,7 +256,7 @@ Mace:
|
||||||
- '4'
|
- '4'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 1d6
|
- 1d6
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '500'
|
- '500'
|
||||||
- ''
|
- ''
|
||||||
|
@ -266,7 +266,7 @@ Maul:
|
||||||
- '10'
|
- '10'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 2d6
|
- 2d6
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1000'
|
- '1000'
|
||||||
- heavy, two-handed
|
- heavy, two-handed
|
||||||
|
@ -276,7 +276,7 @@ Morningstar:
|
||||||
- '4'
|
- '4'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1500'
|
- '1500'
|
||||||
- ''
|
- ''
|
||||||
|
@ -285,7 +285,7 @@ Net:
|
||||||
- Ranged
|
- Ranged
|
||||||
- '3'
|
- '3'
|
||||||
- ''
|
- ''
|
||||||
- null
|
- 5
|
||||||
- 5/15
|
- 5/15
|
||||||
- ''
|
- ''
|
||||||
- '100'
|
- '100'
|
||||||
|
@ -296,7 +296,7 @@ Pike:
|
||||||
- '18'
|
- '18'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d10
|
- 1d10
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '500'
|
- '500'
|
||||||
- heavy, reach, two-handed
|
- heavy, reach, two-handed
|
||||||
|
@ -306,7 +306,7 @@ Quarterstaff:
|
||||||
- '4'
|
- '4'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 1d6
|
- 1d6
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '20'
|
- '20'
|
||||||
- versatile
|
- versatile
|
||||||
|
@ -316,7 +316,7 @@ Rapier:
|
||||||
- '2'
|
- '2'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '2500'
|
- '2500'
|
||||||
- finesse
|
- finesse
|
||||||
|
@ -326,7 +326,7 @@ Scimitar:
|
||||||
- '3'
|
- '3'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d6
|
- 1d6
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '2500'
|
- '2500'
|
||||||
- finesse, light
|
- finesse, light
|
||||||
|
@ -346,7 +346,7 @@ Shortsword:
|
||||||
- '2'
|
- '2'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d6
|
- 1d6
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1000'
|
- '1000'
|
||||||
- finesse, light
|
- finesse, light
|
||||||
|
@ -356,7 +356,7 @@ Sickle:
|
||||||
- '2'
|
- '2'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d4
|
- 1d4
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '100'
|
- '100'
|
||||||
- light
|
- light
|
||||||
|
@ -396,7 +396,7 @@ War pick:
|
||||||
- '2'
|
- '2'
|
||||||
- Piercing
|
- Piercing
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '500'
|
- '500'
|
||||||
- ''
|
- ''
|
||||||
|
@ -406,7 +406,7 @@ Warhammer:
|
||||||
- '2'
|
- '2'
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
- 1d8
|
- 1d8
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '1500'
|
- '1500'
|
||||||
- versatile
|
- versatile
|
||||||
|
@ -416,7 +416,7 @@ Whip:
|
||||||
- '3'
|
- '3'
|
||||||
- Slashing
|
- Slashing
|
||||||
- 1d4
|
- 1d4
|
||||||
- null
|
- 5
|
||||||
- ''
|
- ''
|
||||||
- '200'
|
- '200'
|
||||||
- finesse, reach
|
- finesse, reach
|
||||||
|
|
|
@ -11,7 +11,7 @@ import rolltable.types
|
||||||
# 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")
|
||||||
MAGIC_DAMAGE = 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 = {
|
||||||
|
@ -24,79 +24,9 @@ PROPERTIES = {
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Item:
|
class AttributeDict:
|
||||||
"""
|
|
||||||
Item is a data class that constructs its attributes from keyword arguments
|
|
||||||
passed to the Item.from_dict() method. Any args that are dicts are
|
|
||||||
recursively converted into Item objects, allowing for access to nested
|
|
||||||
attribtues using dotted notation. Example:
|
|
||||||
|
|
||||||
>>> orb = Item.from_dict(rarity='rare', name='Orb of Example',
|
|
||||||
extra={'cost_in_gp': 1000})
|
|
||||||
>>> orb.rarity
|
|
||||||
rare
|
|
||||||
>>> orb.name
|
|
||||||
Orb of Example
|
|
||||||
>>> orb.extra.cost_in_gp
|
|
||||||
1000
|
|
||||||
|
|
||||||
String Formatting:
|
|
||||||
|
|
||||||
>>> orb.details
|
|
||||||
Orb of Example
|
|
||||||
* extra.cost_in_gp: 1000
|
|
||||||
* rarity: rare
|
|
||||||
|
|
||||||
Name Templates:
|
|
||||||
|
|
||||||
Item names can be built by overriding the default template, using
|
|
||||||
any available attribute:
|
|
||||||
|
|
||||||
>>> orb = Item.from_dict(
|
|
||||||
? name="orb",
|
|
||||||
? rarity="rare",
|
|
||||||
? extra={"cost_in_gp": 1000, "color": "green"},
|
|
||||||
? template="{rarity} {extra.color} {name} of Example",
|
|
||||||
? )
|
|
||||||
>>> orb.name
|
|
||||||
Rare Green Orb of Example
|
|
||||||
"""
|
|
||||||
_name: str = None
|
|
||||||
_template: str = None
|
|
||||||
_attrs: field(default_factory=dict) = None
|
_attrs: field(default_factory=dict) = None
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self._template.format(name=self._name, **self._attrs).format(**self._attrs).title()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def summary(self):
|
|
||||||
txt = []
|
|
||||||
for k, v in self._attrs['properties']._attrs.items():
|
|
||||||
txt.append(v.description.format(
|
|
||||||
this=v,
|
|
||||||
name=self.name,
|
|
||||||
**self._attrs,
|
|
||||||
))
|
|
||||||
return "\n\n".join(txt)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def details(self):
|
|
||||||
"""
|
|
||||||
Format the item attributes as nested bullet lists.
|
|
||||||
"""
|
|
||||||
return f"{self.name} ({self.rarity.rarity})\n{self.summary}\n--\n{self.properties}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def properties(self):
|
|
||||||
def attrs_to_lines(item, prefix: str = ''):
|
|
||||||
for (k, v) in item._attrs.items():
|
|
||||||
if type(v) is Item:
|
|
||||||
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))))
|
|
||||||
|
|
||||||
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.
|
||||||
|
@ -105,6 +35,88 @@ class Item:
|
||||||
return self._attrs[attr]
|
return self._attrs[attr]
|
||||||
return self.__getattribute__(attr)
|
return self.__getattribute__(attr)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self._flatten(self)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def _flatten(self, obj, prefix=None):
|
||||||
|
if prefix == '':
|
||||||
|
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
|
||||||
|
def from_dict(cls, **kwargs: dict):
|
||||||
|
"""
|
||||||
|
Create a new AttributeDict object using keyword arguments. Dicts are
|
||||||
|
recursively converted to AttributeDict objects; everything else is
|
||||||
|
passed as-is.
|
||||||
|
"""
|
||||||
|
attrs = {}
|
||||||
|
for k, v in sorted(kwargs.items()):
|
||||||
|
attrs[k] = AttributeDict.from_dict(**v)if type(v) is dict else v
|
||||||
|
return cls(_attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Item(AttributeDict):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
_name: str = None
|
||||||
|
rarity: str = None
|
||||||
|
_template: str = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._template.format(name=self._name, **self._attrs).format(**self._attrs).title()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def properties(self):
|
||||||
|
return self._attrs['properties']._attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
txt = []
|
||||||
|
for k, v in self.properties.items():
|
||||||
|
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, **kwargs: dict):
|
||||||
"""
|
"""
|
||||||
|
@ -117,18 +129,70 @@ class Item:
|
||||||
accessed directly through dotted attribute notation.
|
accessed directly through dotted attribute notation.
|
||||||
"""
|
"""
|
||||||
name = kwargs.pop('name') if 'name' in kwargs else '(unnamed)'
|
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}'
|
template = kwargs.pop('template') if 'template' in kwargs else '{name}'
|
||||||
attrs = {}
|
attrs = {}
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
attrs[k] = Item.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(_name=name, _template=template, _attrs=attrs)
|
return cls(_name=name, rarity=rarity, _template=template, _attrs=attrs)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
class Weapon(Item):
|
class Weapon(Item):
|
||||||
"""
|
"""
|
||||||
An Item class representing a weapon.
|
An Item class representing a weapon with the following attributes:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def to_hit(self):
|
||||||
|
bonus_val = 0
|
||||||
|
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
|
||||||
|
def damage_dice(self):
|
||||||
|
dmg = dict()
|
||||||
|
dmg[self.base.damage_type] = self.base.damage or ''
|
||||||
|
for prop in self.properties.values():
|
||||||
|
mod = getattr(prop, 'damage', None)
|
||||||
|
if not mod:
|
||||||
|
continue
|
||||||
|
key = str(prop.damage_type).format(**self._attrs).title()
|
||||||
|
if key not in dmg:
|
||||||
|
dmg[key] = str(mod)
|
||||||
|
else:
|
||||||
|
dmg[key] += f"+{mod}"
|
||||||
|
|
||||||
|
return ' + '.join([f"{v} {k}" for k, v in dmg.items()])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def summary(self):
|
||||||
|
return f"{self.to_hit} to hit, {self.base.range} ft., {self.base.targets} targets. {self.damage_dice}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def details(self):
|
||||||
|
"""
|
||||||
|
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:
|
||||||
"""
|
"""
|
||||||
|
@ -165,11 +229,11 @@ class ItemGenerator:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
templates: WeightedSet,
|
templates: WeightedSet,
|
||||||
types: WeightedSet,
|
bases: WeightedSet,
|
||||||
rarity: WeightedSet = RARITY,
|
rarity: WeightedSet = RARITY,
|
||||||
properties: WeightedSet = PROPERTIES,
|
properties: WeightedSet = PROPERTIES,
|
||||||
):
|
):
|
||||||
self.types = types
|
self.bases = bases
|
||||||
self.templates = templates
|
self.templates = templates
|
||||||
self.rarity = rarity
|
self.rarity = rarity
|
||||||
self.properties = properties
|
self.properties = properties
|
||||||
|
@ -191,7 +255,7 @@ class ItemGenerator:
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'template': self.templates.random(),
|
'template': self.templates.random(),
|
||||||
'type': self.types.random(),
|
'base': self.bases.random(),
|
||||||
'rarity': self.rarity.random(),
|
'rarity': self.rarity.random(),
|
||||||
'properties': self.properties.random(),
|
'properties': self.properties.random(),
|
||||||
}
|
}
|
||||||
|
@ -223,37 +287,18 @@ class ItemGenerator:
|
||||||
|
|
||||||
|
|
||||||
class WeaponGenerator(ItemGenerator):
|
class WeaponGenerator(ItemGenerator):
|
||||||
"""
|
|
||||||
An ItemGenerator that generates basic (non-magical) weapons.
|
|
||||||
"""
|
|
||||||
|
|
||||||
item_class = Weapon
|
item_class = Weapon
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
templates: WeightedSet = None,
|
templates: WeightedSet = WeightedSet(('{base.name}', 1.0),),
|
||||||
types: WeightedSet = WEAPON_TYPES,
|
bases: WeightedSet = WEAPON_TYPES,
|
||||||
rarity: WeightedSet = RARITY,
|
rarity: WeightedSet = RARITY,
|
||||||
|
enchanted: WeightedSet = ENCHANTMENT,
|
||||||
properties: WeightedSet = PROPERTIES,
|
properties: WeightedSet = PROPERTIES,
|
||||||
):
|
):
|
||||||
if not templates:
|
super().__init__(bases=bases, templates=None, rarity=rarity, properties=properties)
|
||||||
templates = WeightedSet(('{type.name}', 1.0),)
|
self.enchanted = enchanted
|
||||||
super().__init__(types=types, templates=templates, rarity=rarity, properties=properties)
|
|
||||||
|
|
||||||
|
|
||||||
class MagicWeaponGenerator(WeaponGenerator):
|
|
||||||
"""
|
|
||||||
An ItemGenerator that generates weapons imbued with magical effects.
|
|
||||||
"""
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
types: WeightedSet = WEAPON_TYPES,
|
|
||||||
rarity: WeightedSet = RARITY,
|
|
||||||
element: WeightedSet = MAGIC_DAMAGE,
|
|
||||||
properties: WeightedSet = PROPERTIES,
|
|
||||||
):
|
|
||||||
super().__init__(types=types, templates=None, rarity=rarity, properties=properties)
|
|
||||||
self.element = element
|
|
||||||
self.property_count_by_rarity = {
|
self.property_count_by_rarity = {
|
||||||
'common': WeightedSet((0, 1.0)),
|
'common': WeightedSet((0, 1.0)),
|
||||||
'uncommon': WeightedSet((1, 1.0)),
|
'uncommon': WeightedSet((1, 1.0)),
|
||||||
|
@ -264,19 +309,19 @@ class MagicWeaponGenerator(WeaponGenerator):
|
||||||
|
|
||||||
def get_template(self, attrs) -> WeightedSet:
|
def get_template(self, attrs) -> WeightedSet:
|
||||||
if not attrs['properties']:
|
if not attrs['properties']:
|
||||||
return '{type.name}'
|
return '{base.name}'
|
||||||
options = []
|
options = []
|
||||||
if attrs['nouns']:
|
if attrs['nouns']:
|
||||||
options.append(('{type.name} of {nouns}', 1.0))
|
options.append(('{base.name} of {nouns}', 1.0))
|
||||||
if attrs['adjectives']:
|
if attrs['adjectives']:
|
||||||
options.append(('{adjectives} {type.name}', 1.0))
|
options.append(('{adjectives} {base.name}', 1.0))
|
||||||
if attrs['nouns'] and attrs['adjectives']:
|
if attrs['nouns'] and attrs['adjectives']:
|
||||||
numprops = len(attrs['properties'].keys())
|
numprops = len(attrs['properties'].keys())
|
||||||
if numprops == 1:
|
if numprops == 1:
|
||||||
options.append(('{adjectives} {type.name} of {nouns}', 1.0))
|
options.append(('{adjectives} {base.name} of {nouns}', 0.1))
|
||||||
elif len(attrs['properties'].items()) > 1:
|
elif len(attrs['properties'].items()) > 1:
|
||||||
options.append(('{adjectives} {type.name} of {nouns}', 1.0))
|
options.append(('{adjectives} {base.name} of {nouns}', 1.0))
|
||||||
options.append(('{type.name} of {adjectives} {nouns}', 1.0))
|
options.append(('{base.name} of {adjectives} {nouns}', 1.0))
|
||||||
return WeightedSet(*options).random()
|
return WeightedSet(*options).random()
|
||||||
|
|
||||||
def random_attributes(self) -> dict:
|
def random_attributes(self) -> dict:
|
||||||
|
@ -288,10 +333,16 @@ class MagicWeaponGenerator(WeaponGenerator):
|
||||||
# currently selectedon the rarity data source, which in turn will be
|
# currently selectedon the rarity data source, which in turn will be
|
||||||
# set by self.random(), controllable by the caller.
|
# set by self.random(), controllable by the caller.
|
||||||
attrs = dict(
|
attrs = dict(
|
||||||
type=self.types.random(),
|
base=self.bases.random(),
|
||||||
rarity=self.rarity.random(),
|
rarity=self.rarity.random(),
|
||||||
properties=dict(),
|
properties=dict(),
|
||||||
)
|
)
|
||||||
|
attrs['base']['targets'] = 1
|
||||||
|
|
||||||
|
if attrs['base']['category'] == 'Martial':
|
||||||
|
if not attrs['base']['range']:
|
||||||
|
attrs['base']['range'] = '5'
|
||||||
|
|
||||||
rarity = attrs['rarity']['rarity']
|
rarity = attrs['rarity']['rarity']
|
||||||
|
|
||||||
numprops = min(
|
numprops = min(
|
||||||
|
@ -311,10 +362,10 @@ class MagicWeaponGenerator(WeaponGenerator):
|
||||||
for prop_name, prop in attrs['properties'].items():
|
for prop_name, prop in attrs['properties'].items():
|
||||||
attrs['adjectives'].append(prop['adjectives'])
|
attrs['adjectives'].append(prop['adjectives'])
|
||||||
attrs['nouns'].append(prop['nouns'])
|
attrs['nouns'].append(prop['nouns'])
|
||||||
if prop['name'] == 'element':
|
if prop['name'] == 'enchanted':
|
||||||
attrs['element'] = self.element.random()
|
attrs['enchanted'] = self.enchanted.random()
|
||||||
attrs['element']['adjectives'] = random.choice(attrs['element']['adjectives'].split(',')).strip()
|
attrs['enchanted']['adjectives'] = random.choice(attrs['enchanted']['adjectives'].split(',')).strip()
|
||||||
attrs['element']['nouns'] = random.choice(attrs['element']['nouns'].split(',')).strip()
|
attrs['enchanted']['nouns'] = random.choice(attrs['enchanted']['nouns'].split(',')).strip()
|
||||||
|
|
||||||
attrs['template'] = self.get_template(attrs)
|
attrs['template'] = self.get_template(attrs)
|
||||||
attrs['adjectives'] = ' '.join(attrs['adjectives'])
|
attrs['adjectives'] = ' '.join(attrs['adjectives'])
|
||||||
|
@ -328,10 +379,11 @@ class GeneratorSource:
|
||||||
cr: int
|
cr: int
|
||||||
|
|
||||||
def random_values(self, count: int = 1) -> list:
|
def random_values(self, count: int = 1) -> list:
|
||||||
return [
|
vals = sorted(
|
||||||
[item.name, item.rarity.rarity, item.summary]
|
(item.rarity['sort_order'], [item.name, item.rarity['rarity'], item.summary, item.base.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]
|
||||||
|
|
||||||
|
|
||||||
class RollTable(rolltable.types.RollTable):
|
class RollTable(rolltable.types.RollTable):
|
||||||
|
@ -340,14 +392,14 @@ class RollTable(rolltable.types.RollTable):
|
||||||
sources: list,
|
sources: list,
|
||||||
die: int = 20,
|
die: int = 20,
|
||||||
hide_rolls: bool = False,
|
hide_rolls: bool = False,
|
||||||
challenge_rating: int = 0
|
challenge_rating: int = 0,
|
||||||
):
|
):
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _config(self):
|
def _config(self):
|
||||||
|
@ -358,18 +410,7 @@ class RollTable(rolltable.types.RollTable):
|
||||||
self._headers = [
|
self._headers = [
|
||||||
'Name',
|
'Name',
|
||||||
'Rarity',
|
'Rarity',
|
||||||
'Description',
|
'Summary',
|
||||||
|
'Properties'
|
||||||
]
|
]
|
||||||
self._header_excludes = []
|
self._header_excludes = []
|
||||||
|
|
||||||
@property
|
|
||||||
def _values(self) -> list:
|
|
||||||
if not self._generated_values:
|
|
||||||
ds_values = [t.random_values(self.die) for t in self._data]
|
|
||||||
self._generated_values = []
|
|
||||||
for face in range(self._die):
|
|
||||||
value = []
|
|
||||||
for index, ds in enumerate(ds_values):
|
|
||||||
value += ds_values[index][face]
|
|
||||||
self._generated_values.append(value)
|
|
||||||
return self._generated_values
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user