nodes/waypoint_ui.pyΒΆ

Part of Dungeon Explorer.

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