"""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)