diff --git a/deadsands/pyproject.toml b/deadsands/pyproject.toml index f602a20..7827680 100644 --- a/deadsands/pyproject.toml +++ b/deadsands/pyproject.toml @@ -16,7 +16,8 @@ pelican-drafts = "^0.1.1" pelican-sitemap = "^1.0.2" # 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-calendar = { git = "https://github.com/evilchili/dnd-calendar", 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" roll-table = "rolltable.cli:app" pelican = "site_tools.tasks:pelican_main" -dmsh = "site_tools.cli:dmsh" +dmsh = "dmsh.cli:dmsh" diff --git a/deadsands/site_tools/build_system.py b/deadsands/site_tools/build_system.py index dea757c..c3217d0 100644 --- a/deadsands/site_tools/build_system.py +++ b/deadsands/site_tools/build_system.py @@ -35,13 +35,9 @@ CONFIG.update( "import_path": "imports", # where new asseets will be made available "production_host": "deadsands.froghat.club", - # where to find roll table sources - "table_sources_path": "sources", - # where to store campaign state - "campaign_save_path": '~/.dnd', + + "data_path": '~/.dnd', "campaign_name": "deadsands", - # campaign start date - "campaign_start_date": "2.1125.5.25", } ) diff --git a/deadsands/site_tools/campaign.py b/deadsands/site_tools/campaign.py deleted file mode 100644 index 99b644d..0000000 --- a/deadsands/site_tools/campaign.py +++ /dev/null @@ -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) diff --git a/deadsands/site_tools/cli.py b/deadsands/site_tools/cli.py index e728cc1..cd36e6a 100644 --- a/deadsands/site_tools/cli.py +++ b/deadsands/site_tools/cli.py @@ -1,6 +1,4 @@ -import asyncio import sys -import termios from enum import Enum from pathlib import Path @@ -10,7 +8,6 @@ from rolltable.types import RollTable from typing_extensions import Annotated from site_tools.content_manager import create -from site_tools.shell.interactive_shell import DMShell 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)) -# 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__": site_app() diff --git a/deadsands/site_tools/dmsh.py b/deadsands/site_tools/dmsh.py deleted file mode 100644 index e1488ce..0000000 --- a/deadsands/site_tools/dmsh.py +++ /dev/null @@ -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}") diff --git a/deadsands/site_tools/hoppers.py b/deadsands/site_tools/hoppers.py deleted file mode 100644 index 5499c2e..0000000 --- a/deadsands/site_tools/hoppers.py +++ /dev/null @@ -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 diff --git a/deadsands/site_tools/jobs.py b/deadsands/site_tools/jobs.py deleted file mode 100644 index 851149c..0000000 --- a/deadsands/site_tools/jobs.py +++ /dev/null @@ -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()) diff --git a/deadsands/site_tools/shell/__init__.py b/deadsands/site_tools/shell/__init__.py deleted file mode 100644 index dc414bd..0000000 --- a/deadsands/site_tools/shell/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .base import BasePrompt diff --git a/deadsands/site_tools/shell/base.py b/deadsands/site_tools/shell/base.py deleted file mode 100644 index b921fd3..0000000 --- a/deadsands/site_tools/shell/base.py +++ /dev/null @@ -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 diff --git a/deadsands/site_tools/shell/interactive_shell.py b/deadsands/site_tools/shell/interactive_shell.py deleted file mode 100644 index eabb5d2..0000000 --- a/deadsands/site_tools/shell/interactive_shell.py +++ /dev/null @@ -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("") - 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|[/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)) diff --git a/deadsands/site_tools/striders.py b/deadsands/site_tools/striders.py deleted file mode 100644 index c484a64..0000000 --- a/deadsands/site_tools/striders.py +++ /dev/null @@ -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))