mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-28 22:33:59 -06:00
232 lines
7.2 KiB
Python
232 lines
7.2 KiB
Python
import argparse
|
|
import enum
|
|
import logging
|
|
import signal
|
|
import sys
|
|
import time
|
|
import typing as T
|
|
from datetime import datetime, timedelta
|
|
|
|
from pqcli import lingo
|
|
from pqcli.config import CLASSES, PRIME_STATS, RACES
|
|
from pqcli.mechanic import Player, Simulation, StatsBuilder, create_player
|
|
from pqcli.roster import Roster
|
|
from pqcli.ui.base import BaseUserInterface
|
|
|
|
LOGO = r"""
|
|
____ ___ _
|
|
| _ \ _ __ ___ __ _ _ __ ___ ___ ___ / _ \ _ _ ___ ___| |_
|
|
| |_) | '__/ _ \ / _` | '__/ _ \/ __/ __| | | | | | |/ _ \/ __| __|
|
|
| __/| | | (_) | (_| | | | __/\__ \__ \ |_| | |_| | __/\__ \ |_
|
|
|_| |_| \___/ \__, |_| \___||___/___/\__\_\\__,_|\___||___/\__|
|
|
|___/
|
|
"""
|
|
|
|
|
|
class MainMenu(enum.IntEnum):
|
|
create = 1
|
|
play = 2
|
|
info = 3
|
|
delete = 4
|
|
quit = 5
|
|
|
|
|
|
class BasicUserInterface(BaseUserInterface):
|
|
def __init__(
|
|
self,
|
|
roster: Roster,
|
|
player: T.Optional[Player],
|
|
args: argparse.Namespace,
|
|
) -> None:
|
|
super().__init__(roster, player, args)
|
|
|
|
def signal_handler(sig: T.Any, frame: T.Any) -> None:
|
|
logging.info("Quitting")
|
|
if self.args.use_saves:
|
|
self.roster.save()
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
def run(self) -> None:
|
|
logging.basicConfig(
|
|
stream=sys.stdout,
|
|
level=logging.INFO,
|
|
format="[%(asctime)s] %(message)s",
|
|
)
|
|
|
|
self.logo()
|
|
if self.player:
|
|
self.play(self.player)
|
|
else:
|
|
self.main_menu()
|
|
|
|
def logo(self) -> None:
|
|
print(LOGO)
|
|
|
|
def main_menu(self) -> None:
|
|
while True:
|
|
choice = self.menu(
|
|
[
|
|
(MainMenu.create, "Create a new character"),
|
|
(MainMenu.play, "Play as character"),
|
|
(MainMenu.info, "Query character info"),
|
|
(MainMenu.delete, "Delete a character"),
|
|
(MainMenu.quit, "Quit"),
|
|
]
|
|
)
|
|
|
|
if choice == MainMenu.create:
|
|
player = self.create_player()
|
|
|
|
elif choice == MainMenu.play:
|
|
player = self.choose_player()
|
|
if not player:
|
|
continue
|
|
self.play(player)
|
|
|
|
elif choice == MainMenu.info:
|
|
player = self.choose_player()
|
|
if not player:
|
|
continue
|
|
self.print_player_info(player)
|
|
|
|
elif choice == MainMenu.delete:
|
|
player = self.choose_player()
|
|
if not player:
|
|
continue
|
|
self.delete_player(player)
|
|
|
|
elif choice == MainMenu.quit:
|
|
self.quit()
|
|
break
|
|
|
|
def create_player(self, *, auto_play: bool = True) -> T.Optional[Player]:
|
|
name = input("Name your new character: ")
|
|
if not name:
|
|
print("Cancelled.")
|
|
return None
|
|
|
|
race = self.menu([(race, race.name) for race in RACES])
|
|
class_ = self.menu([(class_, class_.name) for class_ in CLASSES])
|
|
|
|
stats_builder = StatsBuilder()
|
|
while True:
|
|
stats = stats_builder.roll()
|
|
for stat in PRIME_STATS:
|
|
print(f"{stat.value}: {stats[stat]}")
|
|
print(f"Total: {sum(stats[stat] for stat in PRIME_STATS)}")
|
|
if self.confirm("Is this okay?"):
|
|
break
|
|
|
|
player = create_player(
|
|
name=name, race=race, class_=class_, stats=stats
|
|
)
|
|
self.roster.players.append(player)
|
|
if auto_play and self.confirm(
|
|
"Do you want to play as your new character?"
|
|
):
|
|
self.play(player)
|
|
return player
|
|
|
|
def play(self, player: Player) -> None:
|
|
print(f"Playing as {player.name}")
|
|
simulation = Simulation(player)
|
|
last_tick = datetime.now()
|
|
last_level = 0
|
|
while True:
|
|
elapsed = datetime.now() - last_tick
|
|
simulation.tick(elapsed.total_seconds() * 1000)
|
|
last_tick = datetime.now()
|
|
wait = (timedelta(milliseconds=100) - elapsed).total_seconds()
|
|
wait = max(0, wait)
|
|
time.sleep(wait)
|
|
if player.level != last_level:
|
|
last_level = player.level
|
|
self.print_player_info(player)
|
|
if self.args.use_saves:
|
|
self.roster.save_periodically()
|
|
|
|
def print_player_info(self, player: Player) -> None:
|
|
print("--- Character Sheet ---")
|
|
print(f"Name: {player.name}")
|
|
print(f"Race: {player.race.name}")
|
|
print(f"Class: {player.class_.name}")
|
|
print(f"Level: {player.level}")
|
|
print()
|
|
for stat, value in player.stats:
|
|
print(f"{stat.value}: {value}")
|
|
print()
|
|
print("--- Spell Book ---")
|
|
if not player.spell_book:
|
|
print("No spells memorized yet.")
|
|
else:
|
|
for spell in player.spell_book:
|
|
print(f"{spell.name} {lingo.to_roman(spell.level)}")
|
|
print()
|
|
print("--- Equipment ---")
|
|
for equipment_type, name in player.equipment:
|
|
print(f"{equipment_type.value}: {name}")
|
|
print()
|
|
print("--- Inventory ---")
|
|
print(f"Gold: {player.inventory.gold}")
|
|
for item in player.inventory:
|
|
print(f"{item.name}: {item.quantity}")
|
|
print()
|
|
print("--- Plot ---")
|
|
print(f"Current act: {lingo.to_roman(player.quest_book.act)}")
|
|
print(f"Current quest: {player.quest_book.current_quest or '?'}")
|
|
print(
|
|
f"Current task: {player.task.description if player.task else '?'}"
|
|
)
|
|
|
|
def delete_player(self, player: Player) -> None:
|
|
if self.confirm(lingo.terminate_message(player.name)):
|
|
del self.roster.players[self.roster.players.index(player)]
|
|
|
|
def quit(self) -> None:
|
|
if self.args.use_saves:
|
|
self.roster.save()
|
|
|
|
def choose_player(self) -> T.Optional[Player]:
|
|
if not self.roster.players:
|
|
print("No characters to choose from!")
|
|
return None
|
|
|
|
return self.menu(
|
|
[(player, player.name) for player in self.roster.players],
|
|
title="Choose your character",
|
|
)
|
|
|
|
def confirm(self, message: str) -> bool:
|
|
while True:
|
|
choice = input(f"{message} [y/n] ")
|
|
if choice.lower() in {"y", "yes", "1"}:
|
|
return True
|
|
if choice.lower() in {"n", "no", "0"}:
|
|
return False
|
|
|
|
def menu(
|
|
self,
|
|
options: T.Sequence[T.Tuple[T.Any, str]],
|
|
title: T.Optional[str] = None,
|
|
) -> T.Any:
|
|
print()
|
|
if title:
|
|
print(f"{title}:")
|
|
for i, item in enumerate(options, 1):
|
|
_, description = item
|
|
print(f"{i}) {description}")
|
|
print()
|
|
|
|
while True:
|
|
try:
|
|
num = int(input("Your choice: "))
|
|
except ValueError:
|
|
print("Not a number")
|
|
continue
|
|
if num not in range(1, len(options) + 1):
|
|
print(f"Expected a number between 1..{len(options)}")
|
|
continue
|
|
return options[num - 1][0]
|