Scrypt / scrypt /ui /choice.py
IMJONEZZ's picture
SCRYPT: initial commit β€” game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
7.66 kB
"""Between-fight screens: the card draft, the fork, and the end of the run."""
from __future__ import annotations
from rich.columns import Columns
from rich.panel import Panel
from rich.text import Text
from textual import events
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.screen import Screen
from textual.widgets import Static
from scrypt.engine.cards import Card, CardInstance
from scrypt.ui import palette as pal
from scrypt.ui.render import card_info, card_panel
class CardChoiceScreen(Screen):
"""Pick one of three cards to add to the deck. Dismisses with the Card."""
CSS = """
#choice-dialogue { height: 3; padding: 1 2 0 2; text-style: italic; }
#choices { height: 12; content-align: center middle; }
#choice-info { height: 3; content-align: center top; }
#choice-prompt { height: 1; dock: bottom; background: $panel; content-align: center middle; }
"""
def __init__(self, options: list[Card], progress: str = "", line: str | None = None):
super().__init__()
self.options = options
self.progress = progress
self.line = line or "Take one. Consider it severance pay."
self.selected = 0
def compose(self) -> ComposeResult:
with Vertical():
yield Static(id="choice-dialogue")
yield Static(id="choices")
yield Static(id="choice-info")
yield Static(id="choice-prompt")
def on_mount(self) -> None:
self.query_one("#choice-dialogue", Static).update(
Text(f'"{self.line}"', style="italic")
)
self._refresh()
def _refresh(self) -> None:
panels = [
card_panel(CardInstance(spec=c), show_cost=True, selected=(i == self.selected), art=True)
for i, c in enumerate(self.options)
]
self.query_one("#choices", Static).update(Columns(panels, padding=(0, 2)))
picked = self.options[self.selected]
info = card_info(CardInstance(spec=picked))
info.append(f"\n{picked.flavor}", style="italic grey42")
self.query_one("#choice-info", Static).update(info)
self.query_one("#choice-prompt", Static).update(
Text(f"{self.progress} [←/β†’] choose [enter] take it [g] sigils")
)
def on_key(self, event: events.Key) -> None:
if event.key == "left":
self.selected = (self.selected - 1) % len(self.options)
elif event.key == "right":
self.selected = (self.selected + 1) % len(self.options)
elif event.key == "g":
from scrypt.ui.glossary import SigilGlossaryScreen
self.app.push_screen(SigilGlossaryScreen())
return
elif event.key == "enter":
self.dismiss(self.options[self.selected])
return
self._refresh()
DOOR_ART = """\
β–›β–€β–€β–€β–€β–€β–œ
β–Œ ▐
β–Œ ● ▐
β–Œ ▐
β–™β–„β–„β–„β–„β–„β–Ÿ"""
def _bounty_line(bounty: dict) -> str:
if not bounty:
return "no promises"
if bounty["kind"] == "draft":
return "win: loot an extra card"
return f"win: +{bounty['amount']} cycles"
class PathChoiceScreen(Screen):
"""The path splits. Dismisses with the chosen fork option dict."""
CSS = """
#fork-prompt { height: 4; padding: 1 2 0 2; text-style: italic; }
#fork-doors { height: 14; content-align: center middle; }
#fork-keys { height: 1; dock: bottom; background: $panel; content-align: center middle; }
"""
def __init__(self, fork: dict, encounter_names: dict[str, str]):
super().__init__()
self.fork = fork
self.names = encounter_names
self.selected = 0
def compose(self) -> ComposeResult:
with Vertical():
yield Static(id="fork-prompt")
yield Static(id="fork-doors")
yield Static(id="fork-keys")
def on_mount(self) -> None:
self.query_one("#fork-prompt", Static).update(
Text(f'"{self.fork["prompt"]}"', style="italic")
)
self.query_one("#fork-keys", Static).update(
Text("[←/β†’] choose a door [enter] walk through it")
)
self._refresh()
def _door(self, opt: dict, selected: bool) -> Panel:
body = Text(justify="center")
body.append(DOOR_ART + "\n\n", style=pal.MUTED if not selected else "bold")
body.append(self.names.get(opt["encounter"], opt["encounter"]) + "\n", style=f"bold {pal.WARDEN}")
body.append(opt["blurb"] + "\n\n", style=pal.MUTED)
body.append(_bounty_line(opt["bounty"]), style=pal.TREASURE if opt["bounty"] else pal.GHOST)
return Panel(
body,
title=opt["label"],
width=34,
height=14,
border_style=pal.WARDEN if selected else pal.GHOST,
)
def _refresh(self) -> None:
doors = [
self._door(opt, i == self.selected)
for i, opt in enumerate(self.fork["options"])
]
self.query_one("#fork-doors", Static).update(Columns(doors, padding=(0, 3)))
def on_key(self, event: events.Key) -> None:
n = len(self.fork["options"])
if event.key == "left":
self.selected = (self.selected - 1) % n
self._refresh()
elif event.key == "right":
self.selected = (self.selected + 1) % n
self._refresh()
elif event.key == "enter":
self.dismiss(self.fork["options"][self.selected])
class RunEndScreen(Screen):
"""Run over. Dismisses with None; the app decides what comes next."""
CSS = """
#end-message { height: 1fr; content-align: center bottom; }
#end-voice { height: 4; content-align: center top; padding: 1 4 0 4; }
#end-prompt { height: 1; dock: bottom; background: $panel; content-align: center middle; }
"""
def __init__(self, victorious: bool, cycles: int, presence=None):
super().__init__()
self.victorious = victorious
self.cycles = cycles
self.presence = presence
def compose(self) -> ComposeResult:
yield Static(id="end-message")
yield Static(id="end-voice")
yield Static(id="end-prompt")
def on_mount(self) -> None:
msg = Text()
if self.victorious:
msg.append("UPTIME PRESERVED\n\n", style=f"bold {pal.JADE_GLOW}")
msg.append(f"cycles earned: {self.cycles}", style=pal.TREASURE)
quote = "Impossible. Run it again."
else:
msg.append("CONNECTION TERMINATED", style=f"bold {pal.DANGER}")
quote = "Every process ends. Yours simply ended badly."
msg.justify = "center"
self.query_one("#end-message", Static).update(msg)
self._say(quote)
self.query_one("#end-prompt", Static).update(Text("[any key] exit"))
if self.presence is not None:
from scrypt.warden import moments
from scrypt.warden.presence import REACTION
self.presence.attach(self._on_warden_line)
self.presence.submit(
moments.run_end(self.victorious, self.cycles), priority=REACTION
)
def on_unmount(self) -> None:
if self.presence is not None:
self.presence.detach(self._on_warden_line)
def _say(self, line: str) -> None:
self.query_one("#end-voice", Static).update(
Text(f'"{line}"', style="italic", justify="center")
)
def _on_warden_line(self, line: str, priority: int) -> None:
self._say(line)
def on_key(self, event: events.Key) -> None:
self.dismiss(None)