2023-07-03 14:14:03 -07:00
|
|
|
from pathlib import Path
|
2023-07-04 10:59:51 -07:00
|
|
|
|
|
|
|
from prompt_toolkit.application import get_app
|
2023-07-03 14:14:03 -07:00
|
|
|
from prompt_toolkit.completion import WordCompleter
|
|
|
|
from prompt_toolkit.key_binding import KeyBindings
|
2024-01-06 12:23:05 -08:00
|
|
|
from rolltable.types import RollTable
|
2023-07-03 14:14:03 -07:00
|
|
|
|
2023-07-04 10:59:51 -07:00
|
|
|
from site_tools.shell.base import BasePrompt, command
|
2023-08-20 12:07:10 -07:00
|
|
|
from site_tools import campaign
|
2023-08-22 12:16:11 -07:00
|
|
|
from site_tools import jobs
|
2023-12-16 11:55:58 -08:00
|
|
|
from site_tools import striders
|
2024-01-15 00:57:51 -08:00
|
|
|
from site_tools import hoppers
|
2023-07-03 14:14:03 -07:00
|
|
|
|
2023-08-20 12:07:10 -07:00
|
|
|
from reckoning.calendar import TelisaranCalendar
|
2023-08-20 18:09:24 -07:00
|
|
|
from reckoning.telisaran import Day
|
|
|
|
from reckoning.telisaran import ReckoningError
|
2023-07-04 12:14:23 -07:00
|
|
|
|
2023-12-04 22:00:37 -08:00
|
|
|
import npc
|
|
|
|
|
2023-07-04 10:45:10 -07:00
|
|
|
BINDINGS = KeyBindings()
|
2023-07-03 14:14:03 -07:00
|
|
|
|
2023-12-04 22:00:37 -08:00
|
|
|
ANCESTRY_PACK, ANCESTRIES = npc.load_ancestry_pack()
|
2023-12-16 11:55:58 -08:00
|
|
|
ANCESTRIES['strider'] = striders
|
2024-01-15 00:57:51 -08:00
|
|
|
ANCESTRIES['hopper'] = hoppers
|
2023-12-04 22:00:37 -08:00
|
|
|
|
2023-07-04 11:18:31 -07:00
|
|
|
|
2023-07-04 10:45:10 -07:00
|
|
|
class DMShell(BasePrompt):
|
|
|
|
def __init__(self, cache={}):
|
|
|
|
super().__init__(cache)
|
|
|
|
self._name = "DM Shell"
|
2023-07-04 10:59:51 -07:00
|
|
|
self._prompt = ["dm"]
|
|
|
|
self._toolbar = [("class:bold", " DMSH ")]
|
2023-07-04 10:45:10 -07:00
|
|
|
self._key_bindings = BINDINGS
|
2023-07-03 14:14:03 -07:00
|
|
|
self._register_subshells()
|
|
|
|
self._register_keybindings()
|
2023-07-04 11:17:46 -07:00
|
|
|
|
2023-08-20 12:07:10 -07:00
|
|
|
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']
|
|
|
|
|
2023-07-03 14:14:03 -07:00
|
|
|
def _register_keybindings(self):
|
2023-07-04 10:59:51 -07:00
|
|
|
self._toolbar.extend(
|
|
|
|
[
|
2023-07-04 11:17:46 -07:00
|
|
|
("", " [?] Help "),
|
|
|
|
("", " [F2] Wild Magic Table "),
|
2023-08-22 10:49:21 -07:00
|
|
|
("", " [F3] Trinkets"),
|
|
|
|
("", " [F4] NPC"),
|
|
|
|
("", " [F5] Date"),
|
2023-08-22 12:16:11 -07:00
|
|
|
("", " [F6] Job"),
|
2023-08-20 12:07:10 -07:00
|
|
|
("", " [F8] Save"),
|
2023-07-04 11:17:46 -07:00
|
|
|
("", " [^Q] Quit "),
|
2023-07-04 10:59:51 -07:00
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
@self.key_bindings.add("c-q")
|
|
|
|
@self.key_bindings.add("c-d")
|
2023-07-04 11:17:46 -07:00
|
|
|
@self.key_bindings.add("<sigint>")
|
2023-07-03 14:14:03 -07:00
|
|
|
def quit(event):
|
|
|
|
self.quit()
|
2023-07-04 11:17:46 -07:00
|
|
|
|
|
|
|
@self.key_bindings.add("?")
|
2023-07-03 14:14:03 -07:00
|
|
|
def help(event):
|
|
|
|
self.help()
|
2023-07-04 11:17:46 -07:00
|
|
|
|
|
|
|
@self.key_bindings.add("f2")
|
2023-07-03 14:14:03 -07:00
|
|
|
def wmt(event):
|
|
|
|
self.wmt()
|
2023-07-04 11:17:46 -07:00
|
|
|
|
2023-07-04 12:14:23 -07:00
|
|
|
@self.key_bindings.add("f3")
|
2023-08-22 10:49:21 -07:00
|
|
|
def trinkets(event):
|
|
|
|
self.trinkets()
|
|
|
|
|
|
|
|
@self.key_bindings.add("f4")
|
2023-07-04 12:14:23 -07:00
|
|
|
def npc(event):
|
|
|
|
self.npc()
|
|
|
|
|
2023-08-22 10:49:21 -07:00
|
|
|
@self.key_bindings.add("f5")
|
2023-08-20 18:09:24 -07:00
|
|
|
def date(event):
|
|
|
|
self.date()
|
2023-08-20 12:07:10 -07:00
|
|
|
|
2023-08-22 12:16:11 -07:00
|
|
|
@self.key_bindings.add("f6")
|
|
|
|
def job(event):
|
|
|
|
self.job()
|
|
|
|
|
2023-08-20 12:07:10 -07:00
|
|
|
@self.key_bindings.add("f8")
|
|
|
|
def save(event):
|
|
|
|
self.save()
|
|
|
|
|
2023-08-22 10:49:21 -07:00
|
|
|
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):
|
2024-01-06 12:23:05 -08:00
|
|
|
return RollTable(
|
2023-08-22 10:49:21 -07:00
|
|
|
[Path(f"{self.cache['table_sources_path']}/{source}").read_text()],
|
|
|
|
frequency=frequency,
|
|
|
|
die=die
|
2024-01-06 12:23:05 -08:00
|
|
|
).as_table()
|
2023-08-22 10:49:21 -07:00
|
|
|
|
2023-08-20 12:07:10 -07:00
|
|
|
@command(usage="""
|
2023-08-22 10:49:21 -07:00
|
|
|
[title]DATE[/title]
|
2023-08-20 12:07:10 -07:00
|
|
|
|
2023-08-20 18:09:24 -07:00
|
|
|
Work with the Telisaran calendar, including the current campaign date.
|
2023-08-20 12:07:10 -07:00
|
|
|
|
2023-08-20 18:09:24 -07:00
|
|
|
[title]USAGE[/title]
|
|
|
|
|
|
|
|
[link]> date [COMMAND[, ARGS]][/link]
|
|
|
|
|
|
|
|
COMMAND Description
|
2023-08-20 12:07:10 -07:00
|
|
|
|
2023-08-20 18:09:24 -07:00
|
|
|
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].
|
2023-08-20 12:07:10 -07:00
|
|
|
""", completer=WordCompleter(
|
|
|
|
[
|
|
|
|
'season',
|
2023-08-20 18:09:24 -07:00
|
|
|
'year',
|
|
|
|
'inc',
|
|
|
|
'dec',
|
|
|
|
'set',
|
2023-08-20 12:07:10 -07:00
|
|
|
]
|
|
|
|
))
|
2023-08-20 18:09:24 -07:00
|
|
|
def date(self, parts=[]):
|
2023-09-03 17:35:59 -07:00
|
|
|
"""
|
|
|
|
Date and calendaring tools.
|
|
|
|
"""
|
2023-08-20 12:07:10 -07:00
|
|
|
|
|
|
|
if not self.cache['calendar']:
|
2023-08-20 18:09:24 -07:00
|
|
|
self.cache['calendar'] = TelisaranCalendar(today=self._campaign['date'])
|
2023-08-20 12:07:10 -07:00
|
|
|
|
|
|
|
if not parts:
|
2023-08-20 18:09:24 -07:00
|
|
|
self.console.print(f"Today is {self._campaign['date'].short} ({self._campaign['date'].numeric})")
|
2023-08-20 12:07:10 -07:00
|
|
|
return
|
|
|
|
|
2023-08-20 18:09:24 -07:00
|
|
|
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'.")
|
2023-08-20 12:07:10 -07:00
|
|
|
return
|
|
|
|
|
2023-08-20 18:09:24 -07:00
|
|
|
return handler(val)
|
|
|
|
|
2023-08-20 12:07:10 -07:00
|
|
|
@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.")
|
|
|
|
|
2023-07-04 12:14:23 -07:00
|
|
|
@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]
|
2023-12-04 22:00:37 -08:00
|
|
|
""", completer=WordCompleter(list(ANCESTRIES.keys())))
|
2023-07-04 12:14:23 -07:00
|
|
|
def npc(self, parts=[]):
|
|
|
|
"""
|
|
|
|
Generate an NPC commoner
|
|
|
|
"""
|
2023-12-16 11:55:58 -08:00
|
|
|
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")
|
2023-07-04 12:14:23 -07:00
|
|
|
|
2023-07-03 14:14:03 -07:00
|
|
|
@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.
|
|
|
|
"""
|
2023-08-20 12:07:10 -07:00
|
|
|
self.save()
|
2023-07-04 10:45:10 -07:00
|
|
|
try:
|
|
|
|
get_app().exit()
|
|
|
|
finally:
|
|
|
|
raise SystemExit("")
|
2023-07-04 11:18:31 -07:00
|
|
|
|
2023-07-03 14:14:03 -07:00
|
|
|
@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]
|
|
|
|
""")
|
2023-07-04 10:45:10 -07:00
|
|
|
def help(self, parts=[]):
|
2023-07-03 14:14:03 -07:00
|
|
|
"""
|
|
|
|
Display the help message.
|
|
|
|
"""
|
|
|
|
super().help(parts)
|
|
|
|
return True
|
2023-07-04 11:18:31 -07:00
|
|
|
|
2023-07-04 10:59:51 -07:00
|
|
|
@command(
|
|
|
|
usage="""
|
2023-07-03 14:14:03 -07:00
|
|
|
[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]
|
|
|
|
""",
|
2023-07-04 12:14:23 -07:00
|
|
|
completer=WordCompleter(
|
|
|
|
[
|
|
|
|
"The Blooming Wastes",
|
|
|
|
"Dust River Canyon",
|
|
|
|
"Gopher Gulch",
|
|
|
|
"Calamity Ridge"
|
|
|
|
]
|
|
|
|
),
|
2023-07-04 10:59:51 -07:00
|
|
|
)
|
2023-07-04 10:45:10 -07:00
|
|
|
def loc(self, parts=[]):
|
2023-07-03 14:14:03 -07:00
|
|
|
"""
|
|
|
|
Move the party to a new region of the Sahwat Desert.
|
|
|
|
"""
|
|
|
|
if parts:
|
2023-07-04 10:59:51 -07:00
|
|
|
self.cache["location"] = " ".join(parts)
|
2023-07-04 10:45:10 -07:00
|
|
|
self.console.print(f"The party is in {self.cache['location']}.")
|
2023-07-04 11:18:31 -07:00
|
|
|
|
2023-07-03 14:14:03 -07:00
|
|
|
@command(usage="""
|
|
|
|
[title]OVERLAND TRAVEL[/title]
|
|
|
|
|
|
|
|
[b]ot[/b]
|
|
|
|
|
|
|
|
[title]USAGE[/title]
|
|
|
|
|
|
|
|
[link]ot in[/link]
|
|
|
|
""")
|
2023-07-04 10:45:10 -07:00
|
|
|
def ot(self, parts=[]):
|
2023-07-03 14:14:03 -07:00
|
|
|
"""
|
|
|
|
Increment the date by one day and record
|
|
|
|
"""
|
|
|
|
raise NotImplementedError()
|
2023-07-04 11:18:31 -07:00
|
|
|
|
2023-07-03 14:14:03 -07:00
|
|
|
@command(usage="""
|
|
|
|
[title]WILD MAGIC TABLE[/title]
|
|
|
|
|
2023-07-04 10:45:10 -07:00
|
|
|
[b]wmt[/b] Generates a d20 wild magic surge roll table. The table will be cached for the cache.
|
2023-07-03 14:14:03 -07:00
|
|
|
|
|
|
|
[title]USAGE[/title]
|
|
|
|
|
|
|
|
[link]> wmt[/link]
|
|
|
|
|
|
|
|
[title]CLI[/title]
|
|
|
|
|
|
|
|
[link]roll-table \\
|
|
|
|
sources/sahwat_magic_table.yaml \\
|
|
|
|
--frequency default --die 20[/link]
|
|
|
|
""")
|
2023-07-04 12:14:23 -07:00
|
|
|
def wmt(self, parts=[], source="sahwat_magic_table.yaml"):
|
2023-07-03 14:14:03 -07:00
|
|
|
"""
|
|
|
|
Generate a Wild Magic Table for resolving spell effects.
|
|
|
|
"""
|
2023-07-04 10:59:51 -07:00
|
|
|
if "wmt" not in self.cache:
|
2023-08-22 10:49:21 -07:00
|
|
|
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"):
|
2023-09-03 17:35:59 -07:00
|
|
|
"""
|
|
|
|
Generate a trinkets roll table.
|
|
|
|
"""
|
2023-08-22 10:49:21 -07:00
|
|
|
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=[]):
|
2023-09-03 17:35:59 -07:00
|
|
|
"""
|
|
|
|
Get or set the current level of the party.
|
|
|
|
"""
|
2023-08-22 10:49:21 -07:00
|
|
|
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']}.")
|
2023-08-22 12:16:11 -07:00
|
|
|
|
|
|
|
@command(usage="""
|
|
|
|
[title]JOB[/title]
|
|
|
|
|
|
|
|
Generate a random job.
|
|
|
|
|
|
|
|
[title]USAGE[/title]
|
|
|
|
|
|
|
|
[link]> job[/link]
|
|
|
|
|
|
|
|
""")
|
|
|
|
def job(self, parts=[]):
|
2023-09-03 17:35:59 -07:00
|
|
|
"""
|
|
|
|
Generate a random jobs table.
|
|
|
|
"""
|
2023-08-22 12:16:11 -07:00
|
|
|
self.console.print(jobs.generate_job())
|
|
|
|
|
2023-09-03 17:35:59 -07:00
|
|
|
@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))
|