mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-28 22:33:59 -06:00
ui: sketch game view
This commit is contained in:
+35
-7
@@ -4,6 +4,8 @@ import logging
|
|||||||
import typing as T
|
import typing as T
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
|
||||||
from pqcli import random
|
from pqcli import random
|
||||||
from pqcli.config import *
|
from pqcli.config import *
|
||||||
from pqcli.lingo import *
|
from pqcli.lingo import *
|
||||||
@@ -11,19 +13,37 @@ from pqcli.lingo import *
|
|||||||
logger = logging.getLogger(__name__)
|
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:
|
def level_up_time(level: int) -> int:
|
||||||
# seconds
|
# seconds
|
||||||
return 20 * level * 60
|
return 20 * level * 60
|
||||||
|
|
||||||
|
|
||||||
class Bar:
|
class Bar(SignalMixin):
|
||||||
|
signals = ["change"]
|
||||||
|
|
||||||
def __init__(self, max_: int, position: float = 0.0) -> None:
|
def __init__(self, max_: int, position: float = 0.0) -> None:
|
||||||
self.position = position
|
self.position = position
|
||||||
self.max_ = max_
|
self.max_ = max_
|
||||||
|
self.emit("change")
|
||||||
|
|
||||||
def reset(self, new_max: int, position: float = 0.0) -> None:
|
def reset(self, new_max: int, position: float = 0.0) -> None:
|
||||||
self.max_ = new_max
|
self.max_ = new_max
|
||||||
self.position = position
|
self.position = position
|
||||||
|
self.emit("change")
|
||||||
|
|
||||||
def increment(self, inc: float) -> None:
|
def increment(self, inc: float) -> None:
|
||||||
self.reposition(self.position + inc)
|
self.reposition(self.position + inc)
|
||||||
@@ -34,9 +54,12 @@ class Bar:
|
|||||||
|
|
||||||
def reposition(self, new_pos: float) -> None:
|
def reposition(self, new_pos: float) -> None:
|
||||||
self.position = float(min(new_pos, self.max_))
|
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:
|
def __init__(self, values: T.Dict[StatType, int]) -> None:
|
||||||
self.values = values
|
self.values = values
|
||||||
|
|
||||||
@@ -49,6 +72,7 @@ class Stats:
|
|||||||
def increment(self, stat: StatType, qty: int = 1) -> None:
|
def increment(self, stat: StatType, qty: int = 1) -> None:
|
||||||
self.values[stat] += qty
|
self.values[stat] += qty
|
||||||
logger.info("Increased %s to %d", stat.value, self[stat])
|
logger.info("Increased %s to %d", stat.value, self[stat])
|
||||||
|
self.emit("change")
|
||||||
|
|
||||||
|
|
||||||
class QuestBook:
|
class QuestBook:
|
||||||
@@ -184,7 +208,9 @@ class PlotTask(RegularTask):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Player(SignalMixin):
|
||||||
|
signals = ["new_task", "level_up"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -244,6 +270,7 @@ class Player:
|
|||||||
self.task = task
|
self.task = task
|
||||||
self.task_bar.reset(task.duration)
|
self.task_bar.reset(task.duration)
|
||||||
logger.info("%s...", task.description)
|
logger.info("%s...", task.description)
|
||||||
|
self.emit("new_task")
|
||||||
|
|
||||||
def equip_price(self) -> int:
|
def equip_price(self) -> int:
|
||||||
return 5 * (self.level ** 2) + 10 * self.level + 20
|
return 5 * (self.level ** 2) + 10 * self.level + 20
|
||||||
@@ -263,6 +290,7 @@ class Player:
|
|||||||
self.win_stat()
|
self.win_stat()
|
||||||
self.win_spell()
|
self.win_spell()
|
||||||
self.exp_bar.reset(level_up_time(self.level))
|
self.exp_bar.reset(level_up_time(self.level))
|
||||||
|
self.emit("level_up")
|
||||||
|
|
||||||
def win_stat(self) -> bool:
|
def win_stat(self) -> bool:
|
||||||
chosen_stat: T.Optional[StatType] = None
|
chosen_stat: T.Optional[StatType] = None
|
||||||
@@ -346,12 +374,12 @@ class Simulation:
|
|||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
self.last_tick = datetime.datetime.now()
|
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:
|
if not self.player.task_bar.done:
|
||||||
self.elapsed = 100
|
self.elapsed += elapsed
|
||||||
self.player.task_bar.increment(100)
|
self.player.task_bar.increment(elapsed)
|
||||||
return
|
return
|
||||||
|
|
||||||
# gain XP / level up
|
# gain XP / level up
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pqcli import random
|
|||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.confirm_dialog import ConfirmDialog
|
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.new_game_view import NewGameView
|
||||||
from pqcli.ui.roster_view import RosterView
|
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-focus", "light red", "black"),
|
||||||
("linebox-content", "", ""),
|
("linebox-content", "", ""),
|
||||||
("linebox-content-focus", "", ""),
|
("linebox-content-focus", "", ""),
|
||||||
|
("progressbar-done", "black", "light red"),
|
||||||
|
("progressbar-incomplete", "", ""),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +96,9 @@ class Ui:
|
|||||||
self.switch_to_roster_view()
|
self.switch_to_roster_view()
|
||||||
|
|
||||||
def switch_to_game_view(self, player_idx: int) -> None:
|
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:
|
def switch_to_delete_player_view(self, player_idx: int) -> None:
|
||||||
adjective = random.choice(["faithful", "noble", "loyal", "brave"])
|
adjective = random.choice(["faithful", "noble", "loyal", "brave"])
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import typing as T
|
|||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from pqcli.ui.button import MenuButton
|
from pqcli.ui.custom_button import CustomButton
|
||||||
|
|
||||||
|
|
||||||
class ConfirmDialog(urwid.Overlay):
|
class ConfirmDialog(urwid.Overlay):
|
||||||
@@ -10,10 +10,10 @@ class ConfirmDialog(urwid.Overlay):
|
|||||||
|
|
||||||
def __init__(self, label: str, parent: urwid.Widget) -> None:
|
def __init__(self, label: str, parent: urwid.Widget) -> None:
|
||||||
question = urwid.Text(("bold", label), "center")
|
question = urwid.Text(("bold", label), "center")
|
||||||
yes_btn = MenuButton(
|
yes_btn = CustomButton(
|
||||||
"Yes", hint="Y", on_press=lambda _user_data: self.confirm()
|
"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()
|
"No", hint="N", on_press=lambda _user_data: self.cancel()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import typing as T
|
|||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
|
|
||||||
class MenuButton(urwid.Button):
|
class CustomButton(urwid.Button):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
label: str,
|
label: str,
|
||||||
@@ -3,7 +3,7 @@ import typing as T
|
|||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
|
|
||||||
class LineBox(urwid.AttrWrap):
|
class CustomLineBox(urwid.AttrWrap):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, widget: urwid.Widget, *args: T.Any, **kwargs: T.Any
|
self, widget: urwid.Widget, *args: T.Any, **kwargs: T.Any
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+10
-10
@@ -6,11 +6,11 @@ import urwid_readline
|
|||||||
|
|
||||||
from pqcli.config import CLASSES, PRIME_STATS, RACES
|
from pqcli.config import CLASSES, PRIME_STATS, RACES
|
||||||
from pqcli.mechanic import StatsBuilder, create_player, generate_name
|
from pqcli.mechanic import StatsBuilder, create_player, generate_name
|
||||||
from pqcli.ui.button import MenuButton
|
from pqcli.ui.custom_button import CustomButton
|
||||||
from pqcli.ui.line_box import LineBox
|
from pqcli.ui.custom_line_box import CustomLineBox
|
||||||
|
|
||||||
|
|
||||||
class StatsBox(LineBox):
|
class StatsBox(CustomLineBox):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.stats_builder = StatsBuilder()
|
self.stats_builder = StatsBuilder()
|
||||||
self.stats = self.stats_builder.roll()
|
self.stats = self.stats_builder.roll()
|
||||||
@@ -18,10 +18,10 @@ class StatsBox(LineBox):
|
|||||||
self.total_label = urwid.Text("0")
|
self.total_label = urwid.Text("0")
|
||||||
self.update_values()
|
self.update_values()
|
||||||
|
|
||||||
roll_button = MenuButton(
|
roll_button = CustomButton(
|
||||||
"Roll", hint="F5", on_press=self.on_roll_press
|
"Roll", hint="F5", on_press=self.on_roll_press
|
||||||
)
|
)
|
||||||
unroll_button = MenuButton(
|
unroll_button = CustomButton(
|
||||||
"Unroll", hint="F6", on_press=self.on_unroll_press
|
"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:
|
def __init__(self) -> None:
|
||||||
self.race = random.choice(RACES)
|
self.race = random.choice(RACES)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class RaceBox(LineBox):
|
|||||||
self.race = user_data
|
self.race = user_data
|
||||||
|
|
||||||
|
|
||||||
class ClassBox(LineBox):
|
class ClassBox(CustomLineBox):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.class_ = random.choice(CLASSES)
|
self.class_ = random.choice(CLASSES)
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class NewGameView(urwid.Pile):
|
|||||||
self.char_name_edit = urwid_readline.ReadlineEdit(
|
self.char_name_edit = urwid_readline.ReadlineEdit(
|
||||||
"Name: ", generate_name()
|
"Name: ", generate_name()
|
||||||
)
|
)
|
||||||
generate_char_name_btn = MenuButton(
|
generate_char_name_btn = CustomButton(
|
||||||
"Generate random name",
|
"Generate random name",
|
||||||
hint="F7",
|
hint="F7",
|
||||||
on_press=self.on_generate_char_name_press,
|
on_press=self.on_generate_char_name_press,
|
||||||
@@ -136,10 +136,10 @@ class NewGameView(urwid.Pile):
|
|||||||
urwid.Padding(
|
urwid.Padding(
|
||||||
urwid.Pile(
|
urwid.Pile(
|
||||||
[
|
[
|
||||||
MenuButton(
|
CustomButton(
|
||||||
"Sold!", hint="F10", on_press=self.on_confirm_press
|
"Sold!", hint="F10", on_press=self.on_confirm_press
|
||||||
),
|
),
|
||||||
MenuButton(
|
CustomButton(
|
||||||
"Cancel", hint="Esc", on_press=self.on_cancel_press
|
"Cancel", hint="Esc", on_press=self.on_cancel_press
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import typing as T
|
|||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.button import MenuButton
|
from pqcli.ui.custom_button import CustomButton
|
||||||
|
|
||||||
|
|
||||||
class RosterView(urwid.Filler):
|
class RosterView(urwid.Filler):
|
||||||
@@ -19,14 +19,14 @@ class RosterView(urwid.Filler):
|
|||||||
buttons.append(
|
buttons.append(
|
||||||
urwid.Columns(
|
urwid.Columns(
|
||||||
[
|
[
|
||||||
MenuButton(
|
CustomButton(
|
||||||
label=player.name,
|
label=player.name,
|
||||||
on_press=self.on_resume_game_press,
|
on_press=self.on_resume_game_press,
|
||||||
user_data=player_idx,
|
user_data=player_idx,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
10,
|
10,
|
||||||
MenuButton(
|
CustomButton(
|
||||||
label="Delete",
|
label="Delete",
|
||||||
on_press=self.on_delete_game_press,
|
on_press=self.on_delete_game_press,
|
||||||
user_data=player_idx,
|
user_data=player_idx,
|
||||||
@@ -37,7 +37,7 @@ class RosterView(urwid.Filler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
buttons.append(
|
buttons.append(
|
||||||
MenuButton(
|
CustomButton(
|
||||||
label="Create a new character",
|
label="Create a new character",
|
||||||
hint="F1",
|
hint="F1",
|
||||||
on_press=self.on_new_game_press,
|
on_press=self.on_new_game_press,
|
||||||
@@ -45,7 +45,7 @@ class RosterView(urwid.Filler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
buttons.append(
|
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__(
|
super().__init__(
|
||||||
|
|||||||
Reference in New Issue
Block a user