nodes/waypoint_ui.pyΒΆ

Part of Dungeon Explorer.

  1"""Waypoint selection popup: teleport to previously visited floors."""
  2
  3from simvx.core import Anchor, Control, Property
  4from simvx.core.input import Key, KeyEvent, Pointer, PointerEvent, PointerPhase
  5
  6from ._back_button import back_button_hit, draw_back_button
  7
  8
  9class WaypointUI(Control):
 10    """Shows activated waypoints, lets player select and teleport."""
 11
 12    visible = Property(
 13        False,
 14        coerce=bool,
 15        hint="Whether this node and its subtree are drawn",
 16        on_change="_on_visible_changed",
 17    )
 18
 19    def __init__(self, on_teleport=None, **kwargs):
 20        super().__init__(name="WaypointUI", anchor=Anchor.FULL_RECT, **kwargs)
 21
 22        self._waypoints: list[int] = []
 23        self._cursor = 0
 24        self._on_teleport = on_teleport
 25
 26    def set_waypoints(self, waypoints: list[int]):
 27        self._waypoints = sorted(waypoints)
 28
 29    def show(self):
 30        self._cursor = 0
 31        self.show_overlay("blocking")
 32
 33    # -- Input --
 34
 35    def on_gui_input(self, event):
 36        if not self.visible:
 37            return
 38        if isinstance(event, PointerEvent):
 39            if event.button in Pointer.PRIMARY and event.phase is PointerPhase.DOWN:
 40                return self._handle_click(*event.position)
 41            return None
 42        if isinstance(event, KeyEvent) and event.pressed:
 43            if event.key in (Key.W, Key.UP):
 44                self._cursor = max(0, self._cursor - 1)
 45                return True
 46            elif event.key in (Key.S, Key.DOWN):
 47                self._cursor = min(self._cursor + 1, max(0, len(self._waypoints) - 1))
 48                return True
 49            elif event.key in (Key.SPACE, Key.ENTER):
 50                if self._waypoints and 0 <= self._cursor < len(self._waypoints):
 51                    self._teleport_to(self._waypoints[self._cursor])
 52                return True
 53            elif event.key is Key.E:
 54                self.cancel_requested.emit()
 55                self.close_overlay()
 56                return True
 57
 58    def _handle_click(self, mx: float, my: float) -> bool | None:
 59        if back_button_hit(mx, my):
 60            self.cancel_requested.emit()
 61            self.close_overlay()
 62            return True
 63        sw, sh = self._screen_size()
 64        pw = 320
 65        rows = max(1, len(self._waypoints))
 66        ph = 110 + rows * 30 + 30
 67        px = (sw - pw) / 2
 68        py = (sh - ph) / 2
 69        list_y = py + 60
 70        for i, floor in enumerate(self._waypoints):
 71            y = list_y + i * 30
 72            if px + 20 <= mx <= px + pw - 20 and y - 2 <= my <= y + 26:
 73                self._cursor = i
 74                self._teleport_to(floor)
 75                return True
 76
 77    def _teleport_to(self, floor: int):
 78        if self._on_teleport:
 79            self._on_teleport(floor)
 80        self.cancel_requested.emit()
 81        self.close_overlay()
 82
 83    # -- Rendering --
 84
 85    def _screen_size(self) -> tuple[float, float]:
 86        if self.tree is not None:
 87            sw, sh = self.tree.screen_size
 88            return float(sw), float(sh)
 89        return 1280.0, 720.0
 90
 91    def on_draw(self, renderer):
 92        if not self.visible:
 93            return
 94        sw, sh = self._screen_size()
 95        renderer.rect((0, 0), (sw, sh), fill=(0.0, 0.0, 0.0, 0.6))
 96
 97        # Panel: styled to match the other popups in the game.
 98        pw = 320
 99        rows = max(1, len(self._waypoints))
100        ph = 110 + rows * 30 + 30
101        px = (sw - pw) / 2
102        py = (sh - ph) / 2
103        renderer.rect((px, py), (pw, ph), fill=(0.12, 0.12, 0.18, 0.95))
104        renderer.rect((px, py), (pw, 36), fill=(0.15, 0.15, 0.25, 1.0))
105        renderer.text("WAYPOINTS", (px + 12, py + 8), fill=(0.3, 0.7, 1.0), scale=1.5)
106
107        list_y = py + 60
108        cx = px + pw / 2
109        if not self._waypoints:
110            renderer.text("No waypoints activated yet.", (px + 30, list_y), fill=(0.5, 0.5, 0.5), scale=1.0)
111        for i, floor in enumerate(self._waypoints):
112            y = list_y + i * 30
113            sel = i == self._cursor
114            colour = (1.0, 1.0, 1.0) if sel else (0.6, 0.6, 0.6)
115            if sel:
116                renderer.rect((px + 20, y - 2), (pw - 40, 26), fill=(0.2, 0.2, 0.4, 0.8))
117            renderer.text(f"Floor {floor}", (cx - 30, y), fill=colour, scale=1.2)
118
119        renderer.text("Space: Teleport   Esc: Close", (px + 20, py + ph - 25), fill=(0.4, 0.4, 0.4), scale=0.8)
120        draw_back_button(renderer)