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)