nodes/hotbar_assign_ui.pyΒΆ
Part of Dungeon Explorer.
1"""Hotbar assignment popup: assign unlocked active abilities to hotbar slots."""
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 HotbarAssignUI(Control):
11 """Grid of unlocked active abilities and consumable items with 4 hotbar slots at top."""
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, skill_tree=None, **kwargs):
21 super().__init__(name="HotbarAssignUI", **kwargs)
22 self.set_anchor_preset(AnchorPreset.FULL_RECT)
23
24 self._skill_tree = skill_tree
25 self._inventory = None
26 self._slot_cursor = 0
27 self._ability_cursor = 0
28 self._entries: list[dict] = []
29 self._target_slot: int = 0
30
31 def set_skill_tree(self, tree):
32 self._skill_tree = tree
33
34 def set_inventory(self, inventory):
35 self._inventory = inventory
36
37 def open_for_slot(self, slot: int):
38 self._target_slot = slot
39
40 def show(self):
41 self._slot_cursor = self._target_slot
42 self._ability_cursor = 0
43 self._entries = []
44 if self._skill_tree:
45 for sid in self._skill_tree.unlocked:
46 skill = self._skill_tree.get_skill(sid)
47 if skill and skill.get("type") == "active":
48 self._entries.append({**skill, "_kind": "skill"})
49 if self._inventory:
50 seen = set()
51 for idx in range(self._inventory.capacity):
52 item = self._inventory.get(idx)
53 if item and item.is_consumable and item.template_key not in seen:
54 seen.add(item.template_key)
55 self._entries.append(
56 {
57 "id": f"item:{item.template_key}",
58 "name": item.display_base,
59 "_kind": "item",
60 "_template_key": item.template_key,
61 }
62 )
63 self.show_overlay("blocking")
64
65 # -- Input --
66
67 def _on_gui_input(self, event):
68 if not self.visible or self._skill_tree is None:
69 return
70 if event.button == MouseButton.LEFT and event.pressed:
71 self._handle_click(float(event.position[0]), float(event.position[1]), event)
72 return
73 if event.key and event.pressed:
74 if event.key in ("w", "up"):
75 self._ability_cursor = max(0, self._ability_cursor - 1)
76 event.handled = True
77 elif event.key in ("s", "down"):
78 if self._ability_cursor < len(self._entries) - 1:
79 self._ability_cursor += 1
80 event.handled = True
81 elif event.key in ("a", "left"):
82 self._slot_cursor = max(0, self._slot_cursor - 1)
83 event.handled = True
84 elif event.key in ("d", "right"):
85 self._slot_cursor = min(3, self._slot_cursor + 1)
86 event.handled = True
87 elif event.key in ("space", "enter", "return"):
88 if 0 <= self._ability_cursor < len(self._entries):
89 entry = self._entries[self._ability_cursor]
90 self._skill_tree.assign_hotbar(self._slot_cursor, entry["id"])
91 elif not self._entries:
92 self._skill_tree.assign_hotbar(self._slot_cursor, None)
93 event.handled = True
94 elif event.key == "e":
95 self._skill_tree.assign_hotbar(self._slot_cursor, None)
96 event.handled = True
97
98 def _handle_click(self, mx: float, my: float, event):
99 if back_button_hit(mx, my):
100 self.cancel_requested.emit()
101 self.close_overlay()
102 event.handled = True
103 return
104 sw, sh = self._screen_size()
105 panel_w = 500
106 panel_h = 420
107 px = (sw - panel_w) / 2
108 py = (sh - panel_h) / 2
109 slot_size = 60
110 slot_pad = 10
111 total_sw = 4 * (slot_size + slot_pad)
112 slot_sx = px + (panel_w - total_sw) / 2
113 slot_sy = py + 50
114 for i in range(4):
115 sx = slot_sx + i * (slot_size + slot_pad)
116 if sx <= mx <= sx + slot_size and slot_sy <= my <= slot_sy + slot_size:
117 self._slot_cursor = i
118 if self._skill_tree and self._skill_tree.hotbar[i] is not None:
119 self._skill_tree.assign_hotbar(i, None)
120 event.handled = True
121 return
122 list_y = py + 140
123 for j in range(len(self._entries)):
124 iy = list_y + j * 32
125 if px + 20 <= mx <= px + panel_w - 20 and iy - 2 <= my <= iy + 28:
126 self._ability_cursor = j
127 self._skill_tree.assign_hotbar(self._slot_cursor, self._entries[j]["id"])
128 event.handled = True
129 return
130
131 # -- Rendering --
132
133 def _screen_size(self) -> tuple[float, float]:
134 if self.tree is not None:
135 sw, sh = self.tree.screen_size
136 return float(sw), float(sh)
137 return 1280.0, 720.0
138
139 def on_draw(self, renderer):
140 if not self.visible or self._skill_tree is None:
141 return
142 sw, sh = self._screen_size()
143 renderer.draw_rect((0, 0), (sw, sh), colour=(0.0, 0.0, 0.0, 0.7), filled=True)
144
145 panel_w = 500
146 panel_h = 420
147 px = (sw - panel_w) / 2
148 py = (sh - panel_h) / 2
149 renderer.draw_rect((px, py), (panel_w, panel_h), colour=(0.08, 0.08, 0.12, 0.95), filled=True)
150 renderer.draw_rect((px, py), (panel_w, 3), colour=(0.5, 0.5, 0.7, 0.8), filled=True)
151
152 cx = sw / 2
153 renderer.draw_text("ASSIGN ABILITIES", (cx - 85, py + 12), scale=1.8, colour=(1.0, 1.0, 1.0))
154
155 slot_size = 60
156 slot_pad = 10
157 total_sw = 4 * (slot_size + slot_pad)
158 slot_sx = px + (panel_w - total_sw) / 2
159 slot_sy = py + 50
160
161 for i in range(4):
162 sx = slot_sx + i * (slot_size + slot_pad)
163 selected = i == self._slot_cursor
164 bg = (0.2, 0.2, 0.3, 0.9) if selected else (0.12, 0.12, 0.16, 0.9)
165 renderer.draw_rect((sx, slot_sy), (slot_size, slot_size), colour=bg, filled=True)
166 border = (0.5, 0.6, 0.9, 1.0) if selected else (0.3, 0.3, 0.4, 0.6)
167 for edge in (
168 (sx, slot_sy, slot_size, 1),
169 (sx, slot_sy + slot_size - 1, slot_size, 1),
170 (sx, slot_sy, 1, slot_size),
171 (sx + slot_size - 1, slot_sy, 1, slot_size),
172 ):
173 renderer.draw_rect((edge[0], edge[1]), (edge[2], edge[3]), colour=border, filled=True)
174 sid = self._skill_tree.hotbar[i]
175 if sid and sid.startswith("item:"):
176 label = sid[5:].replace("_", " ").title()
177 renderer.draw_text(label[:6], (sx + 4, slot_sy + 22), scale=0.8, colour=(0.5, 0.9, 0.5))
178 elif sid:
179 skill = self._skill_tree.get_skill(sid)
180 if skill:
181 renderer.draw_text(skill["name"][:6], (sx + 4, slot_sy + 22), scale=0.8, colour=(0.8, 0.8, 0.9))
182 else:
183 renderer.draw_text("Empty", (sx + 8, slot_sy + 22), scale=0.7, colour=(0.4, 0.4, 0.4))
184 renderer.draw_text(str(i + 1), (sx + slot_size - 14, slot_sy - 14), scale=0.7, colour=(0.5, 0.5, 0.5))
185
186 renderer.draw_rect((px + 15, py + 125), (panel_w - 30, 1), colour=(0.3, 0.3, 0.35, 0.5), filled=True)
187 list_y = py + 140
188
189 if not self._entries:
190 renderer.draw_text(
191 "No abilities or items available.", (px + 30, list_y + 20), scale=1.0, colour=(0.5, 0.5, 0.5)
192 )
193 else:
194 for j, entry in enumerate(self._entries):
195 iy = list_y + j * 32
196 sel = j == self._ability_cursor
197 if sel:
198 renderer.draw_rect((px + 20, iy - 2), (panel_w - 40, 28), colour=(0.2, 0.2, 0.3, 0.8), filled=True)
199 if entry["_kind"] == "item":
200 colour = (0.5, 1.0, 0.5) if sel else (0.4, 0.7, 0.4)
201 renderer.draw_text(f"{entry['name']} (Item)", (px + 30, iy + 2), scale=1.0, colour=colour)
202 else:
203 colour = (1.0, 0.9, 0.3) if sel else (0.7, 0.7, 0.7)
204 branch = entry.get("branch", "mage")
205 bc = {"warrior": "(W)", "ranger": "(R)", "mage": "(M)"}.get(branch, "")
206 renderer.draw_text(f"{entry['name']} {bc}", (px + 30, iy + 2), scale=1.0, colour=colour)
207 cd = entry.get("cooldown", 0)
208 if cd:
209 renderer.draw_text(f"{cd}s CD", (px + panel_w - 80, iy + 4), scale=0.8, colour=(0.5, 0.5, 0.5))
210
211 renderer.draw_text(
212 "A/D: Slot W/S: Ability Space: Assign E: Clear Esc: Close",
213 (px + 15, py + panel_h - 22),
214 scale=0.75,
215 colour=(0.4, 0.4, 0.4),
216 )
217 draw_back_button(renderer)