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)