mirror of
https://github.com/rr-/pq-cli.git
synced 2026-09-29 14:54:00 -06:00
Compare commits
92
Commits
0.2
..
cli_player
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef9a7f9e20 | ||
|
|
1ad563ad93 | ||
|
|
29207c1d4c | ||
|
|
67c3888f82 | ||
|
|
d4675c747a | ||
|
|
c5f0c60ec8 | ||
|
|
fc4dcff9e3 | ||
|
|
f8a030a648 | ||
|
|
82c8d21395 | ||
|
|
746ca551f3 | ||
|
|
5d47a0f44c | ||
|
|
555f386b24 | ||
|
|
5f34a85c80 | ||
|
|
97552a711a | ||
|
|
561f28bd95 | ||
|
|
0bd4583c73 | ||
|
|
adc3c7914d | ||
|
|
d2c06b08bb | ||
|
|
4dc674816b | ||
|
|
8c6b1a2504 | ||
|
|
8d385523ad | ||
|
|
05c9bcd0e6 | ||
|
|
2c3da6484b | ||
|
|
d4f9326759 | ||
|
|
e85a8d5826 | ||
|
|
2da46a44a4 | ||
|
|
0ecfc2c1fd | ||
|
|
ffe5734433 | ||
|
|
ae99f1f1a4 | ||
|
|
a47d6233be | ||
|
|
1980049c6f | ||
|
|
9fc078ad94 | ||
|
|
7a835c615e | ||
|
|
9412a03da7 | ||
|
|
9012363437 | ||
|
|
021d7eb602 | ||
|
|
3d8034b285 | ||
|
|
bd3f196f84 | ||
|
|
9a90b7b7b4 | ||
|
|
e3ffe51979 | ||
|
|
0133802570 | ||
|
|
b20c4b4b34 | ||
|
|
5bdfc2ed25 | ||
|
|
8b38eeecac | ||
|
|
ed9e513271 | ||
|
|
97d958d2e7 | ||
|
|
9db61f7191 | ||
|
|
4230c57e73 | ||
|
|
871144bf0c | ||
|
|
3fdc2459ca | ||
|
|
e008fc6048 | ||
|
|
ca7ab192c5 | ||
|
|
81f6dbede2 | ||
|
|
478075347c | ||
|
|
595538cdca | ||
|
|
e27f3470a4 | ||
|
|
5545e2324a | ||
|
|
000b11c26c | ||
|
|
b9a0934e74 | ||
|
|
a823c9041e | ||
|
|
58758d35d5 | ||
|
|
e7d5947299 | ||
|
|
16ec0cfa5d | ||
|
|
ce7a422482 | ||
|
|
3cdafe4ec8 | ||
|
|
9bfbaf1646 | ||
|
|
4bb0e9250a | ||
|
|
a930b2b3db | ||
|
|
2372dcdcaa | ||
|
|
8e25fbea04 | ||
|
|
70ab6eab48 | ||
|
|
37252d2727 | ||
|
|
284f549966 | ||
|
|
904951acf4 | ||
|
|
3b19191120 | ||
|
|
72fe5439b0 | ||
|
|
88a936c293 | ||
|
|
0b74d1a36e | ||
|
|
14149cd112 | ||
|
|
bb233b82b8 | ||
|
|
723c4aae88 | ||
|
|
6bd12797f0 | ||
|
|
066fc692e4 | ||
|
|
86f59d8fb2 | ||
|
|
78e39e639e | ||
|
|
6c2fed9d40 | ||
|
|
b5b9554f9b | ||
|
|
3afa965eeb | ||
|
|
b00cf34e8f | ||
|
|
c7c5abf7c8 | ||
|
|
c8d8d646e8 | ||
|
|
7692d2e531 |
@@ -10,16 +10,23 @@ Relive the great adventure… this time in the terminal realm!
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Saves (with backups) to `$XDG_CONFIG_HOME/pqcli/save.dat`
|
|
||||||
- Clean terminal interface
|
|
||||||
- Smooth progress bars using UTF-8 block characters
|
|
||||||
- Scrollable list boxes
|
|
||||||
- Faithful port of the game logic
|
- Faithful port of the game logic
|
||||||
|
- Saves (with backups) to `$XDG_CONFIG_HOME/pqcli/save.dat`
|
||||||
|
- Terminal interface that comes in 2 flavors:
|
||||||
|
- Rich and colorful (`--curses`, default)
|
||||||
|
- Minimal, suitable for raw grind (`--basic`)
|
||||||
- Ideal to run on your server
|
- Ideal to run on your server
|
||||||
|
|
||||||
## How it looks like
|
## How it looks like
|
||||||
|
|
||||||

