nodes/level_up_ui.pyΒΆ

Part of Dungeon Explorer.

  1"""Stat point allocation screen shown on level up."""
  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, check_menu_click, draw_back_button
  7
  8
  9class LevelUpUI(Control):
 10    """Stat point allocation overlay."""
 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    STAT_NAMES = ["Strength", "Dexterity", "Vitality", "Intelligence"]
 20    STAT_KEYS = ["strength", "dexterity", "vitality", "intelligence"]
 21
 22    def __init__(self, **kwargs):
 23        super().__init__(name="LevelUpUI", anchor=Anchor.FULL_RECT, **kwargs)
 24
 25        self._selected = 0
 26        self._player = None
 27
 28    def set_player(self, player) -> None:
 29        self._player = player
 30
 31    def show(self):
 32        self._selected = 0
 33        self.show_overlay("blocking", dismiss=True)
 34
 35    # -- Input --
 36
 37    def on_gui_input(self, event):
 38        if not self.visible:
 39            return
 40        if isinstance(event, PointerEvent):
 41            if event.button in Pointer.PRIMARY and event.phase is PointerPhase.DOWN:
 42                return self._handle_click(*event.position)
 43            return None
 44        if isinstance(event, KeyEvent) and event.pressed:
 45            if event.key in (Key.W, Key.UP):
 46                self._selected = (self._selected - 1) % len(self.STAT_NAMES)
 47                return True
 48            elif event.key in (Key.S, Key.DOWN):
 49                self._selected = (self._selected + 1) % len(self.STAT_NAMES)
 50                return True
 51            elif event.key in (Key.SPACE, Key.ENTER, Key.E):
 52                self._allocate()
 53                if self._player is None or self._player.stat_points <= 0:
 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, _ = self._screen_size()
 64        cx = sw / 2
 65        w = 300
 66        x = cx - w / 2
 67        sy = 280
 68        idx = check_menu_click(mx, my, len(self.STAT_NAMES), x + 20, sy, w - 40, 24, 30)
 69        if idx is not None:
 70            self._selected = idx
 71            self._allocate()
 72            if self._player is None or self._player.stat_points <= 0:
 73                self.cancel_requested.emit()
 74                self.close_overlay()
 75            return True
 76
 77    def _allocate(self):
 78        if self._player is None or self._player.stat_points <= 0:
 79            return
 80        key = self.STAT_KEYS[self._selected]
 81        current = getattr(self._player, key, 0)
 82        setattr(self._player, key, current + 1)
 83        self._player.stat_points -= 1
 84
 85    # -- Rendering --
 86
 87    def _screen_size(self) -> tuple[float, float]:
 88        if self.tree is not None:
 89            sw, sh = self.tree.screen_size
 90            return float(sw), float(sh)
 91        return 1280.0, 720.0
 92
 93    def on_draw(self, renderer):
 94        if not self.visible or self._player is None:
 95            return
 96        p = self._player
 97        sw, sh = self._screen_size()
 98        cx = sw / 2
 99        w, h = 300, 260
100        x, y = cx - w / 2, 200
101
102        renderer.rect((0, 0), (sw, sh), fill=(0.0, 0.0, 0.0, 0.5))
103        renderer.rect((x, y), (w, h), fill=(0.1, 0.1, 0.15, 0.95))
104        renderer.rect((x, y), (w, 2), fill=(1.0, 0.9, 0.3, 1.0))
105        renderer.text("LEVEL UP!", (cx - 40, y + 15), fill=(1.0, 0.9, 0.3, 1.0), scale=1.8)
106        renderer.text(f"Points: {p.stat_points}", (cx - 30, y + 45), fill=(0.8, 0.8, 0.8), scale=1.0)
107
108        sy = y + 80
109        for i, (name, key) in enumerate(zip(self.STAT_NAMES, self.STAT_KEYS, strict=False)):
110            val = getattr(p, key, 0)
111            colour = (1.0, 0.9, 0.3, 1.0) if i == self._selected else (0.7, 0.7, 0.7, 1.0)
112            prefix = "> " if i == self._selected else "  "
113            renderer.text(f"{prefix}{name}: {val}", (x + 20, sy + i * 30), fill=colour, scale=1.2)
114
115        renderer.text("Space: Allocate   Esc: Close", (x + 20, y + h - 25), fill=(0.5, 0.5, 0.5, 1.0), scale=0.8)
116        draw_back_button(renderer)