adding rolltables
This commit is contained in:
parent
d7f4f2ed2d
commit
84c955fb62
|
@ -1,20 +1,30 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from rich import print
|
||||
from rich.logging import RichHandler
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from dnd_item.types import WeaponGenerator, MagicWeaponGenerator
|
||||
from dnd_item.types import WeaponGenerator, MagicWeaponGenerator, RollTable
|
||||
from dnd_item import five_e
|
||||
|
||||
app = typer.Typer()
|
||||
app_state = {}
|
||||
|
||||
|
||||
class OUTPUT_FORMATS(Enum):
|
||||
text = 'text'
|
||||
yaml = 'yaml'
|
||||
markdown = 'markdown'
|
||||
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
cr: int = typer.Option(default=None, help='The Challenge Rating to use when determining rarity.'),
|
||||
|
@ -44,6 +54,48 @@ def magic_weapon(count: int = typer.Option(1, help="The number of weapons to gen
|
|||
for weapon in MagicWeaponGenerator().random(count=count, challenge_rating=app_state['cr']):
|
||||
console.print(weapon.details)
|
||||
|
||||
@app.command("roll-table")
|
||||
def table(
|
||||
die: int = typer.Option(
|
||||
20,
|
||||
help='The size of the die for which to create a table'),
|
||||
hide_rolls: bool = typer.Option(
|
||||
False,
|
||||
help='If True, do not show the Roll column.',
|
||||
),
|
||||
collapsed: bool = typer.Option(
|
||||
True,
|
||||
help='If True, collapse multiple die values with the same option.'),
|
||||
width: int = typer.Option(
|
||||
120,
|
||||
help='Width of the table.'),
|
||||
output: OUTPUT_FORMATS = typer.Option(
|
||||
'text',
|
||||
help='The output format to use.',
|
||||
)
|
||||
):
|
||||
"""
|
||||
CLI for creating roll tables of randomly-generated items.
|
||||
"""
|
||||
rt = RollTable(
|
||||
sources=[MagicWeaponGenerator],
|
||||
die=die,
|
||||
hide_rolls=hide_rolls,
|
||||
challenge_rating=app_state['cr'],
|
||||
)
|
||||
|
||||
if output == OUTPUT_FORMATS.yaml:
|
||||
print(rt.as_yaml())
|
||||
elif output == OUTPUT_FORMATS.markdown:
|
||||
print(rt.as_markdown)
|
||||
else:
|
||||
rows = rt.rows if collapsed else rt.expanded_rows
|
||||
table = Table(*rows[0], width=width)
|
||||
for row in rows[1:]:
|
||||
table.add_row(*row)
|
||||
print(table)
|
||||
|
||||
|
||||
|
||||
@app.command()
|
||||
def convert():
|
||||
|
|
2328
dnd_item/sources/items-base.json
Normal file
2328
dnd_item/sources/items-base.json
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,8 @@
|
|||
metadata:
|
||||
headers:
|
||||
- damage_type
|
||||
- noun
|
||||
- adjective
|
||||
- nouns
|
||||
- adjectives
|
||||
frequencies:
|
||||
default:
|
||||
fire: 1.0
|
||||
|
@ -28,7 +28,7 @@ thunder:
|
|||
- thunder
|
||||
- thundering,booming
|
||||
psychic:
|
||||
- psychic
|
||||
- mind
|
||||
- psychic
|
||||
poison:
|
||||
- poison
|
||||
|
@ -40,8 +40,8 @@ force:
|
|||
- force
|
||||
- forceful
|
||||
necrotic:
|
||||
- necrosis
|
||||
- necrotic, darkness, unholy
|
||||
- necrosis, darkness, shadows
|
||||
- necrotic, dark, unholy
|
||||
radiant:
|
||||
- radiance
|
||||
- radiance, shining
|
||||
- radiant, holy
|
||||
|
|
24
dnd_item/sources/properties.yaml
Normal file
24
dnd_item/sources/properties.yaml
Normal file
|
@ -0,0 +1,24 @@
|
|||
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:
|
12
dnd_item/sources/properties_common.yaml
Normal file
12
dnd_item/sources/properties_common.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
metadata:
|
||||
headers:
|
||||
- name
|
||||
- nouns
|
||||
- adjectives
|
||||
- description
|
||||
- type
|
||||
light:
|
||||
- 'light'
|
||||
- 'light'
|
||||
- 'Weapons with the light property...'
|
||||
- weapon
|
8
dnd_item/sources/properties_legendary.yaml
Normal file
8
dnd_item/sources/properties_legendary.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
metadata:
|
||||
headers:
|
||||
- name
|
||||
- nouns
|
||||
- adjectives
|
||||
- description
|
||||
- type
|
||||
legendary:
|
20
dnd_item/sources/properties_rare.yaml
Normal file
20
dnd_item/sources/properties_rare.yaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
metadata:
|
||||
headers:
|
||||
- name
|
||||
- nouns
|
||||
- adjectives
|
||||
- description
|
||||
- damage_modifier
|
||||
- type
|
||||
element:
|
||||
- '{element.nouns}'
|
||||
- '{element.adjectives}'
|
||||
- 'Attacks made with the {name} do an extra {this.damage_modifier} {element.damage_type} damage.'
|
||||
- d6
|
||||
- weapon
|
||||
'+2':
|
||||
- magic
|
||||
- '+2'
|
||||
- This magical weapon grants +2 to attack and damage rolls.
|
||||
- 2
|
||||
- weapon
|
20
dnd_item/sources/properties_uncommon.yaml
Normal file
20
dnd_item/sources/properties_uncommon.yaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
metadata:
|
||||
headers:
|
||||
- name
|
||||
- nouns
|
||||
- adjectives
|
||||
- description
|
||||
- damage_modifier
|
||||
- type
|
||||
element:
|
||||
- '{element.nouns}'
|
||||
- '{element.adjectives}'
|
||||
- 'Attacks made with the {name} do an extra {this.damage_modifier} {element.damage_type} damage.'
|
||||
- d4
|
||||
- weapon
|
||||
'+1':
|
||||
- magic
|
||||
- '+1'
|
||||
- This magical weapon grants +1 to attack and damage rolls.
|
||||
- 1
|
||||
- weapon
|
20
dnd_item/sources/properties_very_rare.yaml
Normal file
20
dnd_item/sources/properties_very_rare.yaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
metadata:
|
||||
headers:
|
||||
- name
|
||||
- nouns
|
||||
- adjectives
|
||||
- description
|
||||
- damage modifier
|
||||
- type
|
||||
element:
|
||||
- '{element.nouns}'
|
||||
- '{element.adjectives}'
|
||||
- 'Attacks made with the {name} do an extra {this.damage_modifier} {element.damage_type} damage.'
|
||||
- d8
|
||||
- weapon
|
||||
'+3':
|
||||
- magic
|
||||
- '+3'
|
||||
- This magical weapon grants +3 to attack and damage rolls.
|
||||
- 3
|
||||
- weapon
|
|
@ -1,10 +1,11 @@
|
|||
import random
|
||||
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from random_sets.sets import WeightedSet, DataSourceSet
|
||||
|
||||
import rolltable.types
|
||||
|
||||
|
||||
# Create DataSourceSets, which are WeightedSets populated with DataSource
|
||||
# objects generated from yaml data files. These are used to supply default
|
||||
|
@ -13,6 +14,13 @@ sources = Path(__file__).parent / Path("sources")
|
|||
MAGIC_DAMAGE = DataSourceSet(sources / Path('magic_damage_types.yaml'))
|
||||
WEAPON_TYPES = DataSourceSet(sources / Path('weapons.yaml'))
|
||||
RARITY = DataSourceSet(sources / Path('rarity.yaml'))
|
||||
PROPERTIES = {
|
||||
'common': DataSourceSet(sources / Path('properties_common.yaml')),
|
||||
'uncommon': DataSourceSet(sources / Path('properties_uncommon.yaml')),
|
||||
'rare': DataSourceSet(sources / Path('properties_rare.yaml')),
|
||||
'very rare': DataSourceSet(sources / Path('properties_very_rare.yaml')),
|
||||
'legendary': DataSourceSet(sources / Path('properties_legendary.yaml')),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -59,20 +67,35 @@ class Item:
|
|||
|
||||
@property
|
||||
def name(self):
|
||||
return self._template.format(name=self._name, **self._attrs).title()
|
||||
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([self.name] + sorted(list(attrs_to_lines(self))))
|
||||
return "\n".join(["Properties:"] + sorted(list(attrs_to_lines(self))))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""
|
||||
|
@ -144,12 +167,14 @@ class ItemGenerator:
|
|||
templates: WeightedSet,
|
||||
types: WeightedSet,
|
||||
rarity: WeightedSet = RARITY,
|
||||
properties: WeightedSet = PROPERTIES,
|
||||
):
|
||||
self.types = types
|
||||
self.templates = templates
|
||||
self.rarity = rarity
|
||||
self.properties = properties
|
||||
|
||||
def random_properties(self) -> dict:
|
||||
def random_attributes(self) -> dict:
|
||||
"""
|
||||
Select random values from the available attributes. These values will
|
||||
be passed as arguments to the Item constructor.
|
||||
|
@ -164,14 +189,12 @@ class ItemGenerator:
|
|||
This method must return a dict that includes both this and that, and
|
||||
each of them must be either Item instances or dictionaries.
|
||||
"""
|
||||
|
||||
# Select one random template string and one item type.
|
||||
properties = {
|
||||
return {
|
||||
'template': self.templates.random(),
|
||||
'type': self.types.random(),
|
||||
'rarity': self.rarity.random(),
|
||||
'properties': self.properties.random(),
|
||||
}
|
||||
return properties
|
||||
|
||||
def random(self, count: int = 1, challenge_rating: int = 0) -> list:
|
||||
"""
|
||||
|
@ -195,7 +218,7 @@ class ItemGenerator:
|
|||
|
||||
items = []
|
||||
for _ in range(count):
|
||||
items.append(self.item_class.from_dict(**self.random_properties()))
|
||||
items.append(self.item_class.from_dict(**self.random_attributes()))
|
||||
return items
|
||||
|
||||
|
||||
|
@ -211,10 +234,11 @@ class WeaponGenerator(ItemGenerator):
|
|||
templates: WeightedSet = None,
|
||||
types: WeightedSet = WEAPON_TYPES,
|
||||
rarity: WeightedSet = RARITY,
|
||||
properties: WeightedSet = PROPERTIES,
|
||||
):
|
||||
if not templates:
|
||||
templates = WeightedSet(('{type.name}', 1.0),)
|
||||
super().__init__(types=types, templates=templates, rarity=rarity)
|
||||
super().__init__(types=types, templates=templates, rarity=rarity, properties=properties)
|
||||
|
||||
|
||||
class MagicWeaponGenerator(WeaponGenerator):
|
||||
|
@ -223,30 +247,129 @@ class MagicWeaponGenerator(WeaponGenerator):
|
|||
"""
|
||||
def __init__(
|
||||
self,
|
||||
templates: WeightedSet = None,
|
||||
types: WeightedSet = WEAPON_TYPES,
|
||||
rarity: WeightedSet = RARITY,
|
||||
magic: WeightedSet = MAGIC_DAMAGE,
|
||||
element: WeightedSet = MAGIC_DAMAGE,
|
||||
properties: WeightedSet = PROPERTIES,
|
||||
):
|
||||
self.magic = magic
|
||||
if not templates:
|
||||
templates = WeightedSet(
|
||||
# "Shortsword of Flames"
|
||||
('{type.name} of {magic.noun}', 1.0),
|
||||
# "Burning Lance"
|
||||
('{magic.adjective} {type.name}', 1.0),
|
||||
)
|
||||
super().__init__(types=types, templates=templates, rarity=rarity)
|
||||
super().__init__(types=types, templates=None, rarity=rarity, properties=properties)
|
||||
self.element = element
|
||||
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 random_properties(self):
|
||||
def get_template(self, attrs) -> WeightedSet:
|
||||
if not attrs['properties']:
|
||||
return '{type.name}'
|
||||
options = []
|
||||
if attrs['nouns']:
|
||||
options.append(('{type.name} of {nouns}', 1.0))
|
||||
if attrs['adjectives']:
|
||||
options.append(('{adjectives} {type.name}', 1.0))
|
||||
if attrs['nouns'] and attrs['adjectives']:
|
||||
numprops = len(attrs['properties'].keys())
|
||||
if numprops == 1:
|
||||
options.append(('{adjectives} {type.name} of {nouns}', 1.0))
|
||||
elif len(attrs['properties'].items()) > 1:
|
||||
options.append(('{adjectives} {type.name} of {nouns}', 1.0))
|
||||
options.append(('{type.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.
|
||||
"""
|
||||
properties = super().random_properties()
|
||||
magic = self.magic.random()
|
||||
properties['magic'] = {
|
||||
'adjective': random.choice(magic['adjective'].split(',')).strip(),
|
||||
'noun': random.choice(magic['noun'].split(',')).strip(),
|
||||
'die': '1d4'
|
||||
}
|
||||
return 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(
|
||||
type=self.types.random(),
|
||||
rarity=self.rarity.random(),
|
||||
properties=dict(),
|
||||
)
|
||||
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'] == 'element':
|
||||
attrs['element'] = self.element.random()
|
||||
attrs['element']['adjectives'] = random.choice(attrs['element']['adjectives'].split(',')).strip()
|
||||
attrs['element']['nouns'] = random.choice(attrs['element']['nouns'].split(',')).strip()
|
||||
|
||||
attrs['template'] = self.get_template(attrs)
|
||||
attrs['adjectives'] = ' '.join(attrs['adjectives'])
|
||||
attrs['nouns'] = ' '.join(attrs['nouns'])
|
||||
return attrs
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratorSource:
|
||||
generator: ItemGenerator
|
||||
cr: int
|
||||
|
||||
def random_values(self, count: int = 1) -> list:
|
||||
return [
|
||||
[item.name, item.rarity.rarity, item.summary]
|
||||
for item in self.generator.random(count=count, challenge_rating=self.cr)
|
||||
]
|
||||
|
||||
|
||||
class RollTable(rolltable.types.RollTable):
|
||||
def __init__(
|
||||
self,
|
||||
sources: list,
|
||||
die: int = 20,
|
||||
hide_rolls: bool = False,
|
||||
challenge_rating: int = 0
|
||||
):
|
||||
self._cr = challenge_rating
|
||||
super().__init__(
|
||||
sources=sources,
|
||||
frequency='default',
|
||||
die=die,
|
||||
hide_rolls=hide_rolls
|
||||
)
|
||||
|
||||
def _config(self):
|
||||
self._data = []
|
||||
for src in self._sources:
|
||||
self._data.append(GeneratorSource(generator=src(), cr=self._cr))
|
||||
|
||||
self._headers = [
|
||||
'Name',
|
||||
'Rarity',
|
||||
'Description',
|
||||
]
|
||||
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
|
||||
|
|
|
@ -15,6 +15,7 @@ typer = "^0.9.0"
|
|||
dice = "^4.0.0"
|
||||
|
||||
dnd-name-generator = { git = "https://github.com/evilchili/dnd-name-generator", branch='main' }
|
||||
dnd-rolltable = { git = "https://github.com/evilchili/dnd-rolltable", branch='main' }
|
||||
random-sets = { git = "https://github.com/evilchili/random-sets", branch='main' }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
|
Loading…
Reference in New Issue
Block a user