|
Curses interface:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Basic interface:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
@@ -34,3 +41,14 @@ $ git clone https://github.com/rr-/pq-cli.git
|
|||||||
$ cd pq-cli
|
$ cd pq-cli
|
||||||
$ pip install --user .
|
$ pip install --user .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `_curses.error: init_pair() returned ERR`
|
||||||
|
|
||||||
|
If running on Linux and you get the error `_curses.error: init_pair() returned ERR`,
|
||||||
|
try making sure that your `$TERM` variable is set to a value which supports
|
||||||
|
256 colors, such as via the following:
|
||||||
|
|
||||||
|
TERM=xterm-256color pqcli
|
||||||
|
|
||||||
|
|||||||
+69
-3
@@ -1,27 +1,93 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import sys
|
||||||
|
import typing as T
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import xdg
|
import xdg
|
||||||
|
|
||||||
|
from pqcli.mechanic import Player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui import Ui
|
from pqcli.ui.basic import BasicUserInterface
|
||||||
|
from pqcli.ui.curses import CursesUserInterface
|
||||||
|
|
||||||
SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat"
|
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("--cheats", help="enable cheats", action="store_true")
|
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.set_defaults(ui=CursesUserInterface)
|
||||||
|
group.add_argument(
|
||||||
|
"--basic",
|
||||||
|
dest="ui",
|
||||||
|
action="store_const",
|
||||||
|
const=BasicUserInterface,
|
||||||
|
help="Use basic user interface (very crude, but uses least CPU)",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--curses",
|
||||||
|
dest="ui",
|
||||||
|
action="store_const",
|
||||||
|
const=CursesUserInterface,
|
||||||
|
help="Use curses user interface (fast, but no colors output)",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-colors",
|
||||||
|
dest="colors",
|
||||||
|
action="store_false",
|
||||||
|
help="Disable color highlighting in curses interface",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-save",
|
||||||
|
dest="use_saves",
|
||||||
|
action="store_false",
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
parser.add_argument("--cheats", action="store_true", help="???")
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-saves",
|
||||||
|
action="store_true",
|
||||||
|
help="list saved characters and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--load-save",
|
||||||
|
type=int,
|
||||||
|
metavar="NUM",
|
||||||
|
help="play chosen character",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def list_players(roster: Roster, file: T.Optional[T.Any] = None) -> None:
|
||||||
|
for i, player in enumerate(roster.players, start=1):
|
||||||
|
print(f"{i}. {player.name}", file=file)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
roster = Roster.load(SAVE_PATH)
|
roster = Roster.load(SAVE_PATH)
|
||||||
|
|
||||||
|
if args.list_saves:
|
||||||
|
list_players(roster)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
player: T.Optional[Player] = None
|
||||||
|
if args.load_save:
|
||||||
try:
|
try:
|
||||||
Ui(roster, args).run()
|
player = list(roster.players)[args.load_save - 1]
|
||||||
|
except IndexError:
|
||||||
|
print(f"Invalid player. Available players:", file=sys.stderr)
|
||||||
|
list_players(roster, file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ui = args.ui(roster, player, args)
|
||||||
|
ui.run()
|
||||||
finally:
|
finally:
|
||||||
|
if args.use_saves:
|
||||||
roster.save()
|
roster.save()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
|
import datetime
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
from pqcli import random
|
from pqcli import random
|
||||||
|
|
||||||
|
|
||||||
|
def format_float(num: float) -> str:
|
||||||
|
ret = f"{num:.01f}"
|
||||||
|
if ret.endswith("0"):
|
||||||
|
ret = ret[:-2]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def format_timespan(timespan: datetime.timedelta) -> str:
|
||||||
|
num = timespan.total_seconds()
|
||||||
|
if num < 60.0:
|
||||||
|
return f"~{int(num)}s"
|
||||||
|
num /= 60
|
||||||
|
if num < 60.0:
|
||||||
|
return f"~{int(num)}m"
|
||||||
|
num /= 60
|
||||||
|
if num < 24.0:
|
||||||
|
return f"~{format_float(num)}h"
|
||||||
|
num /= 24
|
||||||
|
return f"~{format_float(num)}d"
|
||||||
|
|
||||||
|
|
||||||
def generate_name() -> str:
|
def generate_name() -> str:
|
||||||
parts = [
|
parts = [
|
||||||
"br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z".split("|"),
|
"br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z".split("|"),
|
||||||
@@ -122,3 +144,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) -> str:
|
||||||
|
adjective = random.choice(["faithful", "noble", "loyal", "brave"])
|
||||||
|
return f"Terminate {adjective} {player_name}?"
|
||||||
|
|||||||
+80
-54
@@ -1,11 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import typing as T
|
import typing as T
|
||||||
|
from collections import defaultdict
|
||||||
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 *
|
||||||
@@ -13,23 +13,29 @@ from pqcli.lingo import *
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
_SIGNALS: T.Dict[
|
||||||
|
T.Tuple["SignalMixin", str], T.List[T.Callable[..., T.Any]]
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
|
|
||||||
class SignalMixin:
|
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:
|
def emit(self, signal_name: str, *user_data: T.Any) -> None:
|
||||||
urwid.signals.emit_signal(self, signal_name, *user_data)
|
for callback in _SIGNALS[self, signal_name]:
|
||||||
|
callback(*user_data)
|
||||||
|
|
||||||
def connect(self, signal_name: str, callback: T.Callable) -> None:
|
def connect(
|
||||||
urwid.signals.connect_signal(self, signal_name, callback)
|
self, signal_name: str, callback: T.Callable[..., T.Any]
|
||||||
|
) -> None:
|
||||||
|
_SIGNALS[self, signal_name].append(callback)
|
||||||
|
|
||||||
def __getstate__(self) -> T.Any:
|
def disconnect(
|
||||||
return {
|
self, signal_name: str, callback: T.Callable[..., T.Any]
|
||||||
key: value
|
) -> None:
|
||||||
for key, value in self.__dict__.items()
|
try:
|
||||||
if "signal" not in key
|
idx = _SIGNALS[self, signal_name].index(callback)
|
||||||
}
|
except ValueError:
|
||||||
|
return
|
||||||
|
del _SIGNALS[self, signal_name][idx]
|
||||||
|
|
||||||
|
|
||||||
def level_up_time(level: int) -> int:
|
def level_up_time(level: int) -> int:
|
||||||
@@ -46,6 +52,9 @@ class Bar(SignalMixin):
|
|||||||
self.emit("change")
|
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:
|
||||||
|
if not math.isclose(new_max, self._max) or not math.isclose(
|
||||||
|
position, self._position
|
||||||
|
):
|
||||||
self._max = new_max
|
self._max = new_max
|
||||||
self._position = position
|
self._position = position
|
||||||
self.emit("change")
|
self.emit("change")
|
||||||
@@ -66,7 +75,10 @@ class Bar(SignalMixin):
|
|||||||
return self._position >= self.max_
|
return self._position >= self.max_
|
||||||
|
|
||||||
def reposition(self, new_pos: float) -> None:
|
def reposition(self, new_pos: float) -> None:
|
||||||
self._position = float(min(new_pos, self._max))
|
old_pos = self._position
|
||||||
|
new_pos = float(min(new_pos, self._max))
|
||||||
|
self._position = new_pos
|
||||||
|
if not math.isclose(new_pos, old_pos):
|
||||||
self.emit("change")
|
self.emit("change")
|
||||||
|
|
||||||
|
|
||||||
@@ -86,6 +98,12 @@ class Stats(SignalMixin):
|
|||||||
def best(self) -> StatType:
|
def best(self) -> StatType:
|
||||||
return max(self, key=lambda kv: kv[1])[0]
|
return max(self, key=lambda kv: kv[1])[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def best_prime(self) -> StatType:
|
||||||
|
return max(
|
||||||
|
(kv for kv in self if kv[0] in PRIME_STATS), key=lambda kv: kv[1]
|
||||||
|
)[0]
|
||||||
|
|
||||||
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])
|
||||||
@@ -93,7 +111,7 @@ class Stats(SignalMixin):
|
|||||||
|
|
||||||
|
|
||||||
class QuestBook(SignalMixin):
|
class QuestBook(SignalMixin):
|
||||||
signals = ["complete_act", "complete_quest"]
|
signals = ["start_act", "start_quest"]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._quests: T.List[str] = []
|
self._quests: T.List[str] = []
|
||||||
@@ -118,7 +136,7 @@ class QuestBook(SignalMixin):
|
|||||||
|
|
||||||
def increment_act(self) -> None:
|
def increment_act(self) -> None:
|
||||||
self._act += 1
|
self._act += 1
|
||||||
self.emit("complete_act")
|
self.emit("start_act", self._act)
|
||||||
|
|
||||||
def add_quest(self, name: str) -> None:
|
def add_quest(self, name: str) -> None:
|
||||||
logger.info("Commencing quest: %s", name)
|
logger.info("Commencing quest: %s", name)
|
||||||
@@ -164,7 +182,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)
|
||||||
@@ -231,6 +249,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:
|
||||||
@@ -302,6 +326,7 @@ class Player(SignalMixin):
|
|||||||
self.race: Race = race
|
self.race: Race = race
|
||||||
self.class_: Class = class_
|
self.class_: Class = class_
|
||||||
self.stats: Stats = stats
|
self.stats: Stats = stats
|
||||||
|
self.elapsed = 0.0
|
||||||
|
|
||||||
self.exp_bar = Bar(max_=level_up_time(1))
|
self.exp_bar = Bar(max_=level_up_time(1))
|
||||||
self.level = 1
|
self.level = 1
|
||||||
@@ -315,6 +340,10 @@ class Player(SignalMixin):
|
|||||||
self.task: T.Optional[BaseTask] = None
|
self.task: T.Optional[BaseTask] = None
|
||||||
self.queue: T.List[BaseTask] = []
|
self.queue: T.List[BaseTask] = []
|
||||||
|
|
||||||
|
def __setstate__(self, obj: T.Any) -> None:
|
||||||
|
self.__dict__.update(obj)
|
||||||
|
self.elapsed = obj.get("elapsed", 0.0)
|
||||||
|
|
||||||
def set_task(self, task: BaseTask) -> None:
|
def set_task(self, task: BaseTask) -> None:
|
||||||
self.task = task
|
self.task = task
|
||||||
self.task_bar.reset(task.duration)
|
self.task_bar.reset(task.duration)
|
||||||
@@ -423,38 +452,38 @@ 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.0
|
|
||||||
|
|
||||||
def tick(self, elapsed: float = 100.0) -> None:
|
def tick(self, elapsed: float = 100.0) -> None:
|
||||||
|
self.player.elapsed += elapsed
|
||||||
|
|
||||||
if self.player.task is None:
|
if self.player.task is None:
|
||||||
self.player.set_task(RegularTask("Loading", 2_000))
|
self.player.set_task(RegularTask("Loading", 2000))
|
||||||
self.player.queue += [
|
self.player.queue += [
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Experiencing an enigmatic and foreboding night vision",
|
"Experiencing an enigmatic and foreboding night vision",
|
||||||
10_000,
|
10000,
|
||||||
),
|
),
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Much is revealed about that wise old bastard "
|
"Much is revealed about that wise old bastard "
|
||||||
"you'd underestimated",
|
"you'd underestimated",
|
||||||
6_000,
|
6000,
|
||||||
),
|
),
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"A shocking series of events leaves you "
|
"A shocking series of events leaves you "
|
||||||
"alone and bewildered, but resolute",
|
"alone and bewildered, but resolute",
|
||||||
6_000,
|
6000,
|
||||||
),
|
),
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Drawing upon an unrealized reserve of determination, "
|
"Drawing upon an unrealized reserve of determination, "
|
||||||
"you set out on a long and dangerous journey",
|
"you set out on a long and dangerous journey",
|
||||||
4_000,
|
4000,
|
||||||
),
|
),
|
||||||
PlotTask(f"Loading {act_name(1)}", 2_000),
|
PlotTask(f"Loading {act_name(1)}", 2000),
|
||||||
]
|
]
|
||||||
self.player.quest_book.plot_bar.reset(28)
|
self.player.quest_book.plot_bar.reset(28)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.player.task_bar.done:
|
if not self.player.task_bar.done:
|
||||||
self.elapsed += elapsed
|
|
||||||
self.player.task_bar.increment(elapsed)
|
self.player.task_bar.increment(elapsed)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -528,7 +557,7 @@ class Simulation:
|
|||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
SellTask(
|
SellTask(
|
||||||
"Selling " + indefinite(item.name, item.quantity),
|
"Selling " + indefinite(item.name, item.quantity),
|
||||||
1_000,
|
1000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@@ -541,21 +570,19 @@ class Simulation:
|
|||||||
self.player.set_task(self.player.queue.pop(0))
|
self.player.set_task(self.player.queue.pop(0))
|
||||||
elif self.player.inventory.encum_bar.done:
|
elif self.player.inventory.encum_bar.done:
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
HeadingToMarketTask(
|
HeadingToMarketTask("Heading to market to sell loot", 4000)
|
||||||
"Heading to market to sell loot", 4_000
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)):
|
elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)):
|
||||||
if self.player.inventory.gold > self.player.equip_price():
|
if self.player.inventory.gold > self.player.equip_price():
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
BuyTask(
|
BuyTask(
|
||||||
"Negotiating purchase of better equipment", 5_000
|
"Negotiating purchase of better equipment", 5000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
HeadingToKillingFieldsTask(
|
HeadingToKillingFieldsTask(
|
||||||
"Heading to the killing fields", 4_000
|
"Heading to the killing fields", 4000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -612,7 +639,7 @@ class Simulation:
|
|||||||
raise AssertionError
|
raise AssertionError
|
||||||
|
|
||||||
self.player.quest_book.add_quest(caption)
|
self.player.quest_book.add_quest(caption)
|
||||||
self.player.quest_book.emit("complete_quest")
|
self.player.quest_book.emit("start_quest", caption)
|
||||||
|
|
||||||
def interplot_cinematic(self) -> None:
|
def interplot_cinematic(self) -> None:
|
||||||
def enqueue(task: BaseTask) -> None:
|
def enqueue(task: BaseTask) -> None:
|
||||||
@@ -625,19 +652,19 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
"Exhausted, you arrive at a friendly oasis "
|
"Exhausted, you arrive at a friendly oasis "
|
||||||
"in a hostile land",
|
"in a hostile land",
|
||||||
1_000,
|
1000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask("You greet old friends and meet new allies", 2_000)
|
RegularTask("You greet old friends and meet new allies", 2000)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"You are privy to a council of powerful do-gooders", 2_000
|
"You are privy to a council of powerful do-gooders", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask("There is much to be done. You are chosen!", 1_000)
|
RegularTask("There is much to be done. You are chosen!", 1000)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif choice == 1:
|
elif choice == 1:
|
||||||
@@ -645,7 +672,7 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
"Your quarry is in sight, "
|
"Your quarry is in sight, "
|
||||||
"but a mighty enemy bars your path!",
|
"but a mighty enemy bars your path!",
|
||||||
1_000,
|
1000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -653,7 +680,7 @@ class Simulation:
|
|||||||
|
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"A desperate struggle commences with {nemesis}", 4_000
|
f"A desperate struggle commences with {nemesis}", 4000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -665,20 +692,20 @@ class Simulation:
|
|||||||
if s % 3 == 0:
|
if s % 3 == 0:
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"Locked in grim combat with {nemesis}", 2_000
|
f"Locked in grim combat with {nemesis}", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif s % 3 == 1:
|
elif s % 3 == 1:
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"{nemesis} seems to have the upper hand", 2_000
|
f"{nemesis} seems to have the upper hand", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif s % 3 == 2:
|
elif s % 3 == 2:
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"You seem to gain the advantage over {nemesis}",
|
f"You seem to gain the advantage over {nemesis}",
|
||||||
2_000,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -688,12 +715,12 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
f"Victory! {nemesis} is slain! "
|
f"Victory! {nemesis} is slain! "
|
||||||
"Exhausted, you lose conciousness",
|
"Exhausted, you lose conciousness",
|
||||||
3_000,
|
3000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"You awake in a friendly place, but the road awaits", 2_000
|
"You awake in a friendly place, but the road awaits", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -703,37 +730,35 @@ class Simulation:
|
|||||||
RegularTask(
|
RegularTask(
|
||||||
"Oh sweet relief! "
|
"Oh sweet relief! "
|
||||||
f"You've reached the protection of the good {nemesis}",
|
f"You've reached the protection of the good {nemesis}",
|
||||||
2_000,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"There is rejoicing, "
|
"There is rejoicing, "
|
||||||
f"and an unnerving encouter with {nemesis} in private",
|
f"and an unnerving encouter with {nemesis} in private",
|
||||||
3_000,
|
3000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
f"You forget your {boring_item()} and go back to get it",
|
f"You forget your {boring_item()} and go back to get it",
|
||||||
2_000,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"What's this!? You overhear something shocking!", 2_000
|
"What's this!? You overhear something shocking!", 2000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(f"Could {nemesis} be a dirty double-dealer?", 2000)
|
||||||
f"Could {nemesis} be a dirty double-dealer?", 2_000
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Who can possibly be trusted with this news!? ... "
|
"Who can possibly be trusted with this news!? ... "
|
||||||
"Oh yes, of course",
|
"Oh yes, of course",
|
||||||
3_000,
|
3000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -742,7 +767,7 @@ class Simulation:
|
|||||||
|
|
||||||
enqueue(
|
enqueue(
|
||||||
PlotTask(
|
PlotTask(
|
||||||
f"Loading {act_name(self.player.quest_book.act + 1)}", 1_000
|
f"Loading {act_name(self.player.quest_book.act + 1)}", 1000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -892,6 +917,7 @@ class StatsBuilder:
|
|||||||
return stats
|
return stats
|
||||||
|
|
||||||
def unroll(self) -> Stats:
|
def unroll(self) -> Stats:
|
||||||
|
if len(self.history) > 1:
|
||||||
self.history.pop()
|
self.history.pop()
|
||||||
return self.history[-1]
|
return self.history[-1]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import logging
|
||||||
import pickle
|
import pickle
|
||||||
import typing as T
|
import typing as T
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
@@ -9,6 +11,7 @@ class Roster:
|
|||||||
def __init__(self, path: Path, players: T.List[Player]) -> None:
|
def __init__(self, path: Path, players: T.List[Player]) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.players = players
|
self.players = players
|
||||||
|
self._last_save = datetime.now()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(path: T.Union[str, Path]) -> "Roster":
|
def load(path: T.Union[str, Path]) -> "Roster":
|
||||||
@@ -20,10 +23,18 @@ class Roster:
|
|||||||
return Roster(real_path, players=[])
|
return Roster(real_path, players=[])
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
|
self._last_save = datetime.now()
|
||||||
old_path = self.path.with_name(self.path.name + ".old")
|
old_path = self.path.with_name(self.path.name + ".old")
|
||||||
tmp_path = self.path.with_name(self.path.name + ".new")
|
tmp_path = self.path.with_name(self.path.name + ".new")
|
||||||
tmp_path.parent.mkdir(parents=True, exist_ok=True)
|
tmp_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
tmp_path.write_bytes(pickle.dumps(self.players))
|
tmp_path.write_bytes(pickle.dumps(self.players))
|
||||||
if self.path.exists():
|
if self.path.exists():
|
||||||
|
if old_path.exists():
|
||||||
|
old_path.unlink()
|
||||||
self.path.rename(old_path)
|
self.path.rename(old_path)
|
||||||
tmp_path.rename(self.path)
|
tmp_path.rename(self.path)
|
||||||
|
|
||||||
|
def save_periodically(self) -> None:
|
||||||
|
if (datetime.now() - self._last_save).total_seconds() >= 300:
|
||||||
|
logging.info("Saving...")
|
||||||
|
self.save()
|
||||||
|
|||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from signal import SIG_DFL, SIGPIPE, signal
|
|
||||||
|
|
||||||
from pqcli import random
|
|
||||||
from pqcli.config import CLASSES, RACES
|
|
||||||
from pqcli.mechanic import (
|
|
||||||
Player,
|
|
||||||
Simulation,
|
|
||||||
StatsBuilder,
|
|
||||||
act_name,
|
|
||||||
create_player,
|
|
||||||
generate_name,
|
|
||||||
)
|
|
||||||
from pqcli.roster import Roster
|
|
||||||
|
|
||||||
signal(SIGPIPE, SIG_DFL)
|
|
||||||
logging.basicConfig(
|
|
||||||
stream=sys.stdout, level=logging.DEBUG, format="%(message)s"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# make things predictable
|
|
||||||
SEED_A = 1664525
|
|
||||||
SEED_B = 2141
|
|
||||||
SEED_C = 1013904223
|
|
||||||
|
|
||||||
|
|
||||||
def custom_random(num: int) -> int:
|
|
||||||
global SEED_A, SEED_B, SEED_C
|
|
||||||
SEED_B = (SEED_A * SEED_B + SEED_C) % 0x100000000
|
|
||||||
return SEED_B % num
|
|
||||||
|
|
||||||
|
|
||||||
random.below = custom_random
|
|
||||||
|
|
||||||
|
|
||||||
def character_sheet(player: Player) -> None:
|
|
||||||
print(f"{player.name} the {player.race.name}")
|
|
||||||
print(f"Level {player.level} {player.class_.name}")
|
|
||||||
print(f"Plot stage: {act_name(player.quest_book.act)}")
|
|
||||||
print(f"Quest: {player.quest_book.current_quest}")
|
|
||||||
print()
|
|
||||||
print("Stats:")
|
|
||||||
for stat, value in player.stats:
|
|
||||||
print(f" {stat.value}: {value}")
|
|
||||||
print()
|
|
||||||
print(f"Equipment (best: {player.equipment.best}):")
|
|
||||||
for equipment, item_name in player.equipment:
|
|
||||||
print(f" {equipment.value}: {item_name}")
|
|
||||||
print()
|
|
||||||
print("Spell Book:")
|
|
||||||
for spell in player.spell_book:
|
|
||||||
print(f" {spell.name}: {spell.level}")
|
|
||||||
print()
|
|
||||||
print("Inventory:")
|
|
||||||
print(f" Gold: {player.inventory.gold}")
|
|
||||||
for item in player.inventory:
|
|
||||||
print(f" {item.name}: {item.quantity}")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-r", "--roster", type=Path)
|
|
||||||
parser.add_argument("-l", "--max-level", type=int, default=5)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def random_player() -> Player:
|
|
||||||
return create_player(
|
|
||||||
stats=StatsBuilder().roll(),
|
|
||||||
name=generate_name(),
|
|
||||||
race=random.choice(RACES),
|
|
||||||
class_=random.choice(CLASSES),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
|
|
||||||
if args.roster:
|
|
||||||
roster = Roster.load(args.roster)
|
|
||||||
if not roster.players:
|
|
||||||
roster.players.append(random_player())
|
|
||||||
roster.save()
|
|
||||||
player = roster.players[0]
|
|
||||||
else:
|
|
||||||
roster = None
|
|
||||||
player = random_player()
|
|
||||||
|
|
||||||
start = datetime.datetime.now()
|
|
||||||
elapsed = 0
|
|
||||||
last_level = 0
|
|
||||||
simulation = Simulation(player)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
if player.level != last_level:
|
|
||||||
last_level = player.level
|
|
||||||
character_sheet(player)
|
|
||||||
if player.level >= args.max_level:
|
|
||||||
break
|
|
||||||
simulation.tick()
|
|
||||||
elapsed += 100
|
|
||||||
finally:
|
|
||||||
print("real time: ", (datetime.datetime.now() - start))
|
|
||||||
print("virtual time: ", datetime.timedelta(milliseconds=elapsed))
|
|
||||||
if roster:
|
|
||||||
roster.save()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
TICK_FREQ = 0.1
|
|
||||||
SAVE_FREQ = 60.0
|
|
||||||
PALETTE: T.List[T.Tuple[str, ...]] = [
|
|
||||||
("button", "", ""),
|
|
||||||
("button-focus", "light red", "black"),
|
|
||||||
("linebox", "", ""),
|
|
||||||
("linebox-focus", "light red", "black"),
|
|
||||||
("linebox-content", "", ""),
|
|
||||||
("linebox-content-focus", "", ""),
|
|
||||||
("progressbar-normal", "", "", "standout"),
|
|
||||||
("progressbar-done", "black", "light red"),
|
|
||||||
("progressbar-smooth", "light red", ""),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmExitDialog(ConfirmDialog):
|
|
||||||
def __init__(self, old_view: urwid.Widget) -> None:
|
|
||||||
super().__init__("Really quit?", old_view)
|
|
||||||
|
|
||||||
|
|
||||||
def bind_commands() -> None:
|
|
||||||
for key, direction in {
|
|
||||||
"k": "up",
|
|
||||||
"j": "down",
|
|
||||||
"h": "left",
|
|
||||||
"l": "right",
|
|
||||||
}.items():
|
|
||||||
for key_variant in {key, key.upper()}:
|
|
||||||
urwid.command_map[key_variant] = f"cursor {direction}"
|
|
||||||
urwid.command_map["tab"] = f"cursor down"
|
|
||||||
urwid.command_map["shift tab"] = f"cursor up"
|
|
||||||
|
|
||||||
|
|
||||||
class Ui:
|
|
||||||
def __init__(self, roster: Roster, args: argparse.Namespace) -> None:
|
|
||||||
bind_commands()
|
|
||||||
|
|
||||||
self.roster = roster
|
|
||||||
self.args = args
|
|
||||||
self.loop = urwid.MainLoop(
|
|
||||||
None, PALETTE, unhandled_input=self.unhandled_input
|
|
||||||
)
|
|
||||||
self.old_views: T.List[urwid.Widget] = []
|
|
||||||
|
|
||||||
self.switch_to_roster_view()
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self.loop.set_alarm_in(TICK_FREQ, self.on_tick)
|
|
||||||
self.loop.set_alarm_in(SAVE_FREQ, self.on_save)
|
|
||||||
self.loop.run()
|
|
||||||
|
|
||||||
def unhandled_input(self, key: str) -> bool:
|
|
||||||
callback = getattr(self.loop.widget, "unhandled_input", None)
|
|
||||||
if callback and callback(key):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if key == "ctrl q":
|
|
||||||
self.switch_to_exit_view()
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def switch_to_roster_view(self) -> None:
|
|
||||||
self.loop.widget = RosterView(self.roster)
|
|
||||||
self._connect("load_game", self.switch_to_game_view)
|
|
||||||
self._connect("new_game", self.switch_to_new_game_view)
|
|
||||||
self._connect("delete_game", self.switch_to_delete_player_view)
|
|
||||||
self._connect("exit", self.switch_to_exit_view)
|
|
||||||
|
|
||||||
def switch_to_exit_view(self) -> None:
|
|
||||||
if isinstance(self.loop.widget, ConfirmExitDialog):
|
|
||||||
return
|
|
||||||
self.old_views.append(self.loop.widget)
|
|
||||||
self.loop.widget = ConfirmExitDialog(self.old_views[-1])
|
|
||||||
self._connect("confirm", self.exit)
|
|
||||||
self._connect("cancel", self.cancel_dialog)
|
|
||||||
|
|
||||||
def switch_to_new_game_view(self) -> None:
|
|
||||||
self.loop.widget = NewGameView()
|
|
||||||
self._connect("confirm", self.create_player)
|
|
||||||
self._connect("cancel", self.switch_to_roster_view)
|
|
||||||
|
|
||||||
def create_player(self, player: Player) -> None:
|
|
||||||
self.roster.players.append(player)
|
|
||||||
self.switch_to_roster_view()
|
|
||||||
|
|
||||||
def delete_player(self, player_idx: int) -> None:
|
|
||||||
del self.roster.players[player_idx]
|
|
||||||
self.switch_to_roster_view()
|
|
||||||
|
|
||||||
def switch_to_game_view(self, player_idx: int) -> None:
|
|
||||||
player = self.roster.players[player_idx]
|
|
||||||
self.loop.widget = GameView(self.loop, player, self.args)
|
|
||||||
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"])
|
|
||||||
player_name = self.roster.players[player_idx].name
|
|
||||||
|
|
||||||
self.old_views.append(self.loop.widget)
|
|
||||||
self.loop.widget = ConfirmDialog(
|
|
||||||
f"Terminate {adjective} {player_name}?", self.old_views[-1]
|
|
||||||
)
|
|
||||||
self._connect("confirm", lambda: self.delete_player(player_idx))
|
|
||||||
self._connect("cancel", self.cancel_dialog)
|
|
||||||
|
|
||||||
def exit(self) -> None:
|
|
||||||
raise urwid.ExitMainLoop()
|
|
||||||
|
|
||||||
def cancel_dialog(self) -> None:
|
|
||||||
assert len(self.old_views)
|
|
||||||
self.loop.widget = self.old_views.pop()
|
|
||||||
|
|
||||||
def _connect(self, signal_name: str, callback: T.Callable) -> None:
|
|
||||||
urwid.signals.connect_signal(
|
|
||||||
self.loop.widget,
|
|
||||||
signal_name,
|
|
||||||
lambda _sender, *args: callback(*args),
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_tick(self, _loop: urwid.MainLoop, _user_data: T.Any) -> None:
|
|
||||||
if hasattr(self.loop.widget, "tick"):
|
|
||||||
self.loop.widget.tick()
|
|
||||||
self.loop.set_alarm_in(TICK_FREQ, self.on_tick)
|
|
||||||
|
|
||||||
def on_save(self, _loop: urwid.MainLoop, _user_data: T.Any) -> None:
|
|
||||||
self.roster.save()
|
|
||||||
self.loop.set_alarm_in(SAVE_FREQ, self.on_save)
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import abc
|
||||||
|
import argparse
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUserInterface(abc.ABC):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
roster: Roster,
|
||||||
|
player: T.Optional[Player],
|
||||||
|
args: argparse.Namespace,
|
||||||
|
) -> None:
|
||||||
|
self.roster = roster
|
||||||
|
self.args = args
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def run(self) -> None:
|
||||||
|
raise NotImplementedError("not implemented")
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
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 = """
|
||||||
|
____ ___ _
|
||||||
|
| _ \ _ __ ___ __ _ _ __ ___ ___ ___ / _ \ _ _ ___ ___| |_
|
||||||
|
| |_) | '__/ _ \ / _` | '__/ _ \/ __/ __| | | | | | |/ _ \/ __| __|
|
||||||
|
| __/| | | (_) | (_| | | | __/\__ \__ \ |_| | |_| | __/\__ \ |_
|
||||||
|
|_| |_| \___/ \__, |_| \___||___/___/\__\_\\\\__,_|\___||___/\__|
|
||||||
|
|___/
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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) -> 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_save = datetime.now()
|
||||||
|
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 betwen 1..{len(options)}")
|
||||||
|
continue
|
||||||
|
return options[num - 1][0]
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
from pqcli.ui.custom_button import CustomButton
|
|
||||||
from pqcli.ui.custom_line_box import CustomLineBox
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmDialog(urwid.Overlay):
|
|
||||||
signals = ["confirm", "cancel"]
|
|
||||||
|
|
||||||
def __init__(self, label: str, parent: urwid.Widget) -> None:
|
|
||||||
question = urwid.Text(("bold", label), "center")
|
|
||||||
yes_btn = CustomButton(
|
|
||||||
"Yes", hint="Y", on_press=lambda _user_data: self.confirm()
|
|
||||||
)
|
|
||||||
no_btn = CustomButton(
|
|
||||||
"No", hint="N", on_press=lambda _user_data: self.cancel()
|
|
||||||
)
|
|
||||||
|
|
||||||
line_box = CustomLineBox(
|
|
||||||
urwid.ListBox(
|
|
||||||
urwid.SimpleFocusListWalker([question, no_btn, yes_btn])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
line_box, parent, "center", len(label) + 6, "middle", 5
|
|
||||||
)
|
|
||||||
|
|
||||||
def keypress(self, size: T.Any, key: str) -> T.Optional[str]:
|
|
||||||
if key in {"y", "Y"}:
|
|
||||||
self.confirm()
|
|
||||||
return None
|
|
||||||
if key in {"n", "N"}:
|
|
||||||
self.cancel()
|
|
||||||
return None
|
|
||||||
return T.cast(T.Optional[str], super().keypress(size, key))
|
|
||||||
|
|
||||||
def confirm(self) -> None:
|
|
||||||
self._emit("confirm")
|
|
||||||
|
|
||||||
def cancel(self) -> None:
|
|
||||||
self._emit("cancel")
|
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import argparse
|
||||||
|
import curses
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli import lingo
|
||||||
|
from pqcli.config import Class, Race
|
||||||
|
from pqcli.mechanic import Player, Stats, create_player
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
from pqcli.ui.base import BaseUserInterface
|
||||||
|
from pqcli.ui.curses.colors import (
|
||||||
|
COLOR_FOCUSED,
|
||||||
|
COLOR_HIGHLIGHT,
|
||||||
|
COLOR_LOGO,
|
||||||
|
COLOR_LOGO_ALT,
|
||||||
|
COLOR_PROGRESSBAR,
|
||||||
|
COLOR_SCROLLBAR_THUMB,
|
||||||
|
COLOR_SCROLLBAR_TRACK,
|
||||||
|
has_colors,
|
||||||
|
set_colors,
|
||||||
|
)
|
||||||
|
from pqcli.ui.curses.views import (
|
||||||
|
BaseView,
|
||||||
|
ChooseCharacterClassView,
|
||||||
|
ChooseCharacterNameView,
|
||||||
|
ChooseCharacterRaceView,
|
||||||
|
ChooseCharacterStatsView,
|
||||||
|
ChooseCharacterView,
|
||||||
|
ConfirmView,
|
||||||
|
GameView,
|
||||||
|
RosterView,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StopMainLoop(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CursesUserInterface(BaseUserInterface):
|
||||||
|
def __init__(
|
||||||
|
self, roster: Roster, player: Player, args: argparse.Namespace
|
||||||
|
) -> None:
|
||||||
|
super().__init__(roster, player, args)
|
||||||
|
os.environ.setdefault("ESCDELAY", "25")
|
||||||
|
self._screen = curses.initscr()
|
||||||
|
self._screen.nodelay(True)
|
||||||
|
self._screen.keypad(True)
|
||||||
|
curses.noecho()
|
||||||
|
curses.curs_set(0)
|
||||||
|
|
||||||
|
set_colors(args.colors)
|
||||||
|
if has_colors():
|
||||||
|
curses.start_color()
|
||||||
|
curses.use_default_colors()
|
||||||
|
curses.init_pair(COLOR_LOGO, -1, -1)
|
||||||
|
curses.init_pair(COLOR_LOGO_ALT, -1, 9)
|
||||||
|
curses.init_pair(COLOR_FOCUSED, -1, 8)
|
||||||
|
curses.init_pair(COLOR_SCROLLBAR_THUMB, -1, 7)
|
||||||
|
curses.init_pair(COLOR_SCROLLBAR_TRACK, -1, 0)
|
||||||
|
curses.init_pair(COLOR_PROGRESSBAR, 0, 9)
|
||||||
|
curses.init_pair(COLOR_HIGHLIGHT, 3, -1)
|
||||||
|
|
||||||
|
def signal_handler(sig: T.Any, frame: T.Any) -> None:
|
||||||
|
if self.args.use_saves:
|
||||||
|
self.roster.save()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
self._roster_view = RosterView(self._screen)
|
||||||
|
self._roster_view.on_create += self._switch_to_create_char_name_view
|
||||||
|
self._roster_view.on_delete += self._switch_to_delete_char_view
|
||||||
|
self._roster_view.on_play += self._switch_to_play_view
|
||||||
|
self._roster_view.on_quit += self._quit
|
||||||
|
self._view: BaseView = self._roster_view
|
||||||
|
self._view.start()
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.player:
|
||||||
|
self._switch_to_game_view(self.player)
|
||||||
|
else:
|
||||||
|
self._switch_to_roster_view()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
key = T.cast(int, self._screen.getch())
|
||||||
|
|
||||||
|
if key == curses.ERR:
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._view.idle()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key == curses.KEY_RESIZE:
|
||||||
|
self._view.stop()
|
||||||
|
self._view.start()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self._view.keypress(key)
|
||||||
|
except StopMainLoop:
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
curses.endwin()
|
||||||
|
curses.curs_set(1)
|
||||||
|
|
||||||
|
def _switch_to_roster_view(self) -> None:
|
||||||
|
self._switch_view(self._roster_view)
|
||||||
|
|
||||||
|
def _switch_to_create_char_name_view(
|
||||||
|
self, player_name: T.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
view = ChooseCharacterNameView(self._screen, player_name)
|
||||||
|
view.on_cancel += self._switch_to_roster_view
|
||||||
|
view.on_confirm += self._switch_to_create_char_race_view
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_create_char_race_view(
|
||||||
|
self, player_name: str, race: T.Optional[Race] = None
|
||||||
|
) -> None:
|
||||||
|
view = ChooseCharacterRaceView(self._screen, race)
|
||||||
|
view.on_cancel += lambda: self._switch_to_create_char_name_view(
|
||||||
|
player_name
|
||||||
|
)
|
||||||
|
view.on_confirm += lambda race: self._switch_to_create_char_class_view(
|
||||||
|
player_name, race
|
||||||
|
)
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_create_char_class_view(
|
||||||
|
self, player_name: str, race: Race, class_: T.Optional[Class] = None
|
||||||
|
) -> None:
|
||||||
|
view = ChooseCharacterClassView(self._screen, class_)
|
||||||
|
view.on_cancel += lambda: self._switch_to_create_char_race_view(
|
||||||
|
player_name, race
|
||||||
|
)
|
||||||
|
view.on_confirm += (
|
||||||
|
lambda class_: self._switch_to_create_char_stats_view(
|
||||||
|
player_name, race, class_
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_create_char_stats_view(
|
||||||
|
self, player_name: str, race: Race, class_: Class
|
||||||
|
) -> None:
|
||||||
|
view = ChooseCharacterStatsView(self._screen)
|
||||||
|
view.on_cancel += lambda: self._switch_to_create_char_class_view(
|
||||||
|
player_name, race, class_
|
||||||
|
)
|
||||||
|
view.on_confirm += lambda stats: self._create_character(
|
||||||
|
player_name, race, class_, stats
|
||||||
|
)
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_delete_char_view(self) -> None:
|
||||||
|
view = ChooseCharacterView(
|
||||||
|
self._screen, self.roster, "Choose character to delete"
|
||||||
|
)
|
||||||
|
view.on_cancel += self._switch_to_roster_view
|
||||||
|
view.on_confirm += self._switch_to_confirm_delete_char_view
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_confirm_delete_char_view(self, player: Player) -> None:
|
||||||
|
view = ConfirmView(self._screen, lingo.terminate_message(player.name))
|
||||||
|
view.on_cancel += self._switch_to_delete_char_view
|
||||||
|
view.on_confirm += lambda: self._delete_character(player)
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_play_view(self) -> None:
|
||||||
|
view = ChooseCharacterView(
|
||||||
|
self._screen, self.roster, "Choose character to play"
|
||||||
|
)
|
||||||
|
view.on_cancel += self._switch_to_roster_view
|
||||||
|
view.on_confirm += self._switch_to_game_view
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_to_game_view(self, player: Player) -> None:
|
||||||
|
view = GameView(self._screen, self.roster, player, self.args)
|
||||||
|
view.on_exit += self._switch_to_roster_view
|
||||||
|
self._switch_view(view)
|
||||||
|
|
||||||
|
def _switch_view(self, view: BaseView) -> None:
|
||||||
|
self._view.stop()
|
||||||
|
self._view = view
|
||||||
|
self._view.start()
|
||||||
|
|
||||||
|
def _create_character(
|
||||||
|
self, name: str, race: Race, class_: Class, stats: Stats
|
||||||
|
) -> None:
|
||||||
|
player = create_player(name, race, class_, stats)
|
||||||
|
self.roster.players.append(player)
|
||||||
|
if self.args.use_saves:
|
||||||
|
self.roster.save()
|
||||||
|
self._switch_to_game_view(player)
|
||||||
|
|
||||||
|
def _delete_character(self, player: Player) -> None:
|
||||||
|
self.roster.players.remove(player)
|
||||||
|
self._switch_to_roster_view()
|
||||||
|
|
||||||
|
def _quit(self) -> None:
|
||||||
|
raise StopMainLoop
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
_HAS_COLORS = False
|
||||||
|
|
||||||
|
COLOR_LOGO = 1
|
||||||
|
COLOR_LOGO_ALT = 2
|
||||||
|
COLOR_FOCUSED = 3
|
||||||
|
COLOR_SCROLLBAR_TRACK = 4
|
||||||
|
COLOR_SCROLLBAR_THUMB = 5
|
||||||
|
COLOR_PROGRESSBAR = 6
|
||||||
|
COLOR_HIGHLIGHT = 7
|
||||||
|
|
||||||
|
|
||||||
|
def has_colors() -> bool:
|
||||||
|
return _HAS_COLORS
|
||||||
|
|
||||||
|
|
||||||
|
def set_colors(has_colors: bool) -> None:
|
||||||
|
global _HAS_COLORS
|
||||||
|
_HAS_COLORS = has_colors
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
|
||||||
|
class EventHandler:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.callbacks: T.List[T.Callable[..., T.Any]] = []
|
||||||
|
|
||||||
|
def __call__(self, *args: T.Any, **kwargs: T.Any) -> None:
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
|
||||||
|
def __iadd__(self, callback: T.Callable[..., T.Any]) -> "EventHandler":
|
||||||
|
self.callbacks.append(callback)
|
||||||
|
return self
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import curses
|
||||||
|
import curses.ascii
|
||||||
|
import typing as T
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
KEYS_CYCLE = {curses.ascii.TAB}
|
||||||
|
KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN}
|
||||||
|
KEYS_LEFT = set(map(ord, "hH")) | {curses.KEY_LEFT}
|
||||||
|
KEYS_RIGHT = set(map(ord, "lL")) | {curses.KEY_RIGHT}
|
||||||
|
KEYS_UP = set(map(ord, "kK")) | {curses.KEY_UP}
|
||||||
|
KEYS_CANCEL = set(map(ord, "qQ")) | {curses.ascii.ESC}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Choice:
|
||||||
|
keys: T.List[int]
|
||||||
|
desc: str
|
||||||
|
callback: T.Callable[..., T.Any]
|
||||||
|
|
||||||
|
|
||||||
|
def first(source: T.Iterable[T.Any], default: T.Any = None) -> T.Any:
|
||||||
|
return next(iter(source), default)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .base_view import BaseView
|
||||||
|
from .choose_character_view import ChooseCharacterView
|
||||||
|
from .confirm_view import ConfirmView
|
||||||
|
from .create_character_view import (
|
||||||
|
ChooseCharacterClassView,
|
||||||
|
ChooseCharacterNameView,
|
||||||
|
ChooseCharacterRaceView,
|
||||||
|
ChooseCharacterStatsView,
|
||||||
|
)
|
||||||
|
from .game_view import GameView
|
||||||
|
from .roster_view import RosterView
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
|
||||||
|
class BaseView:
|
||||||
|
def __init__(self, screen: T.Any) -> None:
|
||||||
|
self.screen = screen
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def idle(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import functools
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.lingo import act_name, to_roman
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
|
||||||
|
from pqcli.ui.curses.views.menu_view import MenuView
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseCharacterView(MenuView):
|
||||||
|
def __init__(self, screen: T.Any, roster: Roster, title: str) -> None:
|
||||||
|
super().__init__(screen, title)
|
||||||
|
|
||||||
|
for y, player in enumerate(roster.players, 1):
|
||||||
|
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||||
|
|
||||||
|
best_equip = player.equipment.best
|
||||||
|
|
||||||
|
best_spell = player.spell_book.best
|
||||||
|
best_spell_name = (
|
||||||
|
(best_spell.name + " " + to_roman(best_spell.level))
|
||||||
|
if best_spell
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
best_stat_name = (
|
||||||
|
f"{player.stats.best_prime.value} "
|
||||||
|
f"{player.stats[player.stats.best_prime]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
label = (
|
||||||
|
f"[{key or '-'}] "
|
||||||
|
f"{player.name} the {player.race.name} "
|
||||||
|
f"({act_name(player.quest_book.act)})\n"
|
||||||
|
f" Level {player.level} {player.class_.name}\n"
|
||||||
|
f" {best_equip} / {best_spell_name} / {best_stat_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._choices.append(
|
||||||
|
Choice(
|
||||||
|
keys=[ord(key)] if key is not None else [],
|
||||||
|
desc=label,
|
||||||
|
callback=functools.partial(self.on_confirm, player),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._choices.append(
|
||||||
|
Choice(
|
||||||
|
keys=list(KEYS_CANCEL),
|
||||||
|
desc="[Q] Cancel",
|
||||||
|
callback=self.on_cancel,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
from pqcli.ui.curses.util import Choice
|
||||||
|
from pqcli.ui.curses.widgets import Menu
|
||||||
|
|
||||||
|
from .base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmView(BaseView):
|
||||||
|
def __init__(self, screen: T.Any, title: str) -> None:
|
||||||
|
super().__init__(screen)
|
||||||
|
|
||||||
|
self.title = f"{title}:"
|
||||||
|
self.on_cancel = EventHandler()
|
||||||
|
self.on_confirm = EventHandler()
|
||||||
|
|
||||||
|
self.choices: T.List[Choice] = [
|
||||||
|
Choice(
|
||||||
|
keys=list(map(ord, "yY")),
|
||||||
|
desc=f"[Y] Yes",
|
||||||
|
callback=self.on_confirm,
|
||||||
|
),
|
||||||
|
Choice(
|
||||||
|
keys=list(map(ord, "nNqQ\N{ESC}")),
|
||||||
|
desc="[N] No",
|
||||||
|
callback=self.on_cancel,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
scr_height, scr_width = self.screen.getmaxyx()
|
||||||
|
self.screen.erase()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
self.main_menu = Menu(
|
||||||
|
header=self.title,
|
||||||
|
choices=self.choices,
|
||||||
|
active_choice=0,
|
||||||
|
scr_height=scr_height,
|
||||||
|
scr_width=scr_width,
|
||||||
|
)
|
||||||
|
self.main_menu.render()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.main_menu.stop()
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
self.main_menu.keypress(key)
|
||||||
|
self.main_menu.render()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .choose_character_class_view import ChooseCharacterClassView
|
||||||
|
from .choose_character_name_view import ChooseCharacterNameView
|
||||||
|
from .choose_character_race_view import ChooseCharacterRaceView
|
||||||
|
from .choose_character_stats_view import ChooseCharacterStatsView
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import functools
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.config import CLASSES, Class
|
||||||
|
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
|
||||||
|
from pqcli.ui.curses.views.menu_view import MenuView
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseCharacterClassView(MenuView):
|
||||||
|
def __init__(
|
||||||
|
self, screen: T.Any, class_: T.Optional[Class] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
screen,
|
||||||
|
"Choose character class",
|
||||||
|
CLASSES.index(class_) if class_ is not None else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
for y, class_ in enumerate(CLASSES, 1):
|
||||||
|
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||||
|
self._choices.append(
|
||||||
|
Choice(
|
||||||
|
keys=[ord(key)] if key is not None else [],
|
||||||
|
desc=f"[{key or '-'}] {class_.name}",
|
||||||
|
callback=functools.partial(self.on_confirm, class_),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._choices.append(
|
||||||
|
Choice(
|
||||||
|
keys=list(KEYS_CANCEL),
|
||||||
|
desc="[Q] Cancel",
|
||||||
|
callback=self.on_cancel,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import curses
|
||||||
|
import curses.ascii
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import generate_name
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
from pqcli.ui.curses.util import KEYS_CANCEL, KEYS_CYCLE, KEYS_DOWN, KEYS_UP
|
||||||
|
from pqcli.ui.curses.views.base_view import BaseView
|
||||||
|
from pqcli.ui.curses.widgets.focusable import focus_standout
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseCharacterNameView(BaseView):
|
||||||
|
def __init__(
|
||||||
|
self, screen: T.Any, character_name: T.Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(screen)
|
||||||
|
|
||||||
|
self.on_cancel = EventHandler()
|
||||||
|
self.on_confirm = EventHandler()
|
||||||
|
|
||||||
|
self._active_widget = 0
|
||||||
|
self._win: T.Optional[T.Any] = None
|
||||||
|
self._text_win: T.Optional[T.Any] = None
|
||||||
|
self._text = character_name or generate_name()
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
self.screen.erase()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
scr_height, scr_width = self.screen.getmaxyx()
|
||||||
|
|
||||||
|
h = 8
|
||||||
|
w = 30
|
||||||
|
y = (scr_height - h - 2) // 2
|
||||||
|
x = (scr_width - w - 2) // 2
|
||||||
|
self._text_win = curses.newwin(3, w + 2, y + 2, x - 1)
|
||||||
|
self._win = curses.newwin(h + 2, w + 2, y, x)
|
||||||
|
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
del self._win
|
||||||
|
del self._text_win
|
||||||
|
self._win = None
|
||||||
|
self._text_win = None
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
if key == curses.KEY_F5:
|
||||||
|
self._text = generate_name()
|
||||||
|
|
||||||
|
elif key == curses.KEY_F10:
|
||||||
|
self._finish()
|
||||||
|
|
||||||
|
elif key in KEYS_CYCLE:
|
||||||
|
self._active_widget = (self._active_widget + 1) % 4
|
||||||
|
|
||||||
|
elif self._active_widget == 0:
|
||||||
|
if key == curses.ascii.ESC:
|
||||||
|
self.on_cancel()
|
||||||
|
elif curses.ascii.isprint(key):
|
||||||
|
self._text += chr(key)
|
||||||
|
elif key == curses.KEY_BACKSPACE or key == curses.ascii.DEL:
|
||||||
|
self._text = self._text[:-1]
|
||||||
|
elif key == curses.ascii.NL:
|
||||||
|
self._finish()
|
||||||
|
elif key == curses.ascii.ETB: # ^w
|
||||||
|
self._text = ""
|
||||||
|
elif key == curses.KEY_DOWN:
|
||||||
|
self._active_widget = 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
if key == curses.ascii.NL:
|
||||||
|
if self._active_widget == 1:
|
||||||
|
self._text = generate_name()
|
||||||
|
elif self._active_widget == 2:
|
||||||
|
self._finish()
|
||||||
|
elif self._active_widget == 3:
|
||||||
|
self.on_cancel()
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
elif key in KEYS_CANCEL:
|
||||||
|
self.on_cancel()
|
||||||
|
|
||||||
|
elif key in KEYS_DOWN and self._active_widget < 3:
|
||||||
|
self._active_widget += 1
|
||||||
|
|
||||||
|
elif key in KEYS_UP and self._active_widget > 0:
|
||||||
|
self._active_widget -= 1
|
||||||
|
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _finish(self) -> None:
|
||||||
|
self._text = self._text.strip()
|
||||||
|
if self._text:
|
||||||
|
self.on_confirm(self._text)
|
||||||
|
else:
|
||||||
|
self.on_cancel()
|
||||||
|
|
||||||
|
def _render(self) -> None:
|
||||||
|
if not self._win or not self._text_win:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._win.erase()
|
||||||
|
self._win.addstr(0, 0, "Choose character name:")
|
||||||
|
with focus_standout(self._active_widget == 1, self._win):
|
||||||
|
self._win.addstr(6, 0, "[F5 ] Generate random name")
|
||||||
|
with focus_standout(self._active_widget == 2, self._win):
|
||||||
|
self._win.addstr(7, 0, "[F10 ] Continue")
|
||||||
|
with focus_standout(self._active_widget == 3, self._win):
|
||||||
|
self._win.addstr(8, 0, "[Esc ] Cancel")
|
||||||
|
self._win.refresh()
|
||||||
|
|
||||||
|
self._text_win.erase()
|
||||||
|
with focus_standout(self._active_widget == 0, self._text_win):
|
||||||
|
self._text_win.box()
|
||||||
|
self._text_win.addnstr(
|
||||||
|
1, 1, self._text, self._text_win.getmaxyx()[1] - 2
|
||||||
|
)
|
||||||
|
self._text_win.refresh()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import functools
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.config import RACES, Race
|
||||||
|
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
|
||||||
|
from pqcli.ui.curses.views.menu_view import MenuView
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseCharacterRaceView(MenuView):
|
||||||
|
def __init__(self, screen: T.Any, race: T.Optional[Race] = None) -> None:
|
||||||
|
super().__init__(
|
||||||
|
screen,
|
||||||
|
"Choose character race",
|
||||||
|
RACES.index(race) if race is not None else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
for y, race in enumerate(RACES, 1):
|
||||||
|
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||||
|
self._choices.append(
|
||||||
|
Choice(
|
||||||
|
keys=[ord(key)] if key is not None else [],
|
||||||
|
desc=f"[{key or '-'}] {race.name}",
|
||||||
|
callback=functools.partial(self.on_confirm, race),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._choices.append(
|
||||||
|
Choice(
|
||||||
|
keys=list(KEYS_CANCEL),
|
||||||
|
desc="[Q] Cancel",
|
||||||
|
callback=self.on_cancel,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import curses.ascii
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.config import PRIME_STATS
|
||||||
|
from pqcli.mechanic import StatsBuilder
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
from pqcli.ui.curses.util import KEYS_CANCEL, KEYS_CYCLE, KEYS_DOWN, KEYS_UP
|
||||||
|
from pqcli.ui.curses.views.base_view import BaseView
|
||||||
|
from pqcli.ui.curses.widgets.focusable import focus_standout
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseCharacterStatsView(BaseView):
|
||||||
|
def __init__(self, screen: T.Any) -> None:
|
||||||
|
super().__init__(screen)
|
||||||
|
|
||||||
|
self.on_cancel = EventHandler()
|
||||||
|
self.on_confirm = EventHandler()
|
||||||
|
|
||||||
|
self._active_widget = 0
|
||||||
|
self._win: T.Optional[T.Any] = None
|
||||||
|
self._stats_win: T.Optional[T.Any] = None
|
||||||
|
|
||||||
|
self._stats_builder = StatsBuilder()
|
||||||
|
self._stats = self._stats_builder.roll()
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
scr_height, scr_width = self.screen.getmaxyx()
|
||||||
|
|
||||||
|
h2 = len(PRIME_STATS) + 2
|
||||||
|
h = 7 + h2
|
||||||
|
w = 30
|
||||||
|
y = (scr_height - h - 2) // 2
|
||||||
|
x = (scr_width - w - 2) // 2
|
||||||
|
|
||||||
|
self.screen.erase()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
self._win = curses.newwin(h, w, y, x)
|
||||||
|
self._stats_win = curses.newwin(h2, w, y + 2, x - 1)
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
del self._win
|
||||||
|
del self._stats_win
|
||||||
|
self._win = None
|
||||||
|
self._stats_win = None
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
if key in KEYS_CANCEL:
|
||||||
|
self.on_cancel()
|
||||||
|
|
||||||
|
elif key == curses.ascii.NL:
|
||||||
|
if self._active_widget == 0:
|
||||||
|
self._stats = self._stats_builder.roll()
|
||||||
|
elif self._active_widget == 1:
|
||||||
|
self._stats = self._stats_builder.unroll()
|
||||||
|
elif self._active_widget == 2:
|
||||||
|
self.on_confirm(self._stats)
|
||||||
|
elif self._active_widget == 3:
|
||||||
|
self.on_cancel()
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
elif key == curses.KEY_F5:
|
||||||
|
self._stats = self._stats_builder.roll()
|
||||||
|
|
||||||
|
elif key == curses.KEY_F6:
|
||||||
|
self._stats = self._stats_builder.unroll()
|
||||||
|
|
||||||
|
elif key == curses.KEY_F10:
|
||||||
|
self.on_confirm(self._stats)
|
||||||
|
|
||||||
|
elif key in KEYS_CYCLE:
|
||||||
|
self._active_widget = (self._active_widget + 1) % 4
|
||||||
|
|
||||||
|
elif key in KEYS_DOWN and self._active_widget < 3:
|
||||||
|
self._active_widget += 1
|
||||||
|
|
||||||
|
elif key in KEYS_UP and self._active_widget > 0:
|
||||||
|
self._active_widget -= 1
|
||||||
|
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _render(self) -> None:
|
||||||
|
if not self._win or not self._stats_win:
|
||||||
|
return
|
||||||
|
|
||||||
|
y, x = self._win.getbegyx()
|
||||||
|
stats_h = self._stats_win.getmaxyx()[0]
|
||||||
|
|
||||||
|
self._win.erase()
|
||||||
|
self._win.addstr(0, 0, "Roll character stats:")
|
||||||
|
with focus_standout(self._active_widget == 0, self._win):
|
||||||
|
self._win.addstr(3 + stats_h + 0, 0, "[F5 ] Roll")
|
||||||
|
with focus_standout(self._active_widget == 1, self._win):
|
||||||
|
self._win.addstr(3 + stats_h + 1, 0, "[F6 ] Unroll")
|
||||||
|
with focus_standout(self._active_widget == 2, self._win):
|
||||||
|
self._win.addstr(3 + stats_h + 2, 0, "[F10 ] Continue")
|
||||||
|
with focus_standout(self._active_widget == 3, self._win):
|
||||||
|
self._win.addstr(3 + stats_h + 3, 0, "[Esc ] Cancel")
|
||||||
|
self._win.noutrefresh()
|
||||||
|
|
||||||
|
self._stats_win.erase()
|
||||||
|
for y, stat in enumerate(PRIME_STATS):
|
||||||
|
self._stats_win.addstr(
|
||||||
|
y + 1, 1, f"{stat.value}: {self._stats[stat]}"
|
||||||
|
)
|
||||||
|
self._stats_win.box()
|
||||||
|
self._stats_win.noutrefresh()
|
||||||
|
|
||||||
|
curses.doupdate()
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import argparse
|
||||||
|
import curses
|
||||||
|
import datetime
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import Player, SignalMixin, Simulation
|
||||||
|
from pqcli.roster import Roster
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
from pqcli.ui.curses.util import (
|
||||||
|
KEYS_CANCEL,
|
||||||
|
KEYS_CYCLE,
|
||||||
|
KEYS_DOWN,
|
||||||
|
KEYS_LEFT,
|
||||||
|
KEYS_RIGHT,
|
||||||
|
KEYS_UP,
|
||||||
|
)
|
||||||
|
from pqcli.ui.curses.views.base_view import BaseView
|
||||||
|
from pqcli.ui.curses.widgets import Focusable, Scrollable, Widget
|
||||||
|
|
||||||
|
from .character_sheet_window import CharacterSheetWindow
|
||||||
|
from .equipment_window import EquipmentWindow
|
||||||
|
from .inventory_window import InventoryWindow
|
||||||
|
from .plot_window import PlotWindow
|
||||||
|
from .quest_book_window import QuestBookWindow
|
||||||
|
from .spell_book_window import SpellBookWindow
|
||||||
|
from .task_progress_window import TaskProgressWindow
|
||||||
|
|
||||||
|
|
||||||
|
class GameView(BaseView):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
screen: T.Any,
|
||||||
|
roster: Roster,
|
||||||
|
player: Player,
|
||||||
|
args: argparse.Namespace,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(screen)
|
||||||
|
self.on_exit = EventHandler()
|
||||||
|
|
||||||
|
self._roster = roster
|
||||||
|
self._player = player
|
||||||
|
self._args = args
|
||||||
|
|
||||||
|
self._simulation = Simulation(player)
|
||||||
|
self._last_tick = datetime.datetime.now()
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
focused = self.focused
|
||||||
|
|
||||||
|
if key in KEYS_CANCEL:
|
||||||
|
self.on_exit()
|
||||||
|
|
||||||
|
elif key in {curses.KEY_PPAGE}:
|
||||||
|
if hasattr(focused, "scroll_page_up"):
|
||||||
|
focused.scroll_page_up()
|
||||||
|
|
||||||
|
elif key in {curses.KEY_NPAGE}:
|
||||||
|
if hasattr(focused, "scroll_page_down"):
|
||||||
|
focused.scroll_page_down()
|
||||||
|
|
||||||
|
elif key in KEYS_CYCLE:
|
||||||
|
if focused == self._char_sheet_win:
|
||||||
|
self.focus(self._spell_book_win)
|
||||||
|
elif focused == self._spell_book_win:
|
||||||
|
self.focus(self._equipment_win)
|
||||||
|
elif focused == self._equipment_win:
|
||||||
|
self.focus(self._inventory_win)
|
||||||
|
elif focused == self._inventory_win:
|
||||||
|
self.focus(self._plot_win)
|
||||||
|
elif focused == self._plot_win:
|
||||||
|
self.focus(self._quest_book_win)
|
||||||
|
elif focused == self._quest_book_win:
|
||||||
|
self.focus(self._char_sheet_win)
|
||||||
|
|
||||||
|
elif key in KEYS_DOWN:
|
||||||
|
if focused == self._char_sheet_win:
|
||||||
|
self.focus(self._spell_book_win)
|
||||||
|
elif focused == self._equipment_win:
|
||||||
|
self.focus(self._inventory_win)
|
||||||
|
elif focused == self._plot_win:
|
||||||
|
self.focus(self._quest_book_win)
|
||||||
|
|
||||||
|
elif key in KEYS_UP:
|
||||||
|
if focused == self._spell_book_win:
|
||||||
|
self.focus(self._char_sheet_win)
|
||||||
|
elif focused == self._inventory_win:
|
||||||
|
self.focus(self._equipment_win)
|
||||||
|
elif focused == self._quest_book_win:
|
||||||
|
self.focus(self._plot_win)
|
||||||
|
|
||||||
|
elif key in KEYS_LEFT:
|
||||||
|
if focused == self._equipment_win:
|
||||||
|
self.focus(self._char_sheet_win)
|
||||||
|
elif focused == self._inventory_win:
|
||||||
|
self.focus(self._spell_book_win)
|
||||||
|
elif focused == self._plot_win:
|
||||||
|
self.focus(self._equipment_win)
|
||||||
|
elif focused == self._quest_book_win:
|
||||||
|
self.focus(self._inventory_win)
|
||||||
|
|
||||||
|
elif key in KEYS_RIGHT:
|
||||||
|
if focused == self._char_sheet_win:
|
||||||
|
self.focus(self._equipment_win)
|
||||||
|
elif focused == self._spell_book_win:
|
||||||
|
self.focus(self._inventory_win)
|
||||||
|
elif focused == self._equipment_win:
|
||||||
|
self.focus(self._plot_win)
|
||||||
|
elif focused == self._inventory_win:
|
||||||
|
self.focus(self._quest_book_win)
|
||||||
|
|
||||||
|
elif self._args.cheats and key == ord("t"):
|
||||||
|
self._simulation.tick()
|
||||||
|
|
||||||
|
elif self._args.cheats and key == ord("T"):
|
||||||
|
for _ in range(100):
|
||||||
|
self._simulation.tick()
|
||||||
|
|
||||||
|
elif self._args.cheats and key == curses.ascii.DC4: # ^t
|
||||||
|
old = SignalMixin.emit
|
||||||
|
SignalMixin.emit = lambda *_: None
|
||||||
|
for _ in range(10000):
|
||||||
|
self._simulation.tick()
|
||||||
|
SignalMixin.emit = old
|
||||||
|
for child in self._children:
|
||||||
|
child.sync()
|
||||||
|
|
||||||
|
else:
|
||||||
|
super().keypress(key)
|
||||||
|
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
scr_height, scr_width = self.screen.getmaxyx()
|
||||||
|
col1_width = int(scr_width * 0.3)
|
||||||
|
col3_width = col1_width
|
||||||
|
col2_width = scr_width - col1_width - col3_width
|
||||||
|
|
||||||
|
self.screen.erase()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
self._big_win = curses.newwin(scr_height, scr_width)
|
||||||
|
self._col1_win = self._big_win.derwin(scr_height - 2, col1_width, 0, 0)
|
||||||
|
self._col2_win = self._big_win.derwin(
|
||||||
|
self._col1_win.getmaxyx()[0],
|
||||||
|
col2_width,
|
||||||
|
0,
|
||||||
|
self._col1_win.getmaxyx()[1],
|
||||||
|
)
|
||||||
|
self._col3_win = self._big_win.derwin(
|
||||||
|
self._col1_win.getmaxyx()[0],
|
||||||
|
col3_width,
|
||||||
|
0,
|
||||||
|
self._col1_win.getmaxyx()[1] + self._col2_win.getmaxyx()[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
self._char_sheet_win = CharacterSheetWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._col1_win,
|
||||||
|
h=min(17, self._col1_win.getmaxyx()[0]),
|
||||||
|
w=self._col1_win.getmaxyx()[1],
|
||||||
|
y=0,
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._spell_book_win = SpellBookWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._col1_win,
|
||||||
|
h=self._col1_win.getmaxyx()[0]
|
||||||
|
- self._char_sheet_win.getmaxyx()[0],
|
||||||
|
w=self._col1_win.getmaxyx()[1],
|
||||||
|
y=self._char_sheet_win.getmaxyx()[0],
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._equipment_win = EquipmentWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._col2_win,
|
||||||
|
h=min(15, self._col2_win.getmaxyx()[0]),
|
||||||
|
w=self._col2_win.getmaxyx()[1],
|
||||||
|
y=0,
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._inventory_win = InventoryWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._col2_win,
|
||||||
|
h=max(
|
||||||
|
1,
|
||||||
|
self._col2_win.getmaxyx()[0]
|
||||||
|
- self._equipment_win.getmaxyx()[0],
|
||||||
|
),
|
||||||
|
w=self._col2_win.getmaxyx()[1],
|
||||||
|
y=self._equipment_win.getmaxyx()[0],
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._plot_win = PlotWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._col3_win,
|
||||||
|
h=self._equipment_win.getmaxyx()[0],
|
||||||
|
w=self._col3_win.getmaxyx()[1],
|
||||||
|
y=0,
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._quest_book_win = QuestBookWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._col3_win,
|
||||||
|
h=max(
|
||||||
|
1, self._col3_win.getmaxyx()[0] - self._plot_win.getmaxyx()[0]
|
||||||
|
),
|
||||||
|
w=self._col3_win.getmaxyx()[1],
|
||||||
|
y=self._plot_win.getmaxyx()[0],
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._task_win = TaskProgressWindow(
|
||||||
|
player=self._player,
|
||||||
|
parent=self._big_win,
|
||||||
|
h=2,
|
||||||
|
w=scr_width,
|
||||||
|
y=scr_height - 2,
|
||||||
|
x=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._focusable_children: T.List[Focusable] = [
|
||||||
|
self._char_sheet_win,
|
||||||
|
self._spell_book_win,
|
||||||
|
self._equipment_win,
|
||||||
|
self._inventory_win,
|
||||||
|
self._plot_win,
|
||||||
|
self._quest_book_win,
|
||||||
|
]
|
||||||
|
self._children: T.List[Widget] = self._focusable_children + [
|
||||||
|
self._task_win
|
||||||
|
]
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
for child in self._children:
|
||||||
|
child.stop()
|
||||||
|
del self._col1_win
|
||||||
|
del self._col2_win
|
||||||
|
del self._col3_win
|
||||||
|
del self._big_win
|
||||||
|
|
||||||
|
def idle(self) -> None:
|
||||||
|
elapsed = datetime.datetime.now() - self._last_tick
|
||||||
|
self._simulation.tick(elapsed.total_seconds() * 1000)
|
||||||
|
self._last_tick = datetime.datetime.now()
|
||||||
|
if self._args.use_saves:
|
||||||
|
self._roster.save_periodically()
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def focused(self) -> Focusable:
|
||||||
|
for widget in self._focusable_children:
|
||||||
|
if widget.focused:
|
||||||
|
return widget
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
def focus(self, widget: Focusable) -> None:
|
||||||
|
self.focused.focused = False
|
||||||
|
widget.focused = True
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import Player, StatType
|
||||||
|
from pqcli.ui.curses.widgets import Focusable
|
||||||
|
|
||||||
|
from .progress_bar_window import DataTableProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterSheetWindow(Focusable, DataTableProgressBarWindow):
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
parent,
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
y,
|
||||||
|
x,
|
||||||
|
" Character Sheet ",
|
||||||
|
align_right=False,
|
||||||
|
show_time=True,
|
||||||
|
)
|
||||||
|
self._on_focus_change += self._render
|
||||||
|
|
||||||
|
self._focused = True
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.connect("level_up", self._sync_traits)
|
||||||
|
self._player.stats.connect("change", self._sync_traits)
|
||||||
|
self._player.exp_bar.connect("change", self._sync_exp)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
self._player.disconnect("level_up", self._sync_traits)
|
||||||
|
self._player.stats.disconnect("change", self._sync_traits)
|
||||||
|
self._player.exp_bar.disconnect("change", self._sync_exp)
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._sync_traits()
|
||||||
|
self._sync_exp()
|
||||||
|
|
||||||
|
def _sync_traits(self) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._data_table.clear()
|
||||||
|
self._data_table.add("Name", self._player.name)
|
||||||
|
self._data_table.add("Race", self._player.race.name)
|
||||||
|
self._data_table.add("Class", self._player.class_.name)
|
||||||
|
self._data_table.add("Level", str(self._player.level))
|
||||||
|
self._data_table.add(" " * 15, "")
|
||||||
|
for stat in StatType:
|
||||||
|
self._data_table.add(stat.value, str(self._player.stats[stat]))
|
||||||
|
|
||||||
|
self._render_data_table()
|
||||||
|
|
||||||
|
def _sync_exp(self) -> None:
|
||||||
|
self._cur_pos = self._player.exp_bar.position
|
||||||
|
self._max_pos = self._player.exp_bar.max_
|
||||||
|
self._progress_title = (
|
||||||
|
f"Experience ({self._max_pos-self._cur_pos:.0f} XP to go)"
|
||||||
|
)
|
||||||
|
self._render_progress_bar()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import EquipmentType, Player
|
||||||
|
from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentWindow(Focusable, WindowWrapper):
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
self._on_focus_change += self._render
|
||||||
|
|
||||||
|
self._data_table = DataTable(
|
||||||
|
self._win, h - 2, w - 2, 1, 1, align_right=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.equipment.connect("change", self._sync_equipment_change)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._data_table.stop()
|
||||||
|
|
||||||
|
self._player.equipment.disconnect(
|
||||||
|
"change", self._sync_equipment_change
|
||||||
|
)
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._data_table.clear()
|
||||||
|
for equipment_type in EquipmentType:
|
||||||
|
self._data_table.add(
|
||||||
|
equipment_type.value.ljust(15),
|
||||||
|
self._player.equipment[equipment_type] or "",
|
||||||
|
)
|
||||||
|
self._data_table.select(None)
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _sync_equipment_change(
|
||||||
|
self, equipment_type: EquipmentType, item_name: T.Optional[str]
|
||||||
|
) -> None:
|
||||||
|
self._data_table.set(equipment_type.value, item_name or "")
|
||||||
|
self._data_table.select(equipment_type.value)
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _render(self) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.focus_standout(self._win):
|
||||||
|
self._win.box()
|
||||||
|
text = " Equipment "
|
||||||
|
x = max(0, (self.getmaxyx()[1] - len(text)) // 2)
|
||||||
|
self._win.addnstr(0, x, text, min(len(text), self.getmaxyx()[1]))
|
||||||
|
|
||||||
|
self._win.noutrefresh()
|
||||||
|
self._data_table.render()
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import InventoryItem, Player
|
||||||
|
from pqcli.ui.curses.widgets import Focusable
|
||||||
|
|
||||||
|
from .progress_bar_window import DataTableProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryWindow(Focusable, DataTableProgressBarWindow):
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
parent,
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
y,
|
||||||
|
x,
|
||||||
|
" Inventory ",
|
||||||
|
align_right=True,
|
||||||
|
show_time=False,
|
||||||
|
)
|
||||||
|
self._on_focus_change += self._render
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.connect("new_task", self._on_new_task)
|
||||||
|
self._player.inventory.connect("gold_change", self._sync_gold)
|
||||||
|
self._player.inventory.connect("item_add", self._sync_item_add)
|
||||||
|
self._player.inventory.connect("item_del", self._sync_item_del)
|
||||||
|
self._player.inventory.connect("item_change", self._sync_item_change)
|
||||||
|
self._player.inventory.encum_bar.connect(
|
||||||
|
"change", self._sync_encumbrance
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
self._player.disconnect("new_task", self._on_new_task)
|
||||||
|
self._player.inventory.disconnect("gold_change", self._sync_gold)
|
||||||
|
self._player.inventory.disconnect("item_add", self._sync_item_add)
|
||||||
|
self._player.inventory.disconnect("item_del", self._sync_item_del)
|
||||||
|
self._player.inventory.disconnect(
|
||||||
|
"item_change", self._sync_item_change
|
||||||
|
)
|
||||||
|
self._player.inventory.encum_bar.disconnect(
|
||||||
|
"change", self._sync_encumbrance
|
||||||
|
)
|
||||||
|
|
||||||
|
def scroll_page_up(self) -> None:
|
||||||
|
self._data_table.scroll_page_up()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def scroll_page_down(self) -> None:
|
||||||
|
self._data_table.scroll_page_down()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._sync_gold()
|
||||||
|
self._sync_items()
|
||||||
|
self._sync_encumbrance()
|
||||||
|
|
||||||
|
def _sync_encumbrance(self) -> None:
|
||||||
|
self._cur_pos = self._player.inventory.encum_bar.position
|
||||||
|
self._max_pos = self._player.inventory.encum_bar.max_
|
||||||
|
self._progress_title = (
|
||||||
|
f"Encumbrance ({self._cur_pos:.0f}/{self._max_pos} cubits)"
|
||||||
|
)
|
||||||
|
self._render_progress_bar()
|
||||||
|
|
||||||
|
def _sync_gold(self) -> None:
|
||||||
|
self._data_table.set("Gold", str(self._player.inventory.gold))
|
||||||
|
self._data_table.select("Gold")
|
||||||
|
self._render_data_table()
|
||||||
|
|
||||||
|
def _sync_items(self) -> None:
|
||||||
|
self._data_table.clear()
|
||||||
|
self._sync_gold()
|
||||||
|
for item in self._player.inventory:
|
||||||
|
self._data_table.add(item.name, str(item.quantity))
|
||||||
|
self._data_table.scroll_to_item(0)
|
||||||
|
self._data_table.select(None)
|
||||||
|
self._render_data_table()
|
||||||
|
|
||||||
|
def _sync_item_add(self, item: InventoryItem) -> None:
|
||||||
|
self._data_table.add(item.name, str(item.quantity))
|
||||||
|
self._data_table.select(item.name)
|
||||||
|
self._render_data_table()
|
||||||
|
|
||||||
|
def _sync_item_del(self, item: InventoryItem) -> None:
|
||||||
|
self._data_table.delete(item.name)
|
||||||
|
self._data_table.select(None)
|
||||||
|
self._render_data_table()
|
||||||
|
|
||||||
|
def _sync_item_change(self, item: InventoryItem) -> None:
|
||||||
|
self._data_table.set(item.name, str(item.quantity))
|
||||||
|
self._data_table.select(item.name)
|
||||||
|
self._render_data_table()
|
||||||
|
|
||||||
|
def _on_new_task(self) -> None:
|
||||||
|
description = (
|
||||||
|
self._player.task.description if self._player.task else "?"
|
||||||
|
)
|
||||||
|
if description.lower().startswith("sell"):
|
||||||
|
self._data_table.scroll_to_item(0)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.lingo import act_name
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.ui.curses.widgets import Focusable
|
||||||
|
|
||||||
|
from .progress_bar_window import ListBoxProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
|
class PlotWindow(Focusable, ListBoxProgressBarWindow):
|
||||||
|
cutoff = 100
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
parent, h, w, y, x, " Plot Development ", show_time=True
|
||||||
|
)
|
||||||
|
self._on_focus_change += self._render
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.quest_book.connect("start_act", self._sync_act_add)
|
||||||
|
self._player.quest_book.plot_bar.connect("change", self._sync_position)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._list_box.stop()
|
||||||
|
|
||||||
|
self._player.quest_book.disconnect("start_act", self._sync_act_add)
|
||||||
|
self._player.quest_book.plot_bar.disconnect(
|
||||||
|
"change", self._sync_position
|
||||||
|
)
|
||||||
|
|
||||||
|
def scroll_page_up(self) -> None:
|
||||||
|
self._list_box.scroll_page_up()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def scroll_page_down(self) -> None:
|
||||||
|
self._list_box.scroll_page_down()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._sync_acts()
|
||||||
|
self._sync_position()
|
||||||
|
|
||||||
|
def _sync_acts(self) -> None:
|
||||||
|
self._list_box.clear()
|
||||||
|
for act_number in range(
|
||||||
|
max(0, self._player.quest_book.act - self.cutoff),
|
||||||
|
self._player.quest_book.act + 1,
|
||||||
|
):
|
||||||
|
self._sync_act_add(act_number)
|
||||||
|
self._render_list_box()
|
||||||
|
|
||||||
|
def _sync_act_add(self, act_number: int) -> None:
|
||||||
|
self._list_box.delete(0, max(0, len(self._list_box) - self.cutoff))
|
||||||
|
prev = self._list_box.get(-1)
|
||||||
|
if prev is not None:
|
||||||
|
self._list_box.set(-1, "[X] " + prev[4:])
|
||||||
|
self._list_box.add("[ ] " + act_name(act_number))
|
||||||
|
self._list_box.select(-1)
|
||||||
|
self._render_list_box()
|
||||||
|
|
||||||
|
def _sync_position(self) -> None:
|
||||||
|
self._cur_pos = self._player.quest_book.plot_bar.position
|
||||||
|
self._max_pos = self._player.quest_book.plot_bar.max_
|
||||||
|
self._render_progress_bar()
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.widgets import (
|
||||||
|
DataTable,
|
||||||
|
Focusable,
|
||||||
|
ListBox,
|
||||||
|
ProgressBar,
|
||||||
|
WindowWrapper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressBarWindow(WindowWrapper):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: T.Any,
|
||||||
|
h: int,
|
||||||
|
w: int,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
title: str,
|
||||||
|
show_time: bool,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
|
||||||
|
self._title = title
|
||||||
|
self._cur_pos = 0.0
|
||||||
|
self._max_pos = 1.0
|
||||||
|
self._progress_title = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._progress_bar_win = (
|
||||||
|
self._win.derwin(3, w, h - 3, 0) if self._win else None
|
||||||
|
)
|
||||||
|
except curses.error:
|
||||||
|
self._progress_bar_win = None
|
||||||
|
self._progress_bar = ProgressBar(
|
||||||
|
self._progress_bar_win, 1, w - 2, 1, 1, show_time=show_time
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._progress_bar.stop()
|
||||||
|
|
||||||
|
del self._progress_bar_win
|
||||||
|
self._progress_bar_win = None
|
||||||
|
|
||||||
|
def _render_progress_bar(self) -> None:
|
||||||
|
if not self._progress_bar_win:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._progress_bar_win.erase()
|
||||||
|
|
||||||
|
with Focusable.focus_standout(self, self._progress_bar_win):
|
||||||
|
self._progress_bar_win.border(
|
||||||
|
curses.ACS_VLINE,
|
||||||
|
curses.ACS_VLINE,
|
||||||
|
curses.ACS_HLINE,
|
||||||
|
curses.ACS_HLINE,
|
||||||
|
curses.ACS_LTEE,
|
||||||
|
curses.ACS_RTEE,
|
||||||
|
curses.ACS_LLCORNER,
|
||||||
|
curses.ACS_LRCORNER,
|
||||||
|
)
|
||||||
|
|
||||||
|
text = self._progress_title
|
||||||
|
x = max(0, (self.getmaxyx()[1] - len(text)) // 2)
|
||||||
|
self._progress_bar_win.addnstr(
|
||||||
|
0,
|
||||||
|
x,
|
||||||
|
text,
|
||||||
|
min(len(text), self._progress_bar_win.getmaxyx()[1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._progress_bar.set_position(self._cur_pos, self._max_pos)
|
||||||
|
self._progress_bar_win.noutrefresh()
|
||||||
|
|
||||||
|
|
||||||
|
class DataTableProgressBarWindow(ProgressBarWindow):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: T.Any,
|
||||||
|
h: int,
|
||||||
|
w: int,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
title: str,
|
||||||
|
align_right: bool,
|
||||||
|
show_time: bool,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x, title, show_time)
|
||||||
|
|
||||||
|
self._data_table = DataTable(
|
||||||
|
self._win, h - 4, w - 2, 1, 1, align_right
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._data_table.stop()
|
||||||
|
|
||||||
|
def _render_data_table(self) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
|
||||||
|
with Focusable.focus_standout(self, self._win):
|
||||||
|
self._win.box()
|
||||||
|
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
|
||||||
|
self._win.addnstr(
|
||||||
|
0, x, self._title, min(len(self._title), self.getmaxyx()[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
self._win.noutrefresh()
|
||||||
|
self._data_table.render()
|
||||||
|
|
||||||
|
def _render(self) -> None:
|
||||||
|
self._render_data_table()
|
||||||
|
self._render_progress_bar()
|
||||||
|
|
||||||
|
|
||||||
|
class ListBoxProgressBarWindow(ProgressBarWindow):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: T.Any,
|
||||||
|
h: int,
|
||||||
|
w: int,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
title: str,
|
||||||
|
show_time: bool,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x, title, show_time)
|
||||||
|
|
||||||
|
self._list_box = ListBox(self._win, h - 4, w - 2, 1, 1)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._list_box.stop()
|
||||||
|
|
||||||
|
def _render_list_box(self) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
|
||||||
|
with Focusable.focus_standout(self, self._win):
|
||||||
|
self._win.box()
|
||||||
|
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
|
||||||
|
self._win.addnstr(
|
||||||
|
0, x, self._title, min(len(self._title), self.getmaxyx()[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
self._win.noutrefresh()
|
||||||
|
self._list_box.render()
|
||||||
|
|
||||||
|
def _render(self) -> None:
|
||||||
|
self._render_list_box()
|
||||||
|
self._render_progress_bar()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.ui.curses.widgets import Focusable
|
||||||
|
|
||||||
|
from .progress_bar_window import ListBoxProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
|
class QuestBookWindow(Focusable, ListBoxProgressBarWindow):
|
||||||
|
cutoff = 100
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x, " Quests ", show_time=True)
|
||||||
|
self._on_focus_change += self._render
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.quest_book.connect("start_quest", self._sync_quest_add)
|
||||||
|
self._player.quest_book.quest_bar.connect(
|
||||||
|
"change", self._sync_position
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._list_box.stop()
|
||||||
|
|
||||||
|
self._player.quest_book.disconnect("start_quest", self._sync_quest_add)
|
||||||
|
self._player.quest_book.quest_bar.disconnect(
|
||||||
|
"change", self._sync_position
|
||||||
|
)
|
||||||
|
|
||||||
|
def scroll_page_up(self) -> None:
|
||||||
|
self._list_box.scroll_page_up()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def scroll_page_down(self) -> None:
|
||||||
|
self._list_box.scroll_page_down()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._sync_quests()
|
||||||
|
self._sync_position()
|
||||||
|
|
||||||
|
def _sync_quests(self) -> None:
|
||||||
|
self._list_box.clear()
|
||||||
|
for quest_name in self._player.quest_book.quests[-self.cutoff :]:
|
||||||
|
self._sync_quest_add(quest_name)
|
||||||
|
self._list_box.select(-1)
|
||||||
|
self._render_list_box()
|
||||||
|
|
||||||
|
def _sync_quest_add(self, quest_name: str) -> None:
|
||||||
|
self._list_box.delete(0, max(0, len(self._list_box) - self.cutoff))
|
||||||
|
prev = self._list_box.get(-1)
|
||||||
|
if prev is not None:
|
||||||
|
self._list_box.set(-1, "[X] " + prev[4:])
|
||||||
|
self._list_box.add("[ ] " + quest_name)
|
||||||
|
self._list_box.select(-1)
|
||||||
|
self._render_list_box()
|
||||||
|
|
||||||
|
def _sync_position(self) -> None:
|
||||||
|
self._cur_pos = self._player.quest_book.quest_bar.position
|
||||||
|
self._max_pos = self._player.quest_book.quest_bar.max_
|
||||||
|
self._render_progress_bar()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.lingo import to_roman
|
||||||
|
from pqcli.mechanic import Player, Spell
|
||||||
|
from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class SpellBookWindow(Focusable, WindowWrapper):
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
self._on_focus_change += self._render
|
||||||
|
|
||||||
|
self._data_table = DataTable(
|
||||||
|
self._win, h - 2, w - 2, 1, 1, align_right=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.spell_book.connect("add", self._sync_spell_add)
|
||||||
|
self._player.spell_book.connect("change", self._sync_spell_change)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
self._data_table.stop()
|
||||||
|
|
||||||
|
self._player.spell_book.disconnect("add", self._sync_spell_add)
|
||||||
|
self._player.spell_book.disconnect("change", self._sync_spell_change)
|
||||||
|
|
||||||
|
def scroll_page_up(self) -> None:
|
||||||
|
self._data_table.scroll_page_up()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def scroll_page_down(self) -> None:
|
||||||
|
self._data_table.scroll_page_down()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._data_table.clear()
|
||||||
|
for spell in self._player.spell_book:
|
||||||
|
self._data_table.add(spell.name, to_roman(spell.level))
|
||||||
|
self._data_table.select(None)
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _sync_spell_add(self, spell: Spell) -> None:
|
||||||
|
self._data_table.add(spell.name, to_roman(spell.level))
|
||||||
|
self._data_table.select(spell.name)
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _sync_spell_change(self, spell: Spell) -> None:
|
||||||
|
self._data_table.set(spell.name, to_roman(spell.level))
|
||||||
|
self._data_table.select(spell.name)
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _render(self) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.focus_standout(self._win):
|
||||||
|
self._win.box()
|
||||||
|
text = " Spell Book "
|
||||||
|
x = max(0, (self.getmaxyx()[1] - len(text)) // 2)
|
||||||
|
self._win.addnstr(0, x, text, min(len(text), self.getmaxyx()[1]))
|
||||||
|
|
||||||
|
self._win.noutrefresh()
|
||||||
|
self._data_table.render()
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.mechanic import Player
|
||||||
|
from pqcli.ui.curses.widgets import (
|
||||||
|
Focusable,
|
||||||
|
Label,
|
||||||
|
ProgressBar,
|
||||||
|
WindowWrapper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskProgressWindow(Focusable, WindowWrapper):
|
||||||
|
def __init__(
|
||||||
|
self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
|
||||||
|
self._desc_win = Label(self._win, 1, w, 0, 0)
|
||||||
|
self._progress_bar = ProgressBar(
|
||||||
|
self._win, 1, w, 1, 0, show_time=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self._player = player
|
||||||
|
self._player.connect("new_task", self._sync_task_name)
|
||||||
|
self._player.task_bar.connect("change", self._sync_position)
|
||||||
|
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
self._desc_win.stop()
|
||||||
|
self._progress_bar.stop()
|
||||||
|
|
||||||
|
self._player.disconnect("new_task", self._sync_task_name)
|
||||||
|
self._player.task_bar.disconnect("change", self._sync_position)
|
||||||
|
|
||||||
|
def sync(self) -> None:
|
||||||
|
self._sync_task_name()
|
||||||
|
self._sync_position()
|
||||||
|
|
||||||
|
def _sync_task_name(self) -> None:
|
||||||
|
self._desc_win.set_text(
|
||||||
|
f"{self._player.task.description}..." if self._player.task else "?"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sync_position(self) -> None:
|
||||||
|
self._progress_bar.set_position(
|
||||||
|
self._player.task_bar.position, self._player.task_bar.max_
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
from pqcli.ui.curses.util import Choice
|
||||||
|
from pqcli.ui.curses.views.base_view import BaseView
|
||||||
|
from pqcli.ui.curses.widgets import Menu
|
||||||
|
|
||||||
|
|
||||||
|
class MenuView(BaseView):
|
||||||
|
def __init__(
|
||||||
|
self, screen: T.Any, title: str, active_choice: int = 0
|
||||||
|
) -> None:
|
||||||
|
super().__init__(screen)
|
||||||
|
|
||||||
|
self.on_cancel = EventHandler()
|
||||||
|
self.on_confirm = EventHandler()
|
||||||
|
|
||||||
|
self._title = f"{title}:"
|
||||||
|
self._choices: T.List[Choice] = []
|
||||||
|
self._active_choice = active_choice
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
scr_height, scr_width = self.screen.getmaxyx()
|
||||||
|
self.screen.erase()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
self.main_menu = Menu(
|
||||||
|
header=self._title,
|
||||||
|
choices=self._choices,
|
||||||
|
active_choice=self._active_choice,
|
||||||
|
scr_height=scr_height,
|
||||||
|
scr_width=scr_width,
|
||||||
|
)
|
||||||
|
self.main_menu.render()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.main_menu.stop()
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
self.main_menu.keypress(key)
|
||||||
|
self.main_menu.render()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.colors import COLOR_LOGO, COLOR_LOGO_ALT, has_colors
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
|
||||||
|
from pqcli.ui.curses.widgets import Menu
|
||||||
|
|
||||||
|
from .base_view import BaseView
|
||||||
|
|
||||||
|
LOGO = """
|
||||||
|
█▀▀▄ ▄▀▀█
|
||||||
|
▀▄ ▀▄ ▄▀ ▄▀
|
||||||
|
▀▄ ▀▄ ▄▀ ▄▀
|
||||||
|
▀▄ ▄▀ ▄▀
|
||||||
|
▄▀ ▄▀▄
|
||||||
|
▄▄▄▄▀ ▄▀▄ ▀▄▄▄▄
|
||||||
|
▀▄ █▄▄▀ ▀▄▄█ ▄▀
|
||||||
|
▄▀█▄ █ █ ▄█▀▄
|
||||||
|
▄▄▀▄▀ ▀▀ ▀▀ ▀▄▀▄▄
|
||||||
|
█▄█ █▄█
|
||||||
|
|
||||||
|
█▀▀▄ ▄▀▀▄ █
|
||||||
|
█▄▄▀ █▀▀ ▄▀▀▄ ▄▀▀▄ █▀▀ ▄▀▀▄ ▄▀▀▀ ▄▀▀▀ █ █ █ █ ▄▀▀▄ ▄▀▀▀ ▀█▀
|
||||||
|
█ █ █ █ ▀▄▄█ █ █▀▀ ▀▀▄ ▀▀▄ █ ▌█ █ █ █▀▀ ▀▀▄ █
|
||||||
|
▀ ▀ ▀▀ ▄▄▀ ▀ ▀▀ ▀▀▀ ▀▀▀ ▀▀▌ ▀▀ ▀▀ ▀▀▀ ▀
|
||||||
|
""".strip(
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MainMenu(Menu):
|
||||||
|
def __init__(
|
||||||
|
self, choices: T.List[Choice], scr_height: int, scr_width: int
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
header=LOGO,
|
||||||
|
choices=choices,
|
||||||
|
active_choice=0,
|
||||||
|
scr_height=scr_height,
|
||||||
|
scr_width=scr_width,
|
||||||
|
)
|
||||||
|
if has_colors():
|
||||||
|
for y in range(len(self._header_lines)):
|
||||||
|
self._pad.chgat(y, 0, curses.color_pair(COLOR_LOGO))
|
||||||
|
self._pad.chgat(3, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
self._pad.chgat(4, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
self._pad.chgat(5, 32, 4, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
self._pad.chgat(5, 27, 4, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
self._pad.chgat(6, 34, 2, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
self._pad.chgat(6, 27, 2, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
|
||||||
|
|
||||||
|
class RosterView(BaseView):
|
||||||
|
def __init__(self, screen: T.Any) -> None:
|
||||||
|
super().__init__(screen)
|
||||||
|
|
||||||
|
self.on_create = EventHandler()
|
||||||
|
self.on_play = EventHandler()
|
||||||
|
self.on_delete = EventHandler()
|
||||||
|
self.on_quit = EventHandler()
|
||||||
|
|
||||||
|
self.choices: T.List[Choice] = [
|
||||||
|
Choice(
|
||||||
|
keys=list(map(ord, "cC")),
|
||||||
|
desc="[C] Create new character",
|
||||||
|
callback=self.on_create,
|
||||||
|
),
|
||||||
|
Choice(
|
||||||
|
keys=list(map(ord, "pP")),
|
||||||
|
desc="[P] Play",
|
||||||
|
callback=self.on_play,
|
||||||
|
),
|
||||||
|
Choice(
|
||||||
|
keys=list(map(ord, "dD")),
|
||||||
|
desc="[D] Delete character",
|
||||||
|
callback=self.on_delete,
|
||||||
|
),
|
||||||
|
Choice(
|
||||||
|
keys=list(KEYS_CANCEL), desc="[Q] Quit", callback=self.on_quit
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
scr_height, scr_width = self.screen.getmaxyx()
|
||||||
|
self.screen.erase()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
self.main_menu = MainMenu(
|
||||||
|
choices=self.choices, scr_height=scr_height, scr_width=scr_width
|
||||||
|
)
|
||||||
|
self.main_menu.render()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self.main_menu.stop()
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
self.main_menu.keypress(key)
|
||||||
|
self.main_menu.render()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from .base import Widget, WindowWrapper
|
||||||
|
from .data_table import DataTable
|
||||||
|
from .focusable import Focusable
|
||||||
|
from .label import Label
|
||||||
|
from .list_box import ListBox
|
||||||
|
from .menu import Menu
|
||||||
|
from .progress_bar import ProgressBar
|
||||||
|
from .scrollable import Scrollable
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
|
||||||
|
class Widget:
|
||||||
|
def __del__(self) -> None:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WindowWrapper(Widget):
|
||||||
|
def __init__(
|
||||||
|
self, parent: T.Optional[T.Any], h: int, w: int, y: int, x: int
|
||||||
|
) -> None:
|
||||||
|
self._parent = parent
|
||||||
|
self._win: T.Optional[T.Any]
|
||||||
|
|
||||||
|
if not parent:
|
||||||
|
self._win = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self._win = parent.derwin(h, w, y, x)
|
||||||
|
self._win.noutrefresh()
|
||||||
|
except curses.error:
|
||||||
|
self._win = None
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
del self._win
|
||||||
|
self._win = None
|
||||||
|
|
||||||
|
def getbegyx(self) -> T.Tuple[int, int]:
|
||||||
|
if not self._win:
|
||||||
|
return (0, 0)
|
||||||
|
return self._win.getbegyx()
|
||||||
|
|
||||||
|
def getmaxyx(self) -> T.Tuple[int, int]:
|
||||||
|
if not self._win:
|
||||||
|
return (0, 0)
|
||||||
|
return self._win.getmaxyx()
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
|
||||||
|
|
||||||
|
from .scrollable import Scrollable
|
||||||
|
|
||||||
|
|
||||||
|
class DataTable(Scrollable):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: T.Any,
|
||||||
|
h: int,
|
||||||
|
w: int,
|
||||||
|
y: int,
|
||||||
|
x: int,
|
||||||
|
align_right: bool = False,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
|
||||||
|
self._align_right = align_right
|
||||||
|
self._items: T.List[T.Tuple[str, str]]
|
||||||
|
self._selected: T.Optional[int] = None
|
||||||
|
|
||||||
|
def add(self, text: str, value: str) -> None:
|
||||||
|
self._items.append((text, value))
|
||||||
|
self.scroll_to_item(-1)
|
||||||
|
|
||||||
|
def set(self, text: str, value: str) -> None:
|
||||||
|
idx = self.get_idx(text)
|
||||||
|
if idx is None:
|
||||||
|
self.add(text, value)
|
||||||
|
else:
|
||||||
|
self._items[idx] = (self._items[idx][0], value)
|
||||||
|
self.scroll_to_item(idx)
|
||||||
|
|
||||||
|
def delete(self, text: str) -> None:
|
||||||
|
idx = self.get_idx(text)
|
||||||
|
if idx is not None:
|
||||||
|
del self._items[idx]
|
||||||
|
self.scroll_to_item(idx)
|
||||||
|
|
||||||
|
def select(self, text: T.Optional[str]) -> None:
|
||||||
|
self._selected = None if text is None else self.get_idx(text)
|
||||||
|
|
||||||
|
def get_idx(self, text: str) -> T.Optional[int]:
|
||||||
|
for idx, row in enumerate(self._items):
|
||||||
|
if row[0].strip() == text.strip():
|
||||||
|
return idx
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _render_impl(self, h: int, w: int) -> None:
|
||||||
|
assert self._pad
|
||||||
|
if self._align_right:
|
||||||
|
for y, row in enumerate(self._items):
|
||||||
|
if y == self._selected and has_colors():
|
||||||
|
self._pad.attron(curses.color_pair(COLOR_HIGHLIGHT))
|
||||||
|
self._pad.move(y, 0)
|
||||||
|
self._pad.addnstr(row[0], min(len(row[0]), w))
|
||||||
|
col2_x = max(0, w - len(row[1]))
|
||||||
|
if col2_x < w:
|
||||||
|
self._pad.move(y, col2_x)
|
||||||
|
self._pad.addstr(row[1])
|
||||||
|
if y == self._selected and has_colors():
|
||||||
|
self._pad.attroff(curses.color_pair(COLOR_HIGHLIGHT))
|
||||||
|
else:
|
||||||
|
col2_x = self._get_col_width(0)
|
||||||
|
for y, row in enumerate(self._items):
|
||||||
|
if y == self._selected and has_colors():
|
||||||
|
self._pad.attron(curses.color_pair(COLOR_HIGHLIGHT))
|
||||||
|
self._pad.addnstr(y, 0, row[0], min(len(row[0]), w))
|
||||||
|
if col2_x < w:
|
||||||
|
self._pad.addnstr(
|
||||||
|
y, col2_x, row[1], min(len(row[1]), w - col2_x)
|
||||||
|
)
|
||||||
|
if y == self._selected and has_colors():
|
||||||
|
self._pad.attroff(curses.color_pair(COLOR_HIGHLIGHT))
|
||||||
|
|
||||||
|
def _get_col_width(self, x: int) -> int:
|
||||||
|
return max([len(row[0]) for row in self._items] + [0])
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import contextlib
|
||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.colors import COLOR_FOCUSED, has_colors
|
||||||
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def focus_standout(focused: bool, win: T.Any) -> T.Generator:
|
||||||
|
if focused:
|
||||||
|
if has_colors():
|
||||||
|
win.attron(curses.color_pair(COLOR_FOCUSED))
|
||||||
|
else:
|
||||||
|
win.standout()
|
||||||
|
yield
|
||||||
|
if focused:
|
||||||
|
if has_colors():
|
||||||
|
win.attroff(curses.color_pair(COLOR_FOCUSED))
|
||||||
|
else:
|
||||||
|
win.standend()
|
||||||
|
|
||||||
|
|
||||||
|
class Focusable:
|
||||||
|
def __init__(self, *args: T.Any, **kwargs: T.Any) -> None:
|
||||||
|
self._focused = False
|
||||||
|
self._on_focus_change = EventHandler()
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def focused(self) -> bool:
|
||||||
|
return self._focused
|
||||||
|
|
||||||
|
@focused.setter
|
||||||
|
def focused(self, focused: bool) -> None:
|
||||||
|
self._focused = focused
|
||||||
|
self._on_focus_change()
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def focus_standout(self, win: T.Any) -> T.Generator:
|
||||||
|
with focus_standout(self._focused, win):
|
||||||
|
yield
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from .base import WindowWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Label(WindowWrapper):
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
self._win.erase()
|
||||||
|
self._win.addnstr(text, min(len(text), self.getmaxyx()[1] - 1))
|
||||||
|
self._win.noutrefresh()
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
|
||||||
|
|
||||||
|
from .scrollable import Scrollable
|
||||||
|
|
||||||
|
|
||||||
|
class ListBox(Scrollable):
|
||||||
|
_items: T.List[str]
|
||||||
|
|
||||||
|
def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
self._selected: T.Optional[int] = None
|
||||||
|
|
||||||
|
def add(self, text: str) -> None:
|
||||||
|
self._items.append(text)
|
||||||
|
self.scroll_to_item(-1)
|
||||||
|
|
||||||
|
def set(self, idx: int, value: str) -> None:
|
||||||
|
if idx < 0:
|
||||||
|
idx %= len(self._items)
|
||||||
|
self._items[idx] = value
|
||||||
|
|
||||||
|
def get(self, idx: int) -> T.Optional[str]:
|
||||||
|
if not self._items:
|
||||||
|
return None
|
||||||
|
if idx < 0:
|
||||||
|
idx %= len(self._items)
|
||||||
|
try:
|
||||||
|
return self._items[idx]
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete(self, idx: int, count: int = 1) -> None:
|
||||||
|
del self._items[idx : idx + count]
|
||||||
|
|
||||||
|
def select(self, idx: T.Optional[int]) -> None:
|
||||||
|
if idx < 0 and len(self._items):
|
||||||
|
idx %= len(self._items)
|
||||||
|
self._selected = idx
|
||||||
|
|
||||||
|
def _render_impl(self, h: int, w: int) -> None:
|
||||||
|
assert self._pad
|
||||||
|
for y, item in enumerate(self._items):
|
||||||
|
if y == self._selected and has_colors():
|
||||||
|
self._pad.attron(curses.color_pair(COLOR_HIGHLIGHT))
|
||||||
|
self._pad.addnstr(y, 0, item, min(len(item), w))
|
||||||
|
if y == self._selected and has_colors():
|
||||||
|
self._pad.attroff(curses.color_pair(COLOR_HIGHLIGHT))
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import curses
|
||||||
|
import curses.ascii
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.util import KEYS_CYCLE, KEYS_DOWN, KEYS_UP, Choice
|
||||||
|
from pqcli.ui.curses.widgets.focusable import focus_standout
|
||||||
|
|
||||||
|
from .base import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class Menu(Widget):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
header: str,
|
||||||
|
choices: T.List[Choice],
|
||||||
|
active_choice: int,
|
||||||
|
scr_height: int,
|
||||||
|
scr_width: int,
|
||||||
|
) -> None:
|
||||||
|
self._header_lines = header.split("\n")
|
||||||
|
self._choices = choices
|
||||||
|
self._scr_width = scr_width
|
||||||
|
self._scr_height = scr_height
|
||||||
|
|
||||||
|
all_lines = self._header_lines + sum(
|
||||||
|
[choice.desc.splitlines() for choice in choices], []
|
||||||
|
)
|
||||||
|
|
||||||
|
w = max(map(len, all_lines)) + 1
|
||||||
|
h = len(all_lines) + 1
|
||||||
|
self._pad: T.Optional[T.Any] = curses.newpad(h, w)
|
||||||
|
|
||||||
|
for y, line in enumerate(self._header_lines):
|
||||||
|
self._pad.move(y, 0)
|
||||||
|
self._pad.addstr(line)
|
||||||
|
|
||||||
|
self._active_choice = active_choice
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
del self._pad
|
||||||
|
self._pad = None
|
||||||
|
|
||||||
|
def getmaxyx(self) -> T.Tuple[int, int]:
|
||||||
|
if not self._pad:
|
||||||
|
return (0, 0)
|
||||||
|
return self._pad.getmaxyx()
|
||||||
|
|
||||||
|
def next(self) -> None:
|
||||||
|
self._active_choice = min(
|
||||||
|
len(self._choices) - 1, self._active_choice + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def prev(self) -> None:
|
||||||
|
self._active_choice = max(0, self._active_choice - 1)
|
||||||
|
|
||||||
|
def cycle(self) -> None:
|
||||||
|
self._active_choice = (self._active_choice + 1) % len(self._choices)
|
||||||
|
|
||||||
|
def keypress(self, key: int) -> None:
|
||||||
|
for choice in self._choices:
|
||||||
|
if key in choice.keys:
|
||||||
|
choice.callback()
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == curses.ascii.NL:
|
||||||
|
self._choices[self._active_choice].callback()
|
||||||
|
elif key in KEYS_DOWN:
|
||||||
|
self.next()
|
||||||
|
elif key in KEYS_UP:
|
||||||
|
self.prev()
|
||||||
|
elif key in KEYS_CYCLE:
|
||||||
|
self.cycle()
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
if not self._pad:
|
||||||
|
return
|
||||||
|
|
||||||
|
y = len(self._header_lines) + 1
|
||||||
|
for i, choice in enumerate(self._choices):
|
||||||
|
self._pad.move(y, 0)
|
||||||
|
with focus_standout(i == self._active_choice, self._pad):
|
||||||
|
lines = choice.desc.splitlines()
|
||||||
|
max_len = max(map(len, lines))
|
||||||
|
lines = [line.ljust(max_len) for line in lines]
|
||||||
|
self._pad.addstr("\n".join(lines))
|
||||||
|
y += len(choice.desc.splitlines())
|
||||||
|
|
||||||
|
self._pad.refresh(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
(self._scr_height - self._pad.getmaxyx()[0]) // 2,
|
||||||
|
(self._scr_width - self._pad.getmaxyx()[1]) // 2,
|
||||||
|
self._scr_height - 1,
|
||||||
|
self._scr_width - 1,
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import curses
|
||||||
|
import datetime
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.lingo import format_timespan
|
||||||
|
from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors
|
||||||
|
from pqcli.ui.curses.widgets.base import WindowWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressBar(WindowWrapper):
|
||||||
|
def __init__(
|
||||||
|
self, parent: T.Any, h: int, w: int, y: int, x: int, show_time: bool
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
|
||||||
|
self._cur_pos = 0.0
|
||||||
|
self._max_pos = 1.0
|
||||||
|
|
||||||
|
self._show_time = show_time
|
||||||
|
self._last_tick: T.Optional[T.Tuple[datetime.datetime, float]] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_left(self) -> T.Optional[datetime.timedelta]:
|
||||||
|
if self._last_tick is None:
|
||||||
|
return None
|
||||||
|
time_then, pos_then = self._last_tick
|
||||||
|
time_now, pos_now = datetime.datetime.now(), self._cur_pos
|
||||||
|
div = (time_now - time_then).total_seconds()
|
||||||
|
if div == 0:
|
||||||
|
return None
|
||||||
|
speed = (pos_now - pos_then) / div
|
||||||
|
if not speed:
|
||||||
|
return None
|
||||||
|
return datetime.timedelta(seconds=(self._max_pos - pos_now) / speed)
|
||||||
|
|
||||||
|
def set_position(self, cur_pos: float, max_pos: float) -> None:
|
||||||
|
if not self._win:
|
||||||
|
return
|
||||||
|
self._win.erase()
|
||||||
|
|
||||||
|
if self._last_tick is None or cur_pos == 0 or max_pos != self._max_pos:
|
||||||
|
self._last_tick = (datetime.datetime.now(), cur_pos)
|
||||||
|
self._cur_pos = cur_pos
|
||||||
|
self._max_pos = max_pos
|
||||||
|
|
||||||
|
text = f"{cur_pos / max_pos:.02%}"
|
||||||
|
if self.time_left and self._show_time:
|
||||||
|
text += f" ({format_timespan(self.time_left)})"
|
||||||
|
|
||||||
|
x = max(0, (self.getmaxyx()[1] - len(text)) // 2)
|
||||||
|
self._win.addnstr(0, x, text, min(len(text), self.getmaxyx()[1] - 1))
|
||||||
|
x = int(cur_pos * self.getmaxyx()[1] // max_pos)
|
||||||
|
if x > 0:
|
||||||
|
self._win.chgat(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
curses.color_pair(COLOR_PROGRESSBAR)
|
||||||
|
if has_colors()
|
||||||
|
else curses.A_REVERSE,
|
||||||
|
)
|
||||||
|
if x < self.getmaxyx()[1]:
|
||||||
|
self._win.chgat(0, x, curses.A_NORMAL)
|
||||||
|
self._win.noutrefresh()
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import curses
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.ui.curses.colors import (
|
||||||
|
COLOR_SCROLLBAR_THUMB,
|
||||||
|
COLOR_SCROLLBAR_TRACK,
|
||||||
|
has_colors,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .base import WindowWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class Scrollable(WindowWrapper):
|
||||||
|
def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None:
|
||||||
|
super().__init__(parent, h, w, y, x)
|
||||||
|
self._scroll_pos = 0
|
||||||
|
self._pad: T.Optional[T.Any] = curses.newpad(1, 1)
|
||||||
|
self._items: T.List[T.Any] = []
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self._items = []
|
||||||
|
self.scroll_to_item(0)
|
||||||
|
|
||||||
|
def scroll_page_up(self) -> None:
|
||||||
|
self.scroll_to_item(max(0, self._scroll_pos - self.getmaxyx()[0]))
|
||||||
|
|
||||||
|
def scroll_page_down(self) -> None:
|
||||||
|
self.scroll_to_item(
|
||||||
|
min(
|
||||||
|
len(self._items) - 1,
|
||||||
|
self._scroll_pos + self.getmaxyx()[0] * 2 - 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def scroll_to_item(self, y: int) -> None:
|
||||||
|
if not self._items or not self._win:
|
||||||
|
return
|
||||||
|
if y < 0:
|
||||||
|
y %= len(self._items)
|
||||||
|
|
||||||
|
h = self.getmaxyx()[0]
|
||||||
|
delta = 1 if self._scroll_pos < y else -1
|
||||||
|
while not self._scroll_pos <= y < self._scroll_pos + h:
|
||||||
|
self._scroll_pos += delta
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
super().stop()
|
||||||
|
del self._pad
|
||||||
|
self._pad = None
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
if not self._pad or not self._win:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._items:
|
||||||
|
self._pad.resize(
|
||||||
|
max(1, len(self._items)), max(1, self.getmaxyx()[1] + 1)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._pad.resize(1, 1)
|
||||||
|
|
||||||
|
self._win.erase()
|
||||||
|
self._win.noutrefresh()
|
||||||
|
|
||||||
|
self._pad.erase()
|
||||||
|
h, w = self.getmaxyx()
|
||||||
|
y, x = self.getbegyx()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._scroll_pos > 0 or self._scroll_pos + h < len(self._items):
|
||||||
|
y1 = self._scroll_pos
|
||||||
|
y2 = self._scroll_pos + h
|
||||||
|
thumb_y1 = int(y1 * h // len(self._items))
|
||||||
|
thumb_y2 = int(y2 * h // len(self._items))
|
||||||
|
|
||||||
|
attr_thumb = (
|
||||||
|
curses.color_pair(COLOR_SCROLLBAR_THUMB)
|
||||||
|
if has_colors()
|
||||||
|
else curses.A_REVERSE
|
||||||
|
)
|
||||||
|
attr_track = (
|
||||||
|
curses.color_pair(COLOR_SCROLLBAR_TRACK)
|
||||||
|
if has_colors()
|
||||||
|
else curses.A_NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for win_y in range(h):
|
||||||
|
self._win.chgat(
|
||||||
|
win_y,
|
||||||
|
w - 1,
|
||||||
|
attr_thumb
|
||||||
|
if thumb_y1 <= win_y <= thumb_y2
|
||||||
|
else attr_track,
|
||||||
|
)
|
||||||
|
self._win.noutrefresh()
|
||||||
|
|
||||||
|
self._render_impl(h, w - 1)
|
||||||
|
self._pad.noutrefresh(
|
||||||
|
self._scroll_pos, 0, y, x, y + h - 1, x + w - 2
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._render_impl(h, w)
|
||||||
|
self._pad.noutrefresh(
|
||||||
|
self._scroll_pos, 0, y, x, y + h - 1, x + w - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _render_impl(self, h: int, w: int) -> None:
|
||||||
|
raise NotImplementedError("not implemented")
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
_MAGIC = 999 # where to display the cursor (high value = don't display it)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomButton(urwid.Button):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
label: str,
|
|
||||||
*args: T.Any,
|
|
||||||
hint: T.Optional[str] = None,
|
|
||||||
**kwargs: T.Any,
|
|
||||||
) -> None:
|
|
||||||
super().__init__("", *args, **kwargs)
|
|
||||||
self._w = urwid.AttrMap(
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
("pack", urwid.Text("< ")),
|
|
||||||
urwid.SelectableIcon([label], _MAGIC),
|
|
||||||
(
|
|
||||||
"pack",
|
|
||||||
urwid.Text(f"[{hint}]" if hint else "", align="right"),
|
|
||||||
),
|
|
||||||
("pack", urwid.Text(" >")),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"button",
|
|
||||||
"button-focus",
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
|
|
||||||
class CustomLineBox(urwid.AttrMap):
|
|
||||||
def __init__(
|
|
||||||
self, widget: urwid.Widget, *args: T.Any, **kwargs: T.Any
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
urwid.LineBox(
|
|
||||||
urwid.AttrMap(widget, "linebox-content", "linebox-content"),
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
),
|
|
||||||
"linebox",
|
|
||||||
"linebox-focus",
|
|
||||||
)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
|
|
||||||
class CustomListWalker(urwid.SimpleFocusListWalker):
|
|
||||||
def append(self, item: T.Any) -> None:
|
|
||||||
super().append(item)
|
|
||||||
self.set_focus(len(self) - 1)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomListBox(urwid.ListBox):
|
|
||||||
def __init__(self, widgets: T.List[urwid.Widget]) -> None:
|
|
||||||
super().__init__(CustomListWalker(widgets))
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
# workaround for https://github.com/urwid/urwid/issues/317
|
|
||||||
|
|
||||||
|
|
||||||
class CustomProgressBar(urwid.Widget):
|
|
||||||
_sizing = frozenset(["flow"])
|
|
||||||
text_align = "center"
|
|
||||||
eighths = u" ▏▎▍▌▋▊▉"
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.position = 0.0
|
|
||||||
self.max_ = 1.0
|
|
||||||
self.normal = "progressbar-normal"
|
|
||||||
self.complete = "progressbar-done"
|
|
||||||
self.smooth = "progressbar-smooth"
|
|
||||||
|
|
||||||
def set_completion(self, value: float) -> None:
|
|
||||||
self.position = value
|
|
||||||
self._invalidate()
|
|
||||||
|
|
||||||
def set_max(self, value: T.Union[int, float]) -> None:
|
|
||||||
self.max_ = value
|
|
||||||
self._invalidate()
|
|
||||||
|
|
||||||
def rows(self, size: T.Tuple[int, int], focus: bool = False) -> int:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def get_text(self) -> str:
|
|
||||||
percent = min(100, max(0, int(self.position * 100 / self.max_)))
|
|
||||||
return str(percent) + " %"
|
|
||||||
|
|
||||||
def render(self, size: T.Tuple[int, int], focus: bool = False) -> T.Any:
|
|
||||||
(maxcol,) = size
|
|
||||||
txt = urwid.Text(self.get_text(), self.text_align, "clip")
|
|
||||||
c = txt.render((maxcol,))
|
|
||||||
t = c._text[0]
|
|
||||||
|
|
||||||
cf = self.position * maxcol / self.max_
|
|
||||||
ccol_dirty = int(cf)
|
|
||||||
ccol = len(
|
|
||||||
c._text[0][:ccol_dirty].decode("utf-8", "ignore").encode("utf-8")
|
|
||||||
)
|
|
||||||
|
|
||||||
cs = 0
|
|
||||||
cs = int((cf - ccol) * 8)
|
|
||||||
|
|
||||||
if ccol < 0 or (ccol == 0 and cs == 0):
|
|
||||||
c._attr = [[(self.normal, maxcol)]]
|
|
||||||
elif ccol >= maxcol:
|
|
||||||
c._attr = [[(self.complete, maxcol)]]
|
|
||||||
elif cs and c._text[0][ccol] == 32:
|
|
||||||
t = c._text[0]
|
|
||||||
cenc = self.eighths[cs].encode("utf-8")
|
|
||||||
c._text[0] = t[:ccol] + cenc + t[ccol + 1 :]
|
|
||||||
a = []
|
|
||||||
if ccol > 0:
|
|
||||||
a.append((self.complete, ccol))
|
|
||||||
a.append((self.smooth, len(cenc)))
|
|
||||||
if maxcol - ccol - 1 > 0:
|
|
||||||
a.append((self.normal, maxcol - ccol - 1))
|
|
||||||
c._attr = [a]
|
|
||||||
c._cs = [[(None, len(c._text[0]))]]
|
|
||||||
else:
|
|
||||||
c._attr = [[(self.complete, ccol), (self.normal, maxcol - ccol)]]
|
|
||||||
return c
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import urwid
|
|
||||||
|
|
||||||
|
|
||||||
class DoubleLineBox(urwid.AttrMap):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
top_widget: urwid.Widget,
|
|
||||||
bottom_widget: urwid.Widget,
|
|
||||||
top_title: str = "",
|
|
||||||
bottom_title: str = "",
|
|
||||||
) -> None:
|
|
||||||
self.top_line_box = urwid.LineBox(
|
|
||||||
urwid.AttrMap(top_widget, "linebox-content", "linebox-content"),
|
|
||||||
title=top_title,
|
|
||||||
bline="",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.bottom_line_box = urwid.LineBox(
|
|
||||||
urwid.AttrMap(bottom_widget, "linebox-content", "linebox-content"),
|
|
||||||
title=bottom_title,
|
|
||||||
tlcorner="├",
|
|
||||||
trcorner="┤",
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
urwid.Pile([self.top_line_box, ("pack", self.bottom_line_box)]),
|
|
||||||
"linebox",
|
|
||||||
"linebox-focus",
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_bottom_title(self, text: str) -> None:
|
|
||||||
self.bottom_line_box.set_title(text)
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import contextlib
|
|
||||||
import datetime
|
|
||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
from pqcli.config import EquipmentType, StatType
|
|
||||||
from pqcli.lingo import act_name, to_roman
|
|
||||||
from pqcli.mechanic import (
|
|
||||||
InventoryItem,
|
|
||||||
Player,
|
|
||||||
SignalMixin,
|
|
||||||
Simulation,
|
|
||||||
Spell,
|
|
||||||
)
|
|
||||||
from pqcli.ui.custom_line_box import CustomLineBox
|
|
||||||
from pqcli.ui.custom_list_box import CustomListBox
|
|
||||||
from pqcli.ui.custom_progress_bar import CustomProgressBar
|
|
||||||
from pqcli.ui.double_line_box import DoubleLineBox
|
|
||||||
from pqcli.ui.read_only_check_box import ReadOnlyCheckBox
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterSheetView(DoubleLineBox):
|
|
||||||
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.exp_bar = CustomProgressBar()
|
|
||||||
|
|
||||||
self.list_box = CustomListBox(
|
|
||||||
[
|
|
||||||
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.player.exp_bar.connect("change", self.sync_exp)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
top_widget=self.list_box,
|
|
||||||
bottom_widget=self.exp_bar,
|
|
||||||
top_title="Character Sheet",
|
|
||||||
bottom_title="Experience",
|
|
||||||
)
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
self.sync_level()
|
|
||||||
self.sync_stats()
|
|
||||||
self.sync_exp()
|
|
||||||
|
|
||||||
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]))
|
|
||||||
|
|
||||||
def sync_exp(self) -> None:
|
|
||||||
cur = self.player.exp_bar.position
|
|
||||||
max_ = self.player.exp_bar.max_
|
|
||||||
self.exp_bar.set_completion(cur)
|
|
||||||
self.exp_bar.set_max(max_)
|
|
||||||
self.set_bottom_title(f"Experience ({max_-cur:.0f} XP to go)")
|
|
||||||
|
|
||||||
|
|
||||||
class SpellBookView(CustomLineBox):
|
|
||||||
def __init__(self, player: Player) -> None:
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
self.list_box = CustomListBox([])
|
|
||||||
|
|
||||||
self.player.spell_book.connect("add", self.sync_spell_add)
|
|
||||||
self.player.spell_book.connect("change", self.sync_spell_change)
|
|
||||||
|
|
||||||
super().__init__(self.list_box, title="Spell Book")
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
del self.list_box.body[:]
|
|
||||||
for spell in self.player.spell_book:
|
|
||||||
self.sync_spell_add(spell)
|
|
||||||
|
|
||||||
def sync_spell_add(self, spell: Spell) -> None:
|
|
||||||
self.list_box.body.append(
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
("weight", 3, urwid.Text(spell.name)),
|
|
||||||
urwid.Text(to_roman(spell.level)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_spell_change(self, spell: Spell) -> None:
|
|
||||||
for widget in self.list_box.body:
|
|
||||||
if widget.contents[0][0].text == spell.name:
|
|
||||||
widget.contents[1][0].set_text(to_roman(spell.level))
|
|
||||||
|
|
||||||
|
|
||||||
class EquipmentView(CustomLineBox):
|
|
||||||
def __init__(self, player: Player) -> None:
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
self.value_texts = {
|
|
||||||
equipment_type: urwid.Text("") for equipment_type in EquipmentType
|
|
||||||
}
|
|
||||||
self.list_box = CustomListBox(
|
|
||||||
[
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
urwid.Text(equipment_type.value),
|
|
||||||
("weight", 3, self.value_texts[equipment_type]),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
for equipment_type in EquipmentType
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.player.equipment.connect("change", self.sync_equipment_change)
|
|
||||||
|
|
||||||
super().__init__(self.list_box, title="Equipment")
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
for equipment_type in EquipmentType:
|
|
||||||
self.sync_equipment_change(
|
|
||||||
equipment_type, self.player.equipment[equipment_type]
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_equipment_change(
|
|
||||||
self, equipment_type: EquipmentType, item_name: T.Optional[str]
|
|
||||||
) -> None:
|
|
||||||
self.value_texts[equipment_type].set_text(item_name or "")
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryView(DoubleLineBox):
|
|
||||||
def __init__(self, player: Player) -> None:
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
self.gold_text = urwid.Text("")
|
|
||||||
self.list_box = CustomListBox(
|
|
||||||
[
|
|
||||||
urwid.Columns(
|
|
||||||
[("weight", 3, urwid.Text("Gold")), self.gold_text]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.encumbrance_bar = CustomProgressBar()
|
|
||||||
|
|
||||||
self.player.connect("new_task", self.on_new_task)
|
|
||||||
self.player.inventory.connect("gold_change", self.sync_gold)
|
|
||||||
self.player.inventory.connect("item_add", self.sync_item_add)
|
|
||||||
self.player.inventory.connect("item_del", self.sync_item_del)
|
|
||||||
self.player.inventory.connect("item_change", self.sync_item_change)
|
|
||||||
self.player.inventory.encum_bar.connect(
|
|
||||||
"change", self.sync_encumbrance_position
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
top_widget=self.list_box,
|
|
||||||
top_title="Inventory",
|
|
||||||
bottom_widget=self.encumbrance_bar,
|
|
||||||
bottom_title="Encumbrance",
|
|
||||||
)
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
self.sync_gold()
|
|
||||||
self.sync_items()
|
|
||||||
self.sync_encumbrance_position()
|
|
||||||
|
|
||||||
def sync_gold(self) -> None:
|
|
||||||
self.gold_text.set_text(str(self.player.inventory.gold))
|
|
||||||
|
|
||||||
def sync_items(self) -> None:
|
|
||||||
del self.list_box.body[1:]
|
|
||||||
for item in self.player.inventory:
|
|
||||||
self.sync_item_add(item)
|
|
||||||
|
|
||||||
def sync_encumbrance_position(self) -> None:
|
|
||||||
cur = self.player.inventory.encum_bar.position
|
|
||||||
max_ = self.player.inventory.encum_bar.max_
|
|
||||||
self.encumbrance_bar.set_completion(cur)
|
|
||||||
self.encumbrance_bar.set_max(max_)
|
|
||||||
self.set_bottom_title(f"Encumbrance ({cur:.0f}/{max_} cubits)")
|
|
||||||
|
|
||||||
def sync_item_add(self, item: InventoryItem) -> None:
|
|
||||||
self.list_box.body.append(
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
("weight", 3, urwid.Text(item.name)),
|
|
||||||
urwid.Text(str(item.quantity)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_item_del(self, item: InventoryItem) -> None:
|
|
||||||
idx = self.get_item_idx_by_name(item.name)
|
|
||||||
if idx is not None:
|
|
||||||
del self.list_box.body[idx]
|
|
||||||
|
|
||||||
def sync_item_change(self, item: InventoryItem) -> None:
|
|
||||||
idx = self.get_item_idx_by_name(item.name)
|
|
||||||
if idx is not None:
|
|
||||||
self.list_box.body[idx].contents[1][0].set_text(str(item.quantity))
|
|
||||||
|
|
||||||
def get_item_idx_by_name(self, name: str) -> T.Optional[int]:
|
|
||||||
for i, column_widget in enumerate(self.list_box.body):
|
|
||||||
if column_widget.contents[0][0].text == name:
|
|
||||||
return i
|
|
||||||
return None
|
|
||||||
|
|
||||||
def on_new_task(self) -> None:
|
|
||||||
description = self.player.task.description if self.player.task else "?"
|
|
||||||
if description.lower().startswith("sell"):
|
|
||||||
self.list_box.set_focus(0)
|
|
||||||
|
|
||||||
|
|
||||||
class PlotView(DoubleLineBox):
|
|
||||||
cutoff = 100
|
|
||||||
|
|
||||||
def __init__(self, player: Player) -> None:
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
self.list_box = CustomListBox([])
|
|
||||||
self.plot_bar = CustomProgressBar()
|
|
||||||
|
|
||||||
self.player.quest_book.connect("complete_act", self.sync_act_add)
|
|
||||||
self.player.quest_book.plot_bar.connect("change", self.sync_position)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
top_widget=self.list_box,
|
|
||||||
top_title="Plot Development",
|
|
||||||
bottom_widget=self.plot_bar,
|
|
||||||
)
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
self.sync_acts()
|
|
||||||
self.sync_position()
|
|
||||||
|
|
||||||
def sync_acts(self) -> None:
|
|
||||||
del self.list_box.body[:]
|
|
||||||
for act_number in range(
|
|
||||||
max(0, self.player.quest_book.act - self.cutoff),
|
|
||||||
self.player.quest_book.act,
|
|
||||||
):
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(act_name(act_number), state=True)
|
|
||||||
)
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(act_name(self.player.quest_book.act), state=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_act_add(self) -> None:
|
|
||||||
del self.list_box.body[: -self.cutoff]
|
|
||||||
if self.list_box.body:
|
|
||||||
self.list_box.body[-1].set_state(True)
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(act_name(self.player.quest_book.act), state=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_position(self) -> None:
|
|
||||||
self.plot_bar.set_completion(self.player.quest_book.plot_bar.position)
|
|
||||||
self.plot_bar.set_max(self.player.quest_book.plot_bar.max_)
|
|
||||||
|
|
||||||
|
|
||||||
class QuestBookView(DoubleLineBox):
|
|
||||||
cutoff = 100
|
|
||||||
|
|
||||||
def __init__(self, player: Player) -> None:
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
self.list_box = CustomListBox([])
|
|
||||||
self.quest_bar = CustomProgressBar()
|
|
||||||
|
|
||||||
self.player.quest_book.connect("complete_quest", self.sync_quest_add)
|
|
||||||
self.player.quest_book.quest_bar.connect("change", self.sync_position)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
top_widget=self.list_box,
|
|
||||||
top_title="Quests",
|
|
||||||
bottom_widget=self.quest_bar,
|
|
||||||
)
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
self.sync_quests()
|
|
||||||
self.sync_position()
|
|
||||||
|
|
||||||
def sync_quests(self) -> None:
|
|
||||||
del self.list_box.body[:]
|
|
||||||
for quest_name in self.player.quest_book.quests[-self.cutoff :]:
|
|
||||||
self.list_box.body.append(ReadOnlyCheckBox(quest_name, state=True))
|
|
||||||
if self.list_box.body:
|
|
||||||
self.list_box.body[-1].set_state(False)
|
|
||||||
|
|
||||||
def sync_quest_add(self) -> None:
|
|
||||||
del self.list_box.body[: -self.cutoff]
|
|
||||||
if self.list_box.body:
|
|
||||||
self.list_box.body[-1].set_state(True)
|
|
||||||
self.list_box.body.append(
|
|
||||||
ReadOnlyCheckBox(self.player.quest_book.quests[-1], state=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_position(self) -> None:
|
|
||||||
self.quest_bar.set_completion(
|
|
||||||
self.player.quest_book.quest_bar.position
|
|
||||||
)
|
|
||||||
self.quest_bar.set_max(self.player.quest_book.quest_bar.max_)
|
|
||||||
|
|
||||||
|
|
||||||
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.connect("new_task", self.sync_task_name)
|
|
||||||
self.player.task_bar.connect("change", self.sync_position)
|
|
||||||
|
|
||||||
super().__init__([self.current_task_text, self.current_task_bar])
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
def sync(self) -> None:
|
|
||||||
self.sync_task_name()
|
|
||||||
self.sync_position()
|
|
||||||
|
|
||||||
def sync_task_name(self) -> None:
|
|
||||||
self.current_task_text.set_text(
|
|
||||||
f"{self.player.task.description}..." if self.player.task else "?"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 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, args: argparse.Namespace
|
|
||||||
) -> None:
|
|
||||||
self.loop = loop
|
|
||||||
self.player = player
|
|
||||||
self.args = args
|
|
||||||
self.simulation = Simulation(player)
|
|
||||||
self.last_tick = datetime.datetime.now()
|
|
||||||
|
|
||||||
self.character_sheet_view = CharacterSheetView(player)
|
|
||||||
self.spell_book_view = SpellBookView(player)
|
|
||||||
self.equipment_view = EquipmentView(player)
|
|
||||||
self.inventory_view = InventoryView(player)
|
|
||||||
self.plot_view = PlotView(player)
|
|
||||||
self.quest_book_view = QuestBookView(player)
|
|
||||||
self.task_view = TaskView(player)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
[
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"weight",
|
|
||||||
1,
|
|
||||||
urwid.Pile(
|
|
||||||
[
|
|
||||||
(19, self.character_sheet_view),
|
|
||||||
self.spell_book_view,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"weight",
|
|
||||||
2,
|
|
||||||
urwid.Pile(
|
|
||||||
[
|
|
||||||
(15, self.equipment_view),
|
|
||||||
self.inventory_view,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"weight",
|
|
||||||
2,
|
|
||||||
urwid.Pile(
|
|
||||||
[(15, self.plot_view), self.quest_book_view]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
("pack", self.task_view),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def cancel(self) -> None:
|
|
||||||
self._emit("cancel")
|
|
||||||
|
|
||||||
def tick(self) -> None:
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
elapsed = (now - self.last_tick).total_seconds()
|
|
||||||
self.simulation.tick(elapsed * 1000)
|
|
||||||
self.last_tick = datetime.datetime.now()
|
|
||||||
|
|
||||||
def unhandled_input(self, key: str) -> bool:
|
|
||||||
if key == "esc":
|
|
||||||
self.cancel()
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.args.cheats:
|
|
||||||
if key == "e":
|
|
||||||
self.simulation.player.exp_bar.increment(1)
|
|
||||||
return True
|
|
||||||
|
|
||||||
fast_forward = {"t": 1, "T": 100, "ctrl t": 10000}
|
|
||||||
if key in fast_forward:
|
|
||||||
iterations = fast_forward[key]
|
|
||||||
ctx_mgr = (
|
|
||||||
self.suppress_updates()
|
|
||||||
if iterations >= 1000
|
|
||||||
else contextlib.suppress()
|
|
||||||
)
|
|
||||||
|
|
||||||
with ctx_mgr:
|
|
||||||
for _ in range(iterations):
|
|
||||||
self.simulation.tick()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def suppress_updates(self) -> T.Generator:
|
|
||||||
old = SignalMixin.emit
|
|
||||||
SignalMixin.emit = lambda *_: None
|
|
||||||
yield
|
|
||||||
self.character_sheet_view.sync()
|
|
||||||
self.spell_book_view.sync()
|
|
||||||
self.equipment_view.sync()
|
|
||||||
self.inventory_view.sync()
|
|
||||||
self.plot_view.sync()
|
|
||||||
self.quest_book_view.sync()
|
|
||||||
self.task_view.sync()
|
|
||||||
SignalMixin.emit = old
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import random
|
|
||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
import urwid_readline
|
|
||||||
|
|
||||||
from pqcli.config import CLASSES, PRIME_STATS, RACES
|
|
||||||
from pqcli.mechanic import StatsBuilder, create_player, generate_name
|
|
||||||
from pqcli.ui.custom_button import CustomButton
|
|
||||||
from pqcli.ui.custom_line_box import CustomLineBox
|
|
||||||
|
|
||||||
|
|
||||||
class StatsBox(CustomLineBox):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.stats_builder = StatsBuilder()
|
|
||||||
self.stats = self.stats_builder.roll()
|
|
||||||
self.stat_labels = {stat: urwid.Text("0") for stat in PRIME_STATS}
|
|
||||||
self.total_label = urwid.Text("0")
|
|
||||||
self.update_values()
|
|
||||||
|
|
||||||
roll_button = CustomButton(
|
|
||||||
"Roll", hint="F5", on_press=self.on_roll_press
|
|
||||||
)
|
|
||||||
unroll_button = CustomButton(
|
|
||||||
"Unroll", hint="F6", on_press=self.on_unroll_press
|
|
||||||
)
|
|
||||||
|
|
||||||
value_texts = list(self.stat_labels.values())
|
|
||||||
label_texts = [urwid.Text(f"{stat.value}: ") for stat in PRIME_STATS]
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
urwid.ListBox(
|
|
||||||
[
|
|
||||||
urwid.Columns(
|
|
||||||
[urwid.Pile(label_texts), urwid.Pile(value_texts)]
|
|
||||||
),
|
|
||||||
urwid.Divider(),
|
|
||||||
urwid.Columns([urwid.Text("Total: "), self.total_label]),
|
|
||||||
urwid.Divider(),
|
|
||||||
roll_button,
|
|
||||||
unroll_button,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
title="Stats",
|
|
||||||
)
|
|
||||||
|
|
||||||
def roll(self) -> None:
|
|
||||||
self.stats = self.stats_builder.roll()
|
|
||||||
self.update_values()
|
|
||||||
|
|
||||||
def unroll(self) -> None:
|
|
||||||
try:
|
|
||||||
self.stats = self.stats_builder.unroll()
|
|
||||||
except IndexError:
|
|
||||||
return
|
|
||||||
self.update_values()
|
|
||||||
|
|
||||||
def on_roll_press(self, _user_data: T.Any) -> None:
|
|
||||||
self.roll()
|
|
||||||
|
|
||||||
def on_unroll_press(self, _user_data: T.Any) -> None:
|
|
||||||
self.unroll()
|
|
||||||
|
|
||||||
def update_values(self) -> None:
|
|
||||||
for stat, edit in self.stat_labels.items():
|
|
||||||
edit.set_text(str(self.stats[stat]))
|
|
||||||
self.total_label.set_text(
|
|
||||||
str(sum(self.stats[stat] for stat in self.stat_labels.keys()))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RaceBox(CustomLineBox):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.race = random.choice(RACES)
|
|
||||||
|
|
||||||
radio_buttons: T.List[urwid.RadioButton] = []
|
|
||||||
for race in RACES:
|
|
||||||
urwid.RadioButton(
|
|
||||||
group=radio_buttons,
|
|
||||||
label=race.name,
|
|
||||||
state=race.name == self.race.name,
|
|
||||||
on_state_change=self.on_state_change,
|
|
||||||
user_data=race,
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(urwid.ListBox(radio_buttons), title="Race")
|
|
||||||
|
|
||||||
def on_state_change(
|
|
||||||
self, _widget: urwid.RadioButton, new_state: bool, user_data: T.Any
|
|
||||||
) -> None:
|
|
||||||
if new_state:
|
|
||||||
self.race = user_data
|
|
||||||
|
|
||||||
|
|
||||||
class ClassBox(CustomLineBox):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.class_ = random.choice(CLASSES)
|
|
||||||
|
|
||||||
radio_buttons: T.List[urwid.RadioButton] = []
|
|
||||||
for class_ in CLASSES:
|
|
||||||
urwid.RadioButton(
|
|
||||||
group=radio_buttons,
|
|
||||||
label=class_.name,
|
|
||||||
state=class_.name == self.class_.name,
|
|
||||||
on_state_change=self.on_state_change,
|
|
||||||
user_data=class_,
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(urwid.ListBox(radio_buttons), title="Class")
|
|
||||||
|
|
||||||
def on_state_change(
|
|
||||||
self, _widget: urwid.RadioButton, new_state: bool, user_data: T.Any
|
|
||||||
) -> None:
|
|
||||||
if new_state:
|
|
||||||
self.class_ = user_data
|
|
||||||
|
|
||||||
|
|
||||||
class NewGameView(urwid.Pile):
|
|
||||||
signals = ["confirm", "cancel"]
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.race_box = RaceBox()
|
|
||||||
self.class_box = ClassBox()
|
|
||||||
self.stats_box = StatsBox()
|
|
||||||
|
|
||||||
self.char_name_edit = urwid_readline.ReadlineEdit(
|
|
||||||
"Name: ", generate_name()
|
|
||||||
)
|
|
||||||
generate_char_name_btn = CustomButton(
|
|
||||||
"Generate random name",
|
|
||||||
hint="F7",
|
|
||||||
on_press=self.on_generate_char_name_press,
|
|
||||||
)
|
|
||||||
|
|
||||||
buttons_box = urwid.Filler(
|
|
||||||
urwid.Padding(
|
|
||||||
urwid.Pile(
|
|
||||||
[
|
|
||||||
CustomButton(
|
|
||||||
"Sold!", hint="F10", on_press=self.on_confirm_press
|
|
||||||
),
|
|
||||||
CustomButton(
|
|
||||||
"Cancel", hint="Esc", on_press=self.on_cancel_press
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
width=20,
|
|
||||||
align="center",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
[
|
|
||||||
(1, urwid.Filler(self.char_name_edit)),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
urwid.Filler(
|
|
||||||
urwid.Padding(
|
|
||||||
generate_char_name_btn,
|
|
||||||
left=len(self.char_name_edit.caption),
|
|
||||||
width=25,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
("weight", 2, self.race_box),
|
|
||||||
("weight", 2, self.class_box),
|
|
||||||
urwid.Pile([(13, self.stats_box), buttons_box]),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_random_char_name(self) -> None:
|
|
||||||
self.char_name_edit.edit_text = generate_name()
|
|
||||||
self.char_name_edit.edit_pos = len(self.char_name_edit.edit_text)
|
|
||||||
|
|
||||||
def confirm(self) -> None:
|
|
||||||
player = create_player(
|
|
||||||
name=self.char_name_edit.edit_text,
|
|
||||||
race=self.race_box.race,
|
|
||||||
class_=self.class_box.class_,
|
|
||||||
stats=self.stats_box.stats,
|
|
||||||
)
|
|
||||||
self._emit("confirm", player)
|
|
||||||
|
|
||||||
def cancel(self) -> None:
|
|
||||||
self._emit("cancel")
|
|
||||||
|
|
||||||
def on_generate_char_name_press(self, _user_data: T.Any) -> None:
|
|
||||||
self.generate_random_char_name()
|
|
||||||
|
|
||||||
def on_confirm_press(self, _user_data: T.Any) -> None:
|
|
||||||
self.confirm()
|
|
||||||
|
|
||||||
def on_cancel_press(self, _user_data: T.Any) -> None:
|
|
||||||
self.cancel()
|
|
||||||
|
|
||||||
def unhandled_input(self, key: str) -> bool:
|
|
||||||
if key == "esc":
|
|
||||||
self.cancel()
|
|
||||||
return True
|
|
||||||
|
|
||||||
if key == "f7":
|
|
||||||
self.generate_random_char_name()
|
|
||||||
return True
|
|
||||||
|
|
||||||
if key == "f5":
|
|
||||||
self.stats_box.roll()
|
|
||||||
return True
|
|
||||||
|
|
||||||
if key == "f6":
|
|
||||||
self.stats_box.unroll()
|
|
||||||
return True
|
|
||||||
|
|
||||||
if key == "f10":
|
|
||||||
self.confirm()
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import urwid
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyCheckBox(urwid.Text):
|
|
||||||
def __init__(self, text: str, state: bool = False) -> None:
|
|
||||||
self.icons = {
|
|
||||||
key: widget.text for key, widget in urwid.CheckBox.states.items()
|
|
||||||
}
|
|
||||||
self._caption = text
|
|
||||||
|
|
||||||
super().__init__("")
|
|
||||||
|
|
||||||
self.set_state(state)
|
|
||||||
|
|
||||||
def set_state(self, state: bool) -> None:
|
|
||||||
self.set_text(self.icons[state] + " " + self._caption)
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import typing as T
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
|
|
||||||
from pqcli.lingo import act_name, to_roman
|
|
||||||
from pqcli.roster import Roster
|
|
||||||
from pqcli.ui.custom_button import CustomButton
|
|
||||||
|
|
||||||
|
|
||||||
class RosterView(urwid.Filler):
|
|
||||||
signals = ["load_game", "new_game", "delete_game", "exit"]
|
|
||||||
|
|
||||||
def __init__(self, roster: Roster) -> None:
|
|
||||||
self.roster = roster
|
|
||||||
|
|
||||||
logo = urwid.BigText("ProgressQuest", urwid.HalfBlock5x4Font())
|
|
||||||
|
|
||||||
buttons = []
|
|
||||||
for player_idx, player in enumerate(self.roster.players):
|
|
||||||
best_equip = player.equipment.best
|
|
||||||
|
|
||||||
best_spell = player.spell_book.best
|
|
||||||
if best_spell:
|
|
||||||
best_spell_name = (
|
|
||||||
best_spell.name + " " + to_roman(best_spell.level)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
best_spell_name = "-"
|
|
||||||
|
|
||||||
best_stat_name = (
|
|
||||||
f"{player.stats.best.value} {player.stats[player.stats.best]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
label = (
|
|
||||||
f"{player.name} the {player.race.name} "
|
|
||||||
f"({act_name(player.quest_book.act)})\n"
|
|
||||||
f"Level {player.level} {player.class_.name}\n"
|
|
||||||
f"{best_equip} / {best_spell_name} / {best_stat_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
buttons.append(
|
|
||||||
urwid.Columns(
|
|
||||||
[
|
|
||||||
CustomButton(
|
|
||||||
label=label,
|
|
||||||
on_press=self.on_resume_game_press,
|
|
||||||
user_data=player_idx,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
10,
|
|
||||||
CustomButton(
|
|
||||||
label="Delete",
|
|
||||||
on_press=self.on_delete_game_press,
|
|
||||||
user_data=player_idx,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
buttons.append(urwid.Divider())
|
|
||||||
|
|
||||||
buttons.append(
|
|
||||||
CustomButton(
|
|
||||||
label="Create a new character",
|
|
||||||
hint="F1",
|
|
||||||
on_press=self.on_new_game_press,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
buttons.append(
|
|
||||||
CustomButton(label="Exit", hint="Esc", on_press=self.on_exit_press)
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
urwid.Padding(
|
|
||||||
urwid.Pile(
|
|
||||||
[urwid.Padding(logo, width="clip"), urwid.Divider()]
|
|
||||||
+ buttons
|
|
||||||
+ [
|
|
||||||
urwid.Divider(),
|
|
||||||
urwid.Text("Use arrow keys to move around."),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
align="center",
|
|
||||||
width=logo.pack()[0],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_resume_game_press(
|
|
||||||
self, _widget: urwid.Widget, user_data: T.Any
|
|
||||||
) -> None:
|
|
||||||
player_idx = T.cast(int, user_data)
|
|
||||||
self._emit("load_game", player_idx)
|
|
||||||
|
|
||||||
def on_delete_game_press(
|
|
||||||
self, _widget: urwid.Widget, user_data: T.Any
|
|
||||||
) -> None:
|
|
||||||
player_idx = T.cast(int, user_data)
|
|
||||||
self._emit("delete_game", player_idx)
|
|
||||||
|
|
||||||
def on_new_game_press(self, _user_data: T.Any) -> None:
|
|
||||||
self._emit("new_game")
|
|
||||||
|
|
||||||
def on_exit_press(self, _user_data: T.Any) -> None:
|
|
||||||
self._emit("exit")
|
|
||||||
|
|
||||||
def unhandled_input(self, key: str) -> bool:
|
|
||||||
if key == "esc":
|
|
||||||
self._emit("exit")
|
|
||||||
return True
|
|
||||||
|
|
||||||
if key == "f1":
|
|
||||||
self._emit("new_game")
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
py36 = true
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
known_first_party = "pqcli"
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -1,17 +1,21 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
author="rr-",
|
author="rr-",
|
||||||
author_email="rr-@sakuya.pl",
|
author_email="rr-@sakuya.pl",
|
||||||
name="pqcli",
|
name="pqcli",
|
||||||
long_description="Progress Quest: the CLI edition",
|
long_description="Progress Quest: the CLI edition",
|
||||||
version="0.2",
|
version="1.0.2",
|
||||||
url="https://github.com/rr-/pq-cli.git",
|
url="https://github.com/rr-/pq-cli.git",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
entry_points={"console_scripts": ["pqcli = pqcli.__main__:main"]},
|
entry_points={"console_scripts": ["pqcli = pqcli.__main__:main"]},
|
||||||
package_dir={"pqcli": "pqcli"},
|
package_dir={"pqcli": "pqcli"},
|
||||||
install_requires=["xdg", "urwid", "urwid_readline"],
|
install_requires=[
|
||||||
|
"xdg",
|
||||||
|
"urwid",
|
||||||
|
"urwid_readline",
|
||||||
|
'windows-curses;platform_system=="Windows"',
|
||||||
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
|
|||||||
Reference in New Issue
Block a user