removing dmsh code

This commit is contained in:
evilchili 2024-01-16 19:43:10 -08:00
parent 2cadcf14fa
commit d0f4282220
11 changed files with 5 additions and 1030 deletions

View File

@ -16,7 +16,8 @@ pelican-drafts = "^0.1.1"
pelican-sitemap = "^1.0.2" pelican-sitemap = "^1.0.2"
# local wotsits # local wotsits
dnd-npcs= { git = "https://github.com/evilchili/dnd-npcs", branch = 'main' } dmsh = { git = "https://github.com/evilchili/dmsh", branch = 'main' }
dnd-npcs = { git = "https://github.com/evilchili/dnd-npcs", branch = 'main' }
dnd-rolltable = { git = "https://github.com/evilchili/dnd-rolltable", branch = 'main' } dnd-rolltable = { git = "https://github.com/evilchili/dnd-rolltable", branch = 'main' }
dnd-calendar = { git = "https://github.com/evilchili/dnd-calendar", branch = 'main' } dnd-calendar = { git = "https://github.com/evilchili/dnd-calendar", branch = 'main' }
elethis-cipher= { git = "https://github.com/evilchili/elethis-cipher", branch = 'main' } elethis-cipher= { git = "https://github.com/evilchili/elethis-cipher", branch = 'main' }
@ -34,7 +35,7 @@ pelican-yaml-metadata = "^2.1.2"
site = "site_tools.cli:site_app" site = "site_tools.cli:site_app"
roll-table = "rolltable.cli:app" roll-table = "rolltable.cli:app"
pelican = "site_tools.tasks:pelican_main" pelican = "site_tools.tasks:pelican_main"
dmsh = "site_tools.cli:dmsh" dmsh = "dmsh.cli:dmsh"

View File

@ -35,13 +35,9 @@ CONFIG.update(
"import_path": "imports", "import_path": "imports",
# where new asseets will be made available # where new asseets will be made available
"production_host": "deadsands.froghat.club", "production_host": "deadsands.froghat.club",
# where to find roll table sources
"table_sources_path": "sources", "data_path": '~/.dnd',
# where to store campaign state
"campaign_save_path": '~/.dnd',
"campaign_name": "deadsands", "campaign_name": "deadsands",
# campaign start date
"campaign_start_date": "2.1125.5.25",
} }
) )

View File

@ -1,83 +0,0 @@
from collections import defaultdict
from pathlib import Path
import shutil
import yaml
from reckoning import telisaran
def string_to_date(date):
return telisaran.datetime.from_expression(f"on {date}", timeline={})
def date_to_string(date):
return date.numeric
def _rotate_backups(path, max_backups=10):
oldest = None
if not path.exists():
return oldest
# move file.000 to file.001, file.001 to file.002, etc...
for i in range(max_backups - 2, -1, -1):
source = Path(f"{path}.{i:03d}")
target = Path(f"{path}.{i+1:03d}")
if not source.exists():
continue
if oldest is None:
oldest = i
if i == max_backups:
source.unlink()
shutil.move(source, target)
return oldest
def save(campaign, path='.', name='dnd_campaign'):
savedir = Path(path).expanduser()
savepath = savedir / f"{name}.yaml"
savedir.mkdir(exist_ok=True)
backup_count = _rotate_backups(savepath)
if savepath.exists():
target = Path(f"{savepath}.000")
shutil.move(savepath, target)
campaign['date'] = date_to_string(campaign['date'])
campaign['start_date'] = date_to_string(campaign['start_date'])
savepath.write_text(yaml.safe_dump(dict(campaign)))
return savepath, (backup_count or 0) + 2
def load(path=".", name='dnd_campaign', start_date='', backup=None, console=None):
ext = "" if backup is None else f".{backup:03d}"
default_date = string_to_date(start_date)
campaign = defaultdict(str)
campaign['start_date'] = default_date
campaign['date'] = default_date
campaign['level'] = 1
if console:
console.print(f"Loading campaign {name} from {path}...")
try:
target = Path(path).expanduser() / f"{name}.yaml{ext}"
with open(target, 'rb') as f:
loaded = yaml.safe_load(f)
loaded['start_date'] = string_to_date(loaded['start_date'])
loaded['date'] = string_to_date(loaded['date'])
campaign.update(loaded)
if console:
console.print(f"Successfully loaded Campaign {name} from {target}!")
return campaign
except FileNotFoundError:
console.print(f"No existing campaigns found in {path}.")
return campaign
except yaml.parser.ParserError as e:
if console:
console.print(f"{e}\nWill try an older backup.")
return load(path, 0 if backup is None else backup+1)

