Spaces:
Running on Zero
Running on Zero
| """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) | |