From 2dbbdbe708c5c793d782cb5344cbe57744f0b3f9 Mon Sep 17 00:00:00 2001 From: rr- Date: Sun, 4 Nov 2018 16:17:14 +0100 Subject: [PATCH] ui: sketch game view --- pqcli/mechanic.py | 42 +++++-- pqcli/ui/__init__.py | 7 +- pqcli/ui/confirm_dialog.py | 6 +- pqcli/ui/{button.py => custom_button.py} | 2 +- pqcli/ui/{line_box.py => custom_line_box.py} | 2 +- pqcli/ui/custom_progress_bar.py | 9 ++ pqcli/ui/game_view.py | 115 +++++++++++++++++++ pqcli/ui/new_game_view.py | 20 ++-- pqcli/ui/roster_view.py | 10 +- 9 files changed, 185 insertions(+), 28 deletions(-) rename pqcli/ui/{button.py => custom_button.py} (95%) rename pqcli/ui/{line_box.py => custom_line_box.py} (91%) create mode 100644 pqcli/ui/custom_progress_bar.py create mode 100644 pqcli/ui/game_view.py diff --git a/pqcli/mechanic.py b/pqcli/mechanic.py index 1ddb84c..06c4b9e 100644 --- a/pqcli/mechanic.py +++ b/pqcli/mechanic.py @@ -4,6 +4,8 @@ import logging import typing as T from dataclasses import dataclass +import urwid + from pqcli import random from pqcli.config import * from pqcli.lingo import * @@ -11,19 +13,37 @@ from pqcli.lingo import * logger = logging.getLogger(__name__) +class SignalMixin: + def __init_subclass__(cls: T.Any, **kwargs: T.Any) -> None: + urwid.signals.register_signal(cls, cls.signals) + super().__init_subclass__(**kwargs) + + def emit(self, signal_name: str, user_data: T.Any = None) -> None: + urwid.signals.emit_signal(self, signal_name, user_data) + + def connect(self, signal_name: str, callback: T.Callable) -> None: + urwid.signals.connect_signal( + self, signal_name, lambda _sender, *args: callback(*args) + ) + + def level_up_time(level: int) -> int: # seconds return 20 * level * 60 -class Bar: +class Bar(SignalMixin): + signals = ["change"] + def __init__(self, max_: int, position: float = 0.0) -> None: self.position = position self.max_ = max_ + self.emit("change") def reset(self, new_max: int, position: float = 0.0) -> None: self.max_ = new_max self.position = position + self.emit("change") def increment(self, inc: float) -> None: self.reposition(self.position + inc) @@ -34,9 +54,12 @@ class Bar: def reposition(self, new_pos: float) -> None: self.position = float(min(new_pos, self.max_)) + self.emit("change") -class Stats: +class Stats(SignalMixin): + signals = ["change"] + def __init__(self, values: T.Dict[StatType, int]) -> None: self.values = values @@ -49,6 +72,7 @@ class Stats: def increment(self, stat: StatType, qty: int = 1) -> None: self.values[stat] += qty logger.info("Increased %s to %d", stat.value, self[stat]) + self.emit("change") class QuestBook: @@ -184,7 +208,9 @@ class PlotTask(RegularTask): pass -class Player: +class Player(SignalMixin): + signals = ["new_task", "level_up"] + def __init__( self, name: str, @@ -244,6 +270,7 @@ class Player: self.task = task self.task_bar.reset(task.duration) logger.info("%s...", task.description) + self.emit("new_task") def equip_price(self) -> int: return 5 * (self.level ** 2) + 10 * self.level + 20 @@ -263,6 +290,7 @@ class Player: self.win_stat() self.win_spell() self.exp_bar.reset(level_up_time(self.level)) + self.emit("level_up") def win_stat(self) -> bool: chosen_stat: T.Optional[StatType] = None @@ -346,12 +374,12 @@ class Simulation: def __init__(self, player: Player) -> None: self.player = player self.last_tick = datetime.datetime.now() - self.elapsed = 0 + self.elapsed = 0.0 - def tick(self) -> None: + def tick(self, elapsed: float = 100.0) -> None: if not self.player.task_bar.done: - self.elapsed = 100 - self.player.task_bar.increment(100) + self.elapsed += elapsed + self.player.task_bar.increment(elapsed) return # gain XP / level up diff --git a/pqcli/ui/__init__.py b/pqcli/ui/__init__.py index 1d0df2e..d62585f 100644 --- a/pqcli/ui/__init__.py +++ b/pqcli/ui/__init__.py @@ -6,6 +6,7 @@ from pqcli import random from pqcli.mechanic import Player from pqcli.roster import Roster from pqcli.ui.confirm_dialog import ConfirmDialog +from pqcli.ui.game_view import GameView from pqcli.ui.new_game_view import NewGameView from pqcli.ui.roster_view import RosterView @@ -17,6 +18,8 @@ PALETTE: T.List[T.Tuple[str, str, str]] = [ ("linebox-focus", "light red", "black"), ("linebox-content", "", ""), ("linebox-content-focus", "", ""), + ("progressbar-done", "black", "light red"), + ("progressbar-incomplete", "", ""), ] @@ -93,7 +96,9 @@ class Ui: self.switch_to_roster_view() def switch_to_game_view(self, player_idx: int) -> None: - raise NotImplementedError("not implemented") + player = self.roster.players[player_idx] + self.loop.widget = GameView(self.loop, player) + self._connect("cancel", self.switch_to_roster_view) def switch_to_delete_player_view(self, player_idx: int) -> None: adjective = random.choice(["faithful", "noble", "loyal", "brave"]) diff --git a/pqcli/ui/confirm_dialog.py b/pqcli/ui/confirm_dialog.py index 1476996..7ee41ad 100644 --- a/pqcli/ui/confirm_dialog.py +++ b/pqcli/ui/confirm_dialog.py @@ -2,7 +2,7 @@ import typing as T import urwid -from pqcli.ui.button import MenuButton +from pqcli.ui.custom_button import CustomButton class ConfirmDialog(urwid.Overlay): @@ -10,10 +10,10 @@ class ConfirmDialog(urwid.Overlay): def __init__(self, label: str, parent: urwid.Widget) -> None: question = urwid.Text(("bold", label), "center") - yes_btn = MenuButton( + yes_btn = CustomButton( "Yes", hint="Y", on_press=lambda _user_data: self.confirm() ) - no_btn = MenuButton( + no_btn = CustomButton( "No", hint="N", on_press=lambda _user_data: self.cancel() ) diff --git a/pqcli/ui/button.py b/pqcli/ui/custom_button.py similarity index 95% rename from pqcli/ui/button.py rename to pqcli/ui/custom_button.py index 3970e1f..b64fc1e 100644 --- a/pqcli/ui/button.py +++ b/pqcli/ui/custom_button.py @@ -3,7 +3,7 @@ import typing as T import urwid -class MenuButton(urwid.Button): +class CustomButton(urwid.Button): def __init__( self, label: str, diff --git a/pqcli/ui/line_box.py b/pqcli/ui/custom_line_box.py similarity index 91% rename from pqcli/ui/line_box.py rename to pqcli/ui/custom_line_box.py index 6651d4f..9c0ac49 100644 --- a/pqcli/ui/line_box.py +++ b/pqcli/ui/custom_line_box.py @@ -3,7 +3,7 @@ import typing as T import urwid -class LineBox(urwid.AttrWrap): +class CustomLineBox(urwid.AttrWrap): def __init__( self, widget: urwid.Widget, *args: T.Any, **kwargs: T.Any ) -> None: diff --git a/pqcli/ui/custom_progress_bar.py b/pqcli/ui/custom_progress_bar.py new file mode 100644 index 0000000..eeaec4a --- /dev/null +++ b/pqcli/ui/custom_progress_bar.py @@ -0,0 +1,9 @@ +import urwid + + +class CustomProgressBar(urwid.ProgressBar): + def __init__(self) -> None: + super().__init__("progressbar-incomplete", "progressbar-done", 0, 1) + + def set_max(self, value: int) -> None: + self._done = value diff --git a/pqcli/ui/game_view.py b/pqcli/ui/game_view.py new file mode 100644 index 0000000..021ca80 --- /dev/null +++ b/pqcli/ui/game_view.py @@ -0,0 +1,115 @@ +import typing as T + +import urwid + +from pqcli.config import StatType +from pqcli.mechanic import Player, Simulation +from pqcli.ui.custom_line_box import CustomLineBox +from pqcli.ui.custom_progress_bar import CustomProgressBar + + +class CharacterSheetView(CustomLineBox): + def __init__(self, player: Player) -> None: + self.player = player + + self.level_text = urwid.Text("") + self.stat_texts = {stat: urwid.Text("") for stat in list(StatType)} + + self.list_box = urwid.ListBox( + [ + urwid.Columns([urwid.Text("Name"), urwid.Text(player.name)]), + urwid.Columns( + [urwid.Text("Race"), urwid.Text(player.race.name)] + ), + urwid.Columns( + [urwid.Text("Class"), urwid.Text(player.class_.name)] + ), + urwid.Columns([urwid.Text("Level"), self.level_text]), + urwid.Divider(), + ] + + [ + urwid.Columns([urwid.Text(stat.value), self.stat_texts[stat]]) + for stat in StatType + ] + ) + + self.player.connect("level_up", self.sync_level) + self.player.stats.connect("change", self.sync_stats) + self.sync_level() + self.sync_stats() + + super().__init__(self.list_box, title="Character Sheet") + + def sync_level(self) -> None: + self.level_text.set_text(str(self.player.level)) + + def sync_stats(self) -> None: + for stat in StatType: + self.stat_texts[stat].set_text(str(self.player.stats[stat])) + + +class TaskView(urwid.Pile): + def __init__(self, player: Player) -> None: + self.player = player + + self.current_task_text = urwid.Text("...") + self.current_task_bar = CustomProgressBar() + + self.player.task_bar.connect("change", self.sync_position) + self.player.connect("new_task", self.sync_task_name) + self.sync_position() + self.sync_task_name() + + super().__init__([self.current_task_text, self.current_task_bar]) + + def sync_position(self) -> None: + self.current_task_bar.set_completion(self.player.task_bar.position) + self.current_task_bar.set_max(self.player.task_bar.max_) + + def sync_task_name(self) -> None: + self.current_task_text.set_text( + f"{self.player.task.description}..." if self.player.task else "?" + ) + + def pack(self, size: T.Tuple[int, int]) -> T.Tuple[int, int]: + return (size[0], 2) + + +class GameView(urwid.Pile): + signals = ["cancel"] + + def __init__(self, loop: urwid.MainLoop, player: Player) -> None: + self.loop = loop + self.player = player + self.simulation = Simulation(player) + + self.character_sheet_view = CharacterSheetView(player) + self.task_view = TaskView(player) + + super().__init__( + [ + urwid.Columns( + [ + urwid.Pile([self.character_sheet_view]), + ] + ), + ("pack", self.task_view), + ] + ) + + self.tick() + + def cancel(self) -> None: + self._emit("cancel") + + def tick(self) -> None: + elapsed = 100 # TODO + self.simulation.tick(elapsed) + self.loop.set_alarm_in(0.1, lambda _loop, _user_data: self.tick()) + + def unhandled_input(self, key: str) -> bool: + if key == "esc": + self.cancel() + return True + + return False diff --git a/pqcli/ui/new_game_view.py b/pqcli/ui/new_game_view.py index 33dca94..ea123ba 100644 --- a/pqcli/ui/new_game_view.py +++ b/pqcli/ui/new_game_view.py @@ -6,11 +6,11 @@ import urwid_readline from pqcli.config import CLASSES, PRIME_STATS, RACES from pqcli.mechanic import StatsBuilder, create_player, generate_name -from pqcli.ui.button import MenuButton -from pqcli.ui.line_box import LineBox +from pqcli.ui.custom_button import CustomButton +from pqcli.ui.custom_line_box import CustomLineBox -class StatsBox(LineBox): +class StatsBox(CustomLineBox): def __init__(self) -> None: self.stats_builder = StatsBuilder() self.stats = self.stats_builder.roll() @@ -18,10 +18,10 @@ class StatsBox(LineBox): self.total_label = urwid.Text("0") self.update_values() - roll_button = MenuButton( + roll_button = CustomButton( "Roll", hint="F5", on_press=self.on_roll_press ) - unroll_button = MenuButton( + unroll_button = CustomButton( "Unroll", hint="F6", on_press=self.on_unroll_press ) @@ -69,7 +69,7 @@ class StatsBox(LineBox): ) -class RaceBox(LineBox): +class RaceBox(CustomLineBox): def __init__(self) -> None: self.race = random.choice(RACES) @@ -92,7 +92,7 @@ class RaceBox(LineBox): self.race = user_data -class ClassBox(LineBox): +class ClassBox(CustomLineBox): def __init__(self) -> None: self.class_ = random.choice(CLASSES) @@ -126,7 +126,7 @@ class NewGameView(urwid.Pile): self.char_name_edit = urwid_readline.ReadlineEdit( "Name: ", generate_name() ) - generate_char_name_btn = MenuButton( + generate_char_name_btn = CustomButton( "Generate random name", hint="F7", on_press=self.on_generate_char_name_press, @@ -136,10 +136,10 @@ class NewGameView(urwid.Pile): urwid.Padding( urwid.Pile( [ - MenuButton( + CustomButton( "Sold!", hint="F10", on_press=self.on_confirm_press ), - MenuButton( + CustomButton( "Cancel", hint="Esc", on_press=self.on_cancel_press ), ] diff --git a/pqcli/ui/roster_view.py b/pqcli/ui/roster_view.py index 0c010de..ad259b3 100644 --- a/pqcli/ui/roster_view.py +++ b/pqcli/ui/roster_view.py @@ -3,7 +3,7 @@ import typing as T import urwid from pqcli.roster import Roster -from pqcli.ui.button import MenuButton +from pqcli.ui.custom_button import CustomButton class RosterView(urwid.Filler): @@ -19,14 +19,14 @@ class RosterView(urwid.Filler): buttons.append( urwid.Columns( [ - MenuButton( + CustomButton( label=player.name, on_press=self.on_resume_game_press, user_data=player_idx, ), ( 10, - MenuButton( + CustomButton( label="Delete", on_press=self.on_delete_game_press, user_data=player_idx, @@ -37,7 +37,7 @@ class RosterView(urwid.Filler): ) buttons.append( - MenuButton( + CustomButton( label="Create a new character", hint="F1", on_press=self.on_new_game_press, @@ -45,7 +45,7 @@ class RosterView(urwid.Filler): ) buttons.append( - MenuButton(label="Exit", hint="Esc", on_press=self.on_exit_press) + CustomButton(label="Exit", hint="Esc", on_press=self.on_exit_press) ) super().__init__(