mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-29 06:44:00 -06:00
ui/basic: add basic user interface
This commit is contained in:
+10
-1
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
import xdg
|
import xdg
|
||||||
|
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
|
from pqcli.ui.basic import BasicUserInterface
|
||||||
from pqcli.ui.urwid import UrwidUserInterface
|
from pqcli.ui.urwid import UrwidUserInterface
|
||||||
|
|
||||||
SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat"
|
SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat"
|
||||||
@@ -11,6 +12,9 @@ SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat"
|
|||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(prog="pqcli")
|
parser = argparse.ArgumentParser(prog="pqcli")
|
||||||
|
parser.add_argument(
|
||||||
|
"--basic", action="store_true", help="Use basic user interface"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-config",
|
"--no-config",
|
||||||
dest="use_config",
|
dest="use_config",
|
||||||
@@ -35,7 +39,12 @@ def main() -> None:
|
|||||||
roster = Roster.load(SAVE_PATH)
|
roster = Roster.load(SAVE_PATH)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
UrwidUserInterface(roster, args).run()
|
if args.basic:
|
||||||
|
ui = BasicUserInterface(roster, args)
|
||||||
|
else:
|
||||||
|
ui = UrwidUserInterface(roster, args)
|
||||||
|
|
||||||
|
ui.run()
|
||||||
finally:
|
finally:
|
||||||
if args.use_saves:
|
if args.use_saves:
|
||||||
roster.save()
|
roster.save()
|
||||||
|
|||||||
@@ -122,3 +122,8 @@ def special(m: int, subject: str) -> str:
|
|||||||
return prefix(
|
return prefix(
|
||||||
["Battle-", "cursed ", "Were-", "undead ", "demon "], m, subject, ""
|
["Battle-", "cursed ", "Were-", "undead ", "demon "], m, subject, ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def terminate_message(player_name: str) -> None:
|
||||||
|
adjective = random.choice(["faithful", "noble", "loyal", "brave"])
|
||||||
|
return f"Terminate {adjective} {player_name}?"
|
||||||
|
|||||||
+7
-1
@@ -170,7 +170,7 @@ class Inventory(SignalMixin):
|
|||||||
|
|
||||||
def pop(self, index: int) -> None:
|
def pop(self, index: int) -> None:
|
||||||
item = self._items[index]
|
item = self._items[index]
|
||||||
logger.info("Lost %s (qty=%d)", item.name, item.quantity)
|
logger.info("Lost %s", indefinite(item.name, item.quantity))
|
||||||
self._items.pop(index)
|
self._items.pop(index)
|
||||||
self.sync_encumbrance()
|
self.sync_encumbrance()
|
||||||
self.emit("item_del", item)
|
self.emit("item_del", item)
|
||||||
@@ -237,6 +237,12 @@ class SpellBook(SignalMixin):
|
|||||||
def __iter__(self) -> T.Iterator[Spell]:
|
def __iter__(self) -> T.Iterator[Spell]:
|
||||||
return iter(self._spells)
|
return iter(self._spells)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(self._spells)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._spells)
|
||||||
|
|
||||||
def add(self, spell_name: str, level: int) -> None:
|
def add(self, spell_name: str, level: int) -> None:
|
||||||
for spell in self._spells:
|
for spell in self._spells:
|
||||||
if spell.name == spell_name:
|
if spell.name == spell_name:
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
LOGO = """
|
||||||
|
____ ___ _
|
||||||
|
| _ \ _ __ ___ __ _ _ __ ___ ___ ___ / _ \ _ _ ___ ___| |_
|
||||||
|
| |_) | '__/ _ \ / _` | '__/ _ \/ __/ __| | | | | | |/ _ \/ __| __|
|
||||||
|
| __/| | | (_) | (_| | | | __/\__ \__ \ |_| | |_| | __/\__ \ |_
|
||||||
|
|_| |_| \___/ \__, |_| \___||___/___/\__\_\\\\__,_|\___||___/\__|
|
||||||
|
|___/
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MainMenu(enum.IntEnum):
|
||||||
|
create = 1
|
||||||
|
play = 2
|
||||||
|
info = 3
|
||||||
|
delete = 4
|
||||||
|
quit = 5
|
||||||
|
|
||||||
|
|
||||||
|
class BasicUserInterface:
|
||||||
|
def __init__(self, roster: Roster, args: argparse.Namespace) -> None:
|
||||||
|
self.roster = roster
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print("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()
|
||||||
|
self.main_menu()
|
||||||
|
|
||||||
|
def logo(self) -> None:
|
||||||
|
print(LOGO)
|
||||||
|
|
||||||
|
def main_menu(self):
|
||||||
|
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) -> 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 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)
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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 betwen 1..{len(options)}")
|
||||||
|
continue
|
||||||
|
return options[num - 1][0]
|
||||||
@@ -7,6 +7,7 @@ import urwid
|
|||||||
import xdg
|
import xdg
|
||||||
|
|
||||||
from pqcli import random
|
from pqcli import random
|
||||||
|
from pqcli import lingo
|
||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.urwid.confirm_dialog import ConfirmDialog
|
from pqcli.ui.urwid.confirm_dialog import ConfirmDialog
|
||||||
@@ -132,12 +133,11 @@ class UrwidUserInterface:
|
|||||||
self._connect("cancel", self.switch_to_roster_view)
|
self._connect("cancel", self.switch_to_roster_view)
|
||||||
|
|
||||||
def switch_to_delete_player_view(self, player_idx: int) -> None:
|
def switch_to_delete_player_view(self, player_idx: int) -> None:
|
||||||
adjective = random.choice(["faithful", "noble", "loyal", "brave"])
|
|
||||||
player_name = self.roster.players[player_idx].name
|
player_name = self.roster.players[player_idx].name
|
||||||
|
|
||||||
self.old_views.append(self.loop.widget)
|
self.old_views.append(self.loop.widget)
|
||||||
self.loop.widget = ConfirmDialog(
|
self.loop.widget = ConfirmDialog(
|
||||||
f"Terminate {adjective} {player_name}?", self.old_views[-1]
|
lingo.terminate_message(player_name), self.old_views[-1]
|
||||||
)
|
)
|
||||||
self._connect("confirm", lambda: self.delete_player(player_idx))
|
self._connect("confirm", lambda: self.delete_player(player_idx))
|
||||||
self._connect("cancel", self.cancel_dialog)
|
self._connect("cancel", self.cancel_dialog)
|
||||||
|
|||||||
Reference in New Issue
Block a user