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