View File

@ -1,6 +1,4 @@
import asyncio
import sys import sys
import termios
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
@ -10,7 +8,6 @@ from rolltable.types import RollTable
from typing_extensions import Annotated from typing_extensions import Annotated
from site_tools.content_manager import create from site_tools.content_manager import create
from site_tools.shell.interactive_shell import DMShell
from site_tools import build_system from site_tools import build_system
@ -134,15 +131,5 @@ def new(
click.edit(filename=create(content_type.value, title, template_dir, category, template or content_type.value)) click.edit(filename=create(content_type.value, title, template_dir, category, template or content_type.value))
# STANDALONE ENTRY POINTS
def dmsh():
old_attrs = termios.tcgetattr(sys.stdin)
try:
asyncio.run(DMShell(build_system.CONFIG).start())
finally:
termios.tcsetattr(sys.stdin, termios.TCSANOW, old_attrs)
if __name__ == "__main__": if __name__ == "__main__":
site_app() site_app()

View File

@ -1,32 +0,0 @@
from enum import EnumMeta
from inspect import signature
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import FuzzyWordCompleter, NestedCompleter
from rich import print
from site_tools.cli import app
def dmsh():
session = PromptSession()
def cmd2dict(cmd):
sig = signature(cmd)
if not sig.parameters:
return None
cmds = {}
for k, v in list(sig.parameters.items()):
print(v, dir(v))
if v.annotation.__class__ == EnumMeta:
cmds[k] = FuzzyWordCompleter([e.value for e in v.annotation])
else:
cmds[k] = None
return cmds
commands = dict(site=dict((c.callback.__name__, cmd2dict(c.callback)) for c in app.registered_commands))
print(commands)
completer = NestedCompleter.from_nested_dict(commands)
text = session.prompt("DM> ", completer=completer, complete_while_typing=True, enable_history_search=False)
words = text.split()
print(f"You said {words}")

View File

@ -1,99 +0,0 @@
import npc
from language import defaults, types
from language.languages import common
from random_sets.sets import equal_weights
# some old-west sounding names. The majority of NPC names will use one of these
# as a given name, but will mix in the occasional fully random name from the
# Common language base class to keep it consistent with Telisar as a whole.
given_names = equal_weights([
'Alonzo', 'Amos', 'Arthur', 'Art', 'Austin', 'Bart', 'William', 'Bill',
'Will', 'Boone', 'Buck', 'Butch', 'Calvin', 'Carson', 'Cassidy', 'Charlie',
'Chester', 'Clay', 'Clayton', 'Cole', 'Coleman', 'Colt', 'Cooper', 'Coop',
'Earle', 'Edgar', 'Ed', 'Elijah', 'Eli', 'Ernest', 'Eugene', 'Gene',
'Flynn', 'Frank', 'Gary', 'George', 'Harry', 'Henry', 'Holt', 'Homer',
'Howard', 'Ike', 'James', 'Jasper', 'Jesse', 'John', 'Julian', 'Kit',
'Lawrence', 'Levi', 'Logan', 'Louis', 'Morgan', 'Porter', 'Reid', 'Reuben',
'Rufus', 'Samuel', 'Sam', 'Thomas', 'Tom', 'Tommy', 'Virgil', 'Walter',
'Walt', 'Wayne', 'Wesley', 'Wyatt', 'Zane', 'Zeke', 'Adelaide', 'Alice',
'Anna', 'Annie', 'Beatrice', 'Catherine', 'Cecily', 'Clara', 'Cora',
'Dorothea', 'Dorothy', 'Edith', 'Eleanor', 'Eliza', 'Elizabeth', 'Beth',
'Lizzie', 'Ella', 'Emma', 'Florence', 'Gertrude', 'Gertie', 'Harriet',
'Hazel', 'Ida', 'Josephine', 'Letitia', 'Louise', 'Lucinda', 'Lydia',
'Mary', 'Matilda', 'Tilly', 'Maude', 'Mercy', 'Minnie', 'Olivia',
'Rosemary', 'Sarah', 'Sophia', 'Temperance', 'Teresa', 'Tess', 'Theodora',
'Teddy', 'Virginia', 'Ginny', 'Winifred', 'Winnie',
], blank=False)
initials = defaults.vowels + defaults.consonants
class HopperName(types.NameGenerator):
"""
A variant on the Common name generator. In the Dewa Q'Asos region,
nicknames are much more common, and while folks may still have multiple
given names, they tend to use initials, or omit their middle names
entirely.
"""
def __init__(self):
super().__init__(
language=common.Language,
templates=types.NameSet(
# names without nicknames
(types.NameTemplate("given,surname"), 0.5),
(types.NameTemplate("given,initial,surname"), 0.75),
(types.NameTemplate("given,initial,initial,surname"), 0.5),
(types.NameTemplate("initial,given,surname"), 0.5),
(types.NameTemplate("initial,initial,surname"), 0.5),
# names with nickknames
(types.NameTemplate("nickname,given,surname"), 0.75),
(types.NameTemplate("nickname,given,initial,surname"), 0.5),
(types.NameTemplate("nickname,given,initial,initial,surname"), 0.25),
(types.NameTemplate("nickname,name,surname"), 0.5),
(types.NameTemplate("nickname,name,initial,surname"), 0.25),
),
# these match the default Common language name generator
syllables=types.SyllableSet(
(types.Syllable(template="vowel|consonant"), 1.0),
(types.Syllable(template="consonant,vowel"), 1.0),
),
suffixes=common.names.suffixes,
# add the nicknames, for which we use the list of NPC personality traits.
nicknames=defaults.personality + defaults.adjectives,
)
def get_initial(self):
initial = ''
while not initial:
initial = initials.random()
return initial.capitalize() + '.'
def get_given(self):
return given_names.random().capitalize()
Name = HopperName()
NobleName = Name
class Human(npc.types.NPC):
"""
A varianat human NPC type that generates names suitable for the dead sands.
"""
language = common
@property
def name(self):
if not self._name:
self._name = Name.name()[0]
return self._name['fullname']
# entrypoint for class discovery
NPC = Human

View File

@ -1,187 +0,0 @@
import random
import collections
from pathlib import Path
from rolltable.types import RollTable
from npc import random_npc
Crime = collections.namedtuple('Crime', ['name', 'min_bounty', 'max_bounty'])
def generate_location(frequency='default'):
source = Path("sources/locations.yaml")
rt = RollTable([source.read_text()], hide_rolls=True, frequency=frequency)
return random.choice(rt.rows[1:])[1]
def nearest(value, step=50):
if value < step:
return step
remainder = value % step
if remainder > int(step / 2):
return value - remainder + step
return value - remainder
class BaseJob:
"""
The base class for random odd jobs.
"""
def __init__(
self,
name=None,
details=None,
reward=None,
contact=None,
location=None
):
self._name = name
self._details = details
self._reward = reward
self._contact = contact or random_npc()
self._location = location
@property
def name(self):
return self._name
@property
def details(self):
if not self._details:
self._details = (
f"Speak to {self.contact} in {self.location}. "
)
return self._details
@property
def reward(self):
return self._reward
@property
def contact(self):
return self._contact
@property
def location(self):
return self._location
def __repr__(self):
return f"{self.__class__.__name__}: {self.name}\n{self.details}"
class Bounty(BaseJob):
"""
A Bounty job.
"""
crimes = [
Crime(name='theft', min_bounty=50, max_bounty=500),
Crime(name='overdue faction fees', min_bounty=50, max_bounty=500),
Crime(name='unpaid bar tab', min_bounty=50, max_bounty=100),
Crime(name="unpaid debt", min_bounty=50, max_bounty=200),
Crime(name='cattle rustling', min_bounty=200, max_bounty=1000),
Crime(name='murder', min_bounty=500, max_bounty=2000),
Crime(name='kidnapping', min_bounty=500, max_bounty=2000)
]
def __init__(self, target=None, crime=None, dead=None, alive=None, **kwargs):
super().__init__(**kwargs)
self._target = target
self._crime = crime
dead_or_alive = []
if dead is None:
dead = random.choice([True, False])
if alive is None:
alive = True
if dead:
dead_or_alive.append('Dead')
if alive:
dead_or_alive.append('Alive')
self._dead_or_alive = ' or '.join(dead_or_alive)
if not self._reward:
reward = nearest(random.randint(self.crime.min_bounty, self.crime.max_bounty))
self._reward = f"{reward} Gold Pieces"
if not self._name:
self._name = (
f"{self.reward} for the capture of {self.target.full_name.upper()}, "
f"wanted for the crime of {self.crime.name.upper()}. "
f"Wanted {self._dead_or_alive.upper()}"
)
@property
def crime(self):
if not self._crime:
self._crime = random.choice(Bounty.crimes)
return self._crime
@property
def details(self):
if not self._details:
self._details = f"{self.target.description}\nWhereabouts {self.target.whereabouts}."
return self._details
@property
def target(self):
if not self._target:
self._target = random_npc()
return self._target
class Determinant(BaseJob):
"""
Hiring the services of a Determinant to resolve a dispute.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
class Escort(BaseJob):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._contact = random_npc()
self._location = generate_location('settlements')
self._destination = generate_location('default')
self._reward = f"{nearest(random.randint(5, 20), step=5)} GP/day"
self._name = (
f"Accompany {self.contact} from {self.location} to "
f"{self._destination}. {self.reward}"
)
class Foraging(BaseJob):
def __init__(self, **kwargs):
super().__init__(**kwargs)
source = Path("sources/flora.yaml")
rt = RollTable([source.read_text()], hide_rolls=True)
# [ rarity, name, descr, val ]
self._ingredient = random.choice(rt.rows)
self._amount = nearest(random.randint(0, 300), step=25)
value = self._amount * int(self._ingredient[3].split(' ')[0])
bonus = nearest(random.randint(0, 200))
self._reward = f"{value} GP + {bonus} GP Bonus"
self._name = f"{self.reward} for {self._amount} {self._ingredient[1]}"
self._contact = "Andok"
self._location = "Andok's Apothecary, Tano's Edge"
classes = BaseJob.__subclasses__()
job_types = [c.__name__ for c in classes]
def generate_job():
return random.choice(classes)()
if __name__ == '__main__':
for i in range(10):
print(Escort())

View File

@ -1 +0,0 @@
from .base import BasePrompt

View File

@ -1,133 +0,0 @@
import functools
from collections import defaultdict, namedtuple
from textwrap import dedent
from prompt_toolkit.completion import NestedCompleter
from site_tools.console import Console
COMMANDS = defaultdict(dict)
Command = namedtuple("Commmand", "prompt,handler,usage,completer")
def register_command(handler, usage, completer=None):
prompt = handler.__qualname__.split(".", -1)[0]
cmd = handler.__name__
if cmd not in COMMANDS[prompt]:
COMMANDS[prompt][cmd] = Command(
prompt=prompt,
handler=handler,
usage=usage,
completer=completer,
)
def command(usage, completer=None, binding=None):
def decorator(func):
register_command(func, usage, completer)
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
class BasePrompt(NestedCompleter):
def __init__(self, cache={}):
super(BasePrompt, self).__init__(self._nested_completer_map())
self._prompt = ""
self._console = None
self._theme = None
self._toolbar = None
self._key_bindings = None
self._subshells = {}
self._cache = cache
self._name = "Interactive Shell"
def _register_subshells(self):
for subclass in BasePrompt.__subclasses__():
if subclass.__name__ == self.__class__.__name__:
continue
self._subshells[subclass.__name__] = subclass(parent=self)
def _nested_completer_map(self):
return dict((cmd_name, cmd.completer) for (cmd_name, cmd) in COMMANDS[self.__class__.__name__].items())
def _get_help(self, cmd=None):
try:
return dedent(COMMANDS[self.__class__.__name__][cmd].usage)
except KeyError:
return self.usage
@property
def name(self):
return self._name
@property
def cache(self):
return self._cache
@property
def key_bindings(self):
return self._key_bindings
@property
def usage(self):
text = dedent(f"""
[title]{self.name}[/title]
Available commands are listed below. Try 'help COMMAND' for detailed help.
[title]COMMANDS[/title]
""")
for name, cmd in sorted(self.commands.items()):
text += f" [b]{name:10s}[/b] {cmd.handler.__doc__.strip()}\n"
return text
@property
def commands(self):
return COMMANDS[self.__class__.__name__]
@property
def console(self):
if not self._console:
self._console = Console(color_system="truecolor")
return self._console
@property
def prompt(self):
return self._prompt
@property
def autocomplete_values(self):
return list(self.commands.keys())
@property
def toolbar(self):
return self._toolbar
def help(self, parts):
attr = None
if parts:
attr = parts[0]
self.console.print(self._get_help(attr))
return True
def process(self, cmd, *parts):
if cmd in self.commands:
return self.commands[cmd].handler(self, parts)
self.console.error(f"Command {cmd} not understood; try 'help' for help.")
def start(self, cmd=None):
while True:
if not cmd:
cmd = self.console.prompt(
self.prompt, completer=self, bottom_toolbar=self.toolbar, key_bindings=self.key_bindings
)
if cmd:
cmd, *parts = cmd.split()
self.process(cmd, *parts)
cmd = None

View File

@ -1,389 +0,0 @@
from pathlib import Path
from prompt_toolkit.application import get_app
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.key_binding import KeyBindings
from rolltable.types import RollTable
from site_tools.shell.base import BasePrompt, command
from site_tools import campaign
from site_tools import jobs
from site_tools import striders
from site_tools import hoppers
from reckoning.calendar import TelisaranCalendar
from reckoning.telisaran import Day
from reckoning.telisaran import ReckoningError
import npc
BINDINGS = KeyBindings()
ANCESTRY_PACK, ANCESTRIES = npc.load_ancestry_pack()
ANCESTRIES['strider'] = striders
ANCESTRIES['hopper'] = hoppers
class DMShell(BasePrompt):
def __init__(self, cache={}):
super().__init__(cache)
self._name = "DM Shell"
self._prompt = ["dm"]
self._toolbar = [("class:bold", " DMSH ")]
self._key_bindings = BINDINGS
self._register_subshells()
self._register_keybindings()
self.cache['campaign'] = campaign.load(
path=self.cache['campaign_save_path'],
name=self.cache['campaign_name'],
start_date=self.cache['campaign_start_date'],
console=self.console
)
self._campaign = self.cache['campaign']
def _register_keybindings(self):
self._toolbar.extend(
[
("", " [?] Help "),
("", " [F2] Wild Magic Table "),
("", " [F3] Trinkets"),
("", " [F4] NPC"),
("", " [F5] Date"),
("", " [F6] Job"),
("", " [F8] Save"),
("", " [^Q] Quit "),
]
)
@self.key_bindings.add("c-q")
@self.key_bindings.add("c-d")
@self.key_bindings.add("<sigint>")
def quit(event):
self.quit()
@self.key_bindings.add("?")
def help(event):
self.help()
@self.key_bindings.add("f2")
def wmt(event):
self.wmt()
@self.key_bindings.add("f3")
def trinkets(event):
self.trinkets()
@self.key_bindings.add("f4")
def npc(event):
self.npc()
@self.key_bindings.add("f5")
def date(event):
self.date()
@self.key_bindings.add("f6")
def job(event):
self.job()
@self.key_bindings.add("f8")
def save(event):
self.save()
def _handler_date_season(self, *args):
self.console.print(self.cache['calendar'].season)
def _handler_date_year(self, *args):
self.console.print(self.cache['calendar'].calendar)
def _handler_date_inc(self, days):
offset = int(days or 1) * Day.length_in_seconds
self._campaign['date'] = self._campaign['date'] + offset
return self.date()
def _handler_date_dec(self, days):
offset = int(days or 1) * Day.length_in_seconds
self._campaign['date'] = self._campaign['date'] - offset
return self.date()
def _handler_date_set(self, new_date):
try:
self._campaign['date'] = campaign.string_to_date(new_date)
except ReckoningError as e:
self.console.error(str(e))
self.console.error("Invalid date. Use numeric formats; see 'help date' for more.")
self.cache['calendar'] = TelisaranCalendar(today=self._campaign['date'])
return self.date()
def _rolltable(self, source, frequency='default', die=20):
return RollTable(
[Path(f"{self.cache['table_sources_path']}/{source}").read_text()],
frequency=frequency,
die=die
).as_table()
@command(usage="""
[title]DATE[/title]
Work with the Telisaran calendar, including the current campaign date.
[title]USAGE[/title]
[link]> date [COMMAND[, ARGS]][/link]
COMMAND Description
season Print the spans of the current season, highlighting today
year Print the full year's calendar, highlighting today.
inc N Increment the current date by N days; defaults to 1.
dec N Decrement the current date by N days; defaults to 1.
set DATE Set the current date to DATE, in numeric format, such as
[link]2.1125.1.45[/link].
""", completer=WordCompleter(
[
'season',
'year',
'inc',
'dec',
'set',
]
))
def date(self, parts=[]):
"""
Date and calendaring tools.
"""
if not self.cache['calendar']:
self.cache['calendar'] = TelisaranCalendar(today=self._campaign['date'])
if not parts:
self.console.print(f"Today is {self._campaign['date'].short} ({self._campaign['date'].numeric})")
return
cmd = parts[0]
try:
val = parts[1]
except IndexError:
val = None
handler = getattr(self, f"_handler_date_{cmd}", None)
if not handler:
self.console.error(f"Unsupported command: {cmd}. Try 'help date'.")
return
return handler(val)
@command(usage="""
[title]Save[/title]
Save the campaign state.
[title]USAGE[/title]
[link]> save[/link]
""")
def save(self, parts=[]):
"""
Save the campaign state.
"""
path, count = campaign.save(
self.cache['campaign'],
path=self.cache['campaign_save_path'],
name=self.cache['campaign_name']
)
self.console.print(f"Saved {path}; {count} backups exist.")
@command(usage="""
[title]NPC[/title]
Generate a randomized NPC commoner.
[title]USAGE[/title]
[link]> npc \\[ANCESTRY\\][/link]
[title]CLI[/title]
[link]npc --ancestry ANCESTRY[/link]
""", completer=WordCompleter(list(ANCESTRIES.keys())))
def npc(self, parts=[]):
"""
Generate an NPC commoner
"""
char = npc.random_npc([ANCESTRIES[parts[0]]] if parts else [])
self.console.print(char.description + "\n")
if char.personality:
self.console.print(f"Personality: {char.personality}\n")
if char.flaw:
self.console.print(f"Flaw: {char.flaw}\n")
if char.goal:
self.console.print(f"Goal: {char.goal}\n")
@command(usage="""
[title]QUIT[/title]
The [b]quit[/b] command exits dmsh.
[title]USAGE[/title]
[link]> quit|^D|<ENTER>[/link]
""")
def quit(self, *parts):
"""
Quit dmsh.
"""
self.save()
try:
get_app().exit()
finally:
raise SystemExit("")
@command(usage="""
[title]HELP FOR THE HELP LORD[/title]
The [b]help[/b] command will print usage information for whatever you're currently
doing. You can also ask for help on any command currently available.
[title]USAGE[/title]
[link]> help [COMMAND][/link]
""")
def help(self, parts=[]):
"""
Display the help message.
"""
super().help(parts)
return True
@command(
usage="""
[title]LOCATION[/title]
[b]loc[/b] sets the party's location to the specified region of the Sahwat Desert.
[title]USAGE[/title]
[link]loc LOCATION[/link]
""",
completer=WordCompleter(
[
"The Blooming Wastes",
"Dust River Canyon",
"Gopher Gulch",
"Calamity Ridge"
]
),
)
def loc(self, parts=[]):
"""
Move the party to a new region of the Sahwat Desert.
"""
if parts:
self.cache["location"] = " ".join(parts)
self.console.print(f"The party is in {self.cache['location']}.")
@command(usage="""
[title]OVERLAND TRAVEL[/title]
[b]ot[/b]
[title]USAGE[/title]
[link]ot in[/link]
""")
def ot(self, parts=[]):
"""
Increment the date by one day and record
"""
raise NotImplementedError()
@command(usage="""
[title]WILD MAGIC TABLE[/title]
[b]wmt[/b] Generates a d20 wild magic surge roll table. The table will be cached for the cache.
[title]USAGE[/title]
[link]> wmt[/link]
[title]CLI[/title]
[link]roll-table \\
sources/sahwat_magic_table.yaml \\
--frequency default --die 20[/link]
""")
def wmt(self, parts=[], source="sahwat_magic_table.yaml"):
"""
Generate a Wild Magic Table for resolving spell effects.
"""
if "wmt" not in self.cache:
self.cache['wmt'] = self._rolltable(source)
self.console.print(self.cache['wmt'])
@command(usage="""
[title]TRINKET TABLE[/title]
[b]trinkets[/b] Generates a d20 random trinket table.
[title]USAGE[/title]
[link]> trinkets[/link]
[title]CLI[/title]
[link]roll-table \\
sources/trinkets.yaml \\
--frequency default --die 20[/link]
""")
def trinkets(self, parts=[], source="trinkets.yaml"):
"""
Generate a trinkets roll table.
"""
self.console.print(self._rolltable(source))
@command(usage="""
[title]LEVEL[/title]
Get or set the current campaign's level. Used for generating loot tables.
[title]USAGE[/title]
[link]> level [LEVEL][/link]
""")
def level(self, parts=[]):
"""
Get or set the current level of the party.
"""
if parts:
newlevel = int(parts[0])
if newlevel > 20 or newlevel < 1:
self.console.error(f"Invalid level: {newlevel}. Levels must be between 1 and 20.")
self._campaign['level'] = newlevel
self.console.print(f"Party is currently at level {self._campaign['level']}.")
@command(usage="""
[title]JOB[/title]
Generate a random job.
[title]USAGE[/title]
[link]> job[/link]
""")
def job(self, parts=[]):
"""
Generate a random jobs table.
"""
self.console.print(jobs.generate_job())
@command(usage="""
[title]PLACE[/title]
""")
def place(self, parts=[]):
"""
Select random place names.
"""
freq = parts[0] if parts else 'nodesert'
self.console.print(self._rolltable("locations.yaml", frequency=freq, die=4))

View File

@ -1,85 +0,0 @@
import random
import textwrap
from functools import cached_property
import npc
from language import types
from language.languages import lizardfolk
subspecies = [
('black', 'acid'),
('red', 'fire'),
('blue', 'lightning'),
('green', 'poison'),
('white', 'frost')
]
ages = types.WeightedSet(
('wyrmling', 0.2),
('young', 1.0),
('adult', 1.0),
('ancient', 0.5)
)
class SandStriderNameGenerator(types.NameGenerator):
def __init__(self):
super().__init__(
language=lizardfolk.Language,
templates=types.NameSet(
(types.NameTemplate("name,name"), 1.0),
),
)
class NPC(npc.types.NPC):
language = lizardfolk
has_tail = True
has_horns = True
def __init__(self):
super().__init__(self)
self._personality = ''
self._flaw = ''
self._goal = ''
@cached_property
def name(self) -> str:
return str(SandStriderNameGenerator())
@cached_property
def fullname(self) -> str:
return self.name
@cached_property
def age(self) -> str:
return ages.random()
@cached_property
def subspecies(self) -> tuple:
return random.choice(subspecies)
@cached_property
def color(self) -> str:
return self.subspecies[0]
@cached_property
def spit(self) -> str:
return self.subspecies[1]
@property
def description(self) -> str:
return (
f"{self.name} is {npc.types.a_or_an(self.age)} {self.age} {self.color} sand strider "
f"with {self.horns} horns, {npc.types.a_or_an(self.nose)} {self.nose} snout, "
f"{self.body} body, and {self.tail} tail. {self.name} spits {self.spit}."
)
@property
def character_sheet(self) -> str:
return '\n'.join(textwrap.wrap(self.description, width=120))