dnd/deadsands/site_tools/shell/interactive_shell.py

381 lines
9.9 KiB
Python
Raw Normal View History

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
2023-07-04 10:59:51 -07:00
from rich.table import Table
from rolltable.tables 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-07-03 14:14:03 -07:00
2023-07-04 12:14:23 -07:00
from npc.generator.base import generate_npc
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-07-04 10:45:10 -07:00
BINDINGS = KeyBindings()
2023-07-03 14:14:03 -07: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):
rt = RollTable(
[Path(f"{self.cache['table_sources_path']}/{source}").read_text()],
frequency=frequency,
die=die
)
table = Table(*rt.rows[0])
for row in rt.rows[1:]:
table.add_row(*row)
return table
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-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]
""", completer=WordCompleter(
[
'human',
'dragon',
'drow',
'dwarf',
'elf',
'highelf',
'halfling',
'halforc',
'tiefling',
'hightiefling',
]
))
def npc(self, parts=[]):
"""
Generate an NPC commoner
"""
c = generate_npc(ancestry=parts[0] if parts else None)
self.console.print("\n".join([
"",
f"{c.description}",
f"Personality: {c.personality}",
f"Flaw: {c.flaw}",
f"Goal: {c.goal}",
"",
]))
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"):
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=[]):
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=[]):
self.console.print(jobs.generate_job())