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)