mini-arcade-core 0.9.9__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mini_arcade_core/__init__.py +44 -80
- mini_arcade_core/backend/__init__.py +0 -5
- mini_arcade_core/backend/backend.py +9 -0
- mini_arcade_core/backend/events.py +1 -1
- mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
- mini_arcade_core/bus.py +57 -0
- mini_arcade_core/engine/__init__.py +0 -0
- mini_arcade_core/engine/commands.py +169 -0
- mini_arcade_core/engine/game.py +354 -0
- mini_arcade_core/engine/render/__init__.py +0 -0
- mini_arcade_core/engine/render/packet.py +56 -0
- mini_arcade_core/engine/render/pipeline.py +39 -0
- mini_arcade_core/managers/__init__.py +0 -14
- mini_arcade_core/managers/cheats.py +186 -0
- mini_arcade_core/managers/inputs.py +286 -0
- mini_arcade_core/runtime/__init__.py +0 -0
- mini_arcade_core/runtime/audio/__init__.py +0 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +13 -0
- mini_arcade_core/runtime/audio/audio_port.py +17 -0
- mini_arcade_core/runtime/capture/__init__.py +0 -0
- mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
- mini_arcade_core/runtime/capture/capture_port.py +32 -0
- mini_arcade_core/runtime/context.py +53 -0
- mini_arcade_core/runtime/file/__init__.py +0 -0
- mini_arcade_core/runtime/file/file_adapter.py +20 -0
- mini_arcade_core/runtime/file/file_port.py +31 -0
- mini_arcade_core/runtime/input/__init__.py +0 -0
- mini_arcade_core/runtime/input/input_adapter.py +49 -0
- mini_arcade_core/runtime/input/input_port.py +31 -0
- mini_arcade_core/runtime/input_frame.py +71 -0
- mini_arcade_core/runtime/scene/__init__.py +0 -0
- mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
- mini_arcade_core/runtime/scene/scene_port.py +149 -0
- mini_arcade_core/runtime/services.py +35 -0
- mini_arcade_core/runtime/window/__init__.py +0 -0
- mini_arcade_core/runtime/window/window_adapter.py +26 -0
- mini_arcade_core/runtime/window/window_port.py +47 -0
- mini_arcade_core/scenes/__init__.py +0 -12
- mini_arcade_core/scenes/autoreg.py +1 -1
- mini_arcade_core/scenes/registry.py +21 -19
- mini_arcade_core/scenes/sim_scene.py +41 -0
- mini_arcade_core/scenes/systems/__init__.py +0 -0
- mini_arcade_core/scenes/systems/base_system.py +40 -0
- mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
- mini_arcade_core/sim/__init__.py +0 -0
- mini_arcade_core/sim/protocols.py +41 -0
- mini_arcade_core/sim/runner.py +222 -0
- mini_arcade_core/spaces/__init__.py +0 -0
- mini_arcade_core/spaces/d2/__init__.py +0 -0
- mini_arcade_core/{two_d → spaces/d2}/collision2d.py +25 -28
- mini_arcade_core/{two_d → spaces/d2}/geometry2d.py +18 -0
- mini_arcade_core/{two_d → spaces/d2}/kinematics2d.py +5 -8
- mini_arcade_core/{two_d → spaces/d2}/physics2d.py +9 -0
- mini_arcade_core/ui/__init__.py +0 -14
- mini_arcade_core/ui/menu.py +415 -56
- mini_arcade_core/utils/__init__.py +10 -0
- mini_arcade_core/utils/deprecated_decorator.py +45 -0
- mini_arcade_core/utils/logging.py +174 -0
- {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/METADATA +1 -1
- mini_arcade_core-1.0.0.dist-info/RECORD +65 -0
- {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/WHEEL +1 -1
- mini_arcade_core/cheats.py +0 -235
- mini_arcade_core/entity.py +0 -71
- mini_arcade_core/game.py +0 -287
- mini_arcade_core/keymaps/__init__.py +0 -15
- mini_arcade_core/managers/base.py +0 -91
- mini_arcade_core/managers/entity_manager.py +0 -38
- mini_arcade_core/managers/overlay_manager.py +0 -33
- mini_arcade_core/scenes/scene.py +0 -93
- mini_arcade_core/two_d/__init__.py +0 -30
- mini_arcade_core-0.9.9.dist-info/RECORD +0 -31
- /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
- /mini_arcade_core/{two_d → spaces/d2}/boundaries2d.py +0 -0
- {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/licenses/LICENSE +0 -0
mini_arcade_core/ui/menu.py
CHANGED
|
@@ -5,12 +5,19 @@ Menu system for mini arcade core.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Callable, Sequence
|
|
8
|
+
from typing import Callable, Optional, Sequence
|
|
9
9
|
|
|
10
|
-
from mini_arcade_core.backend import Backend
|
|
11
|
-
from mini_arcade_core.
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
from mini_arcade_core.backend import Backend
|
|
11
|
+
from mini_arcade_core.backend.events import Event, EventType
|
|
12
|
+
from mini_arcade_core.backend.keys import Key
|
|
13
|
+
from mini_arcade_core.backend.types import Color
|
|
14
|
+
from mini_arcade_core.engine.commands import Command, CommandQueue, QuitCommand
|
|
15
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
16
|
+
from mini_arcade_core.runtime.context import RuntimeContext
|
|
17
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
18
|
+
from mini_arcade_core.scenes.systems.system_pipeline import SystemPipeline
|
|
19
|
+
from mini_arcade_core.sim.protocols import SimScene
|
|
20
|
+
from mini_arcade_core.spaces.d2.geometry2d import Size2D
|
|
14
21
|
|
|
15
22
|
|
|
16
23
|
@dataclass(frozen=True)
|
|
@@ -19,11 +26,25 @@ class MenuItem:
|
|
|
19
26
|
Represents a single item in a menu.
|
|
20
27
|
|
|
21
28
|
:ivar label (str): The text label of the menu item.
|
|
22
|
-
:ivar on_select (
|
|
29
|
+
:ivar on_select (BaseCommand): The action to perform when the item is selected.
|
|
23
30
|
"""
|
|
24
31
|
|
|
32
|
+
id: str
|
|
25
33
|
label: str
|
|
26
|
-
|
|
34
|
+
command_factory: Callable[[], Command]
|
|
35
|
+
label_fn: Optional[Callable[[object], str]] = None
|
|
36
|
+
|
|
37
|
+
def resolved_label(self, ctx: object) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Get the resolved label for this menu item.
|
|
40
|
+
|
|
41
|
+
:param ctx: The current ctx instance.
|
|
42
|
+
:type ctx: object
|
|
43
|
+
|
|
44
|
+
:return: The resolved label string.
|
|
45
|
+
:rtype: str
|
|
46
|
+
"""
|
|
47
|
+
return self.label_fn(ctx) if self.label_fn else self.label
|
|
27
48
|
|
|
28
49
|
|
|
29
50
|
# Justification: Data container for styling options needs
|
|
@@ -104,12 +125,11 @@ class MenuStyle:
|
|
|
104
125
|
item_font_size = 24
|
|
105
126
|
|
|
106
127
|
|
|
107
|
-
# pylint: enable=too-many-instance-attributes
|
|
108
|
-
|
|
109
|
-
|
|
110
128
|
class Menu:
|
|
111
129
|
"""A simple text-based menu system."""
|
|
112
130
|
|
|
131
|
+
# Justification: Multiple attributes for menu state
|
|
132
|
+
# pylint: disable=too-many-arguments
|
|
113
133
|
def __init__(
|
|
114
134
|
self,
|
|
115
135
|
items: Sequence[MenuItem],
|
|
@@ -117,6 +137,7 @@ class Menu:
|
|
|
117
137
|
viewport: Size2D | None = None,
|
|
118
138
|
title: str | None = None,
|
|
119
139
|
style: MenuStyle | None = None,
|
|
140
|
+
on_select: Optional[Callable[[MenuItem], None]] = None,
|
|
120
141
|
):
|
|
121
142
|
"""
|
|
122
143
|
:param items: Sequence of MenuItem instances to display.
|
|
@@ -136,6 +157,42 @@ class Menu:
|
|
|
136
157
|
self.title = title
|
|
137
158
|
self.style = style or MenuStyle()
|
|
138
159
|
self.selected_index = 0
|
|
160
|
+
self._on_select = on_select
|
|
161
|
+
self._max_content_w_seen = 0
|
|
162
|
+
self._max_button_w_seen = 0
|
|
163
|
+
self.stable_width = True
|
|
164
|
+
|
|
165
|
+
# pylint: enable=too-many-arguments
|
|
166
|
+
|
|
167
|
+
def set_items(self, items: Sequence[MenuItem]):
|
|
168
|
+
"""Set the menu items.
|
|
169
|
+
:param items: Sequence of new MenuItem instances.
|
|
170
|
+
:type items: Sequence[MenuItem]
|
|
171
|
+
"""
|
|
172
|
+
self.items = list(items)
|
|
173
|
+
|
|
174
|
+
def set_selected_index(self, index: int):
|
|
175
|
+
"""Set the selected index of the menu.
|
|
176
|
+
:param index: New selected index.
|
|
177
|
+
:type index: int
|
|
178
|
+
"""
|
|
179
|
+
if 0 <= index < len(self.items):
|
|
180
|
+
self.selected_index = index
|
|
181
|
+
|
|
182
|
+
def set_labels(self, labels: Sequence[str]):
|
|
183
|
+
"""Set the labels of the menu items.
|
|
184
|
+
:param labels: Sequence of new labels for the menu items.
|
|
185
|
+
:type labels: Sequence[str]
|
|
186
|
+
"""
|
|
187
|
+
for index, label in enumerate(labels):
|
|
188
|
+
if index < len(self.items):
|
|
189
|
+
item = self.items[index]
|
|
190
|
+
self.items[index] = MenuItem(
|
|
191
|
+
id=item.id,
|
|
192
|
+
label=label,
|
|
193
|
+
command_factory=item.command_factory,
|
|
194
|
+
label_fn=item.label_fn,
|
|
195
|
+
)
|
|
139
196
|
|
|
140
197
|
def move_up(self):
|
|
141
198
|
"""Move the selection up by one item, wrapping around if necessary."""
|
|
@@ -149,8 +206,11 @@ class Menu:
|
|
|
149
206
|
|
|
150
207
|
def select(self):
|
|
151
208
|
"""Select the currently highlighted item, invoking its action."""
|
|
152
|
-
if self.items:
|
|
153
|
-
|
|
209
|
+
if not self.items:
|
|
210
|
+
return
|
|
211
|
+
item = self.items[self.selected_index]
|
|
212
|
+
if self._on_select is not None:
|
|
213
|
+
self._on_select(item)
|
|
154
214
|
|
|
155
215
|
def handle_event(
|
|
156
216
|
self,
|
|
@@ -159,7 +219,7 @@ class Menu:
|
|
|
159
219
|
up_key: int,
|
|
160
220
|
down_key: int,
|
|
161
221
|
select_key: int,
|
|
162
|
-
):
|
|
222
|
+
) -> bool:
|
|
163
223
|
"""
|
|
164
224
|
Handle an input event to navigate the menu.
|
|
165
225
|
|
|
@@ -176,43 +236,19 @@ class Menu:
|
|
|
176
236
|
:type select_key: int
|
|
177
237
|
"""
|
|
178
238
|
if event.type != EventType.KEYDOWN or event.key is None:
|
|
179
|
-
return
|
|
239
|
+
return False
|
|
240
|
+
|
|
180
241
|
if event.key == up_key:
|
|
181
242
|
self.move_up()
|
|
182
|
-
|
|
243
|
+
return True
|
|
244
|
+
if event.key == down_key:
|
|
183
245
|
self.move_down()
|
|
184
|
-
|
|
246
|
+
return True
|
|
247
|
+
if event.key == select_key:
|
|
185
248
|
self.select()
|
|
249
|
+
return True
|
|
186
250
|
|
|
187
|
-
|
|
188
|
-
# Returns (content_width, content_height, title_height)
|
|
189
|
-
# where content is items-only (no padding)
|
|
190
|
-
if not self.items and not self.title:
|
|
191
|
-
return 0, 0, 0
|
|
192
|
-
|
|
193
|
-
max_w = 0
|
|
194
|
-
title_h = 0
|
|
195
|
-
|
|
196
|
-
if self.title:
|
|
197
|
-
tw, th = surface.measure_text(
|
|
198
|
-
self.title, self.style.title_font_size
|
|
199
|
-
)
|
|
200
|
-
max_w = max(max_w, tw)
|
|
201
|
-
title_h = th
|
|
202
|
-
|
|
203
|
-
# Items
|
|
204
|
-
for it in self.items:
|
|
205
|
-
w, _ = surface.measure_text(it.label, self.style.item_font_size)
|
|
206
|
-
max_w = max(max_w, w)
|
|
207
|
-
|
|
208
|
-
items_h = len(self.items) * self.style.line_height
|
|
209
|
-
|
|
210
|
-
# Total content height includes title block if present
|
|
211
|
-
content_h = items_h
|
|
212
|
-
if self.title:
|
|
213
|
-
content_h += title_h + self.style.title_spacing
|
|
214
|
-
|
|
215
|
-
return max_w, content_h, title_h
|
|
251
|
+
return False
|
|
216
252
|
|
|
217
253
|
def draw(self, surface: Backend):
|
|
218
254
|
"""
|
|
@@ -226,7 +262,7 @@ class Menu:
|
|
|
226
262
|
"Menu requires viewport=Size2D for centering/layout"
|
|
227
263
|
)
|
|
228
264
|
|
|
229
|
-
vw, vh = self.viewport
|
|
265
|
+
vw, vh = self.viewport
|
|
230
266
|
|
|
231
267
|
# 0) Solid background (for main menus)
|
|
232
268
|
if self.style.background_color is not None:
|
|
@@ -315,10 +351,17 @@ class Menu:
|
|
|
315
351
|
else:
|
|
316
352
|
max_label_w = 0
|
|
317
353
|
for it in self.items:
|
|
318
|
-
w, _ = surface.measure_text(
|
|
354
|
+
w, _ = surface.measure_text(
|
|
355
|
+
it.label, font_size=self.style.item_font_size
|
|
356
|
+
)
|
|
319
357
|
max_label_w = max(max_label_w, w)
|
|
320
358
|
bw = max_label_w + self.style.button_padding_x * 2
|
|
321
359
|
|
|
360
|
+
# ✅ Sticky button width (never shrink)
|
|
361
|
+
if self.stable_width:
|
|
362
|
+
self._max_button_w_seen = max(self._max_button_w_seen, bw)
|
|
363
|
+
bw = self._max_button_w_seen
|
|
364
|
+
|
|
322
365
|
bh = self.style.button_height
|
|
323
366
|
gap = self.style.button_gap
|
|
324
367
|
|
|
@@ -341,10 +384,18 @@ class Menu:
|
|
|
341
384
|
|
|
342
385
|
# Label color
|
|
343
386
|
text_color = self.style.selected if selected else self.style.normal
|
|
344
|
-
tw, th = surface.measure_text(
|
|
387
|
+
tw, th = surface.measure_text(
|
|
388
|
+
item.label, font_size=self.style.item_font_size
|
|
389
|
+
)
|
|
345
390
|
tx = x + (bw - tw) // 2
|
|
346
391
|
ty = y + (bh - th) // 2
|
|
347
|
-
surface.draw_text(
|
|
392
|
+
surface.draw_text(
|
|
393
|
+
tx,
|
|
394
|
+
ty,
|
|
395
|
+
item.label,
|
|
396
|
+
color=text_color,
|
|
397
|
+
font_size=self.style.item_font_size,
|
|
398
|
+
)
|
|
348
399
|
|
|
349
400
|
# pylint: enable=too-many-locals
|
|
350
401
|
|
|
@@ -353,25 +404,32 @@ class Menu:
|
|
|
353
404
|
max_w = 0
|
|
354
405
|
title_h = 0
|
|
355
406
|
|
|
407
|
+
# Title
|
|
356
408
|
if self.title:
|
|
357
|
-
tw, th = surface.measure_text(
|
|
409
|
+
tw, th = surface.measure_text(
|
|
410
|
+
self.title, font_size=self.style.title_font_size
|
|
411
|
+
)
|
|
358
412
|
max_w = max(max_w, tw)
|
|
359
413
|
title_h = th
|
|
360
414
|
|
|
361
415
|
if not self.items:
|
|
362
|
-
content_h = 0
|
|
363
|
-
|
|
364
|
-
|
|
416
|
+
content_h = title_h if self.title else 0
|
|
417
|
+
# Apply stable width even for empty items
|
|
418
|
+
if self.stable_width:
|
|
419
|
+
self._max_content_w_seen = max(self._max_content_w_seen, max_w)
|
|
420
|
+
max_w = self._max_content_w_seen
|
|
365
421
|
return max_w, content_h, title_h
|
|
366
422
|
|
|
367
423
|
if self.style.button_enabled:
|
|
368
|
-
#
|
|
424
|
+
# Width: fixed or auto-fit by longest label
|
|
369
425
|
if self.style.button_width is not None:
|
|
370
426
|
items_w = self.style.button_width
|
|
371
427
|
else:
|
|
372
428
|
max_label_w = 0
|
|
373
429
|
for it in self.items:
|
|
374
|
-
w, _ = surface.measure_text(
|
|
430
|
+
w, _ = surface.measure_text(
|
|
431
|
+
it.label, font_size=self.style.item_font_size
|
|
432
|
+
)
|
|
375
433
|
max_label_w = max(max_label_w, w)
|
|
376
434
|
items_w = max_label_w + self.style.button_padding_x * 2
|
|
377
435
|
|
|
@@ -382,7 +440,9 @@ class Menu:
|
|
|
382
440
|
items_h = len(self.items) * bh + (len(self.items) - 1) * gap
|
|
383
441
|
else:
|
|
384
442
|
for it in self.items:
|
|
385
|
-
w, _ = surface.measure_text(
|
|
443
|
+
w, _ = surface.measure_text(
|
|
444
|
+
it.label, font_size=self.style.item_font_size
|
|
445
|
+
)
|
|
386
446
|
max_w = max(max_w, w)
|
|
387
447
|
items_h = len(self.items) * self.style.line_height
|
|
388
448
|
|
|
@@ -394,6 +454,11 @@ class Menu:
|
|
|
394
454
|
+ self.style.title_margin_bottom
|
|
395
455
|
)
|
|
396
456
|
|
|
457
|
+
# ✅ Sticky width (never shrink)
|
|
458
|
+
if self.stable_width:
|
|
459
|
+
self._max_content_w_seen = max(self._max_content_w_seen, max_w)
|
|
460
|
+
max_w = self._max_content_w_seen
|
|
461
|
+
|
|
397
462
|
return max_w, content_h, title_h
|
|
398
463
|
|
|
399
464
|
# Justification: Many arguments for text drawing utility
|
|
@@ -414,3 +479,297 @@ class Menu:
|
|
|
414
479
|
)
|
|
415
480
|
|
|
416
481
|
# pylint: enable=too-many-arguments
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# pylint: enable=too-many-instance-attributes
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@dataclass
|
|
488
|
+
class MenuModel:
|
|
489
|
+
"""
|
|
490
|
+
Data model for menu scenes.
|
|
491
|
+
|
|
492
|
+
:ivar selected (int): Currently selected menu item index.
|
|
493
|
+
:ivar move_cooldown (float): Cooldown time between menu moves.
|
|
494
|
+
:ivar _cooldown_timer (float): Internal timer for move cooldown.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
selected: int = 0
|
|
498
|
+
move_cooldown: float = 0.12
|
|
499
|
+
_cooldown_timer: float = 0.0
|
|
500
|
+
|
|
501
|
+
def step_timer(self, dt: float):
|
|
502
|
+
"""
|
|
503
|
+
Step the internal cooldown timer.
|
|
504
|
+
|
|
505
|
+
:param dt: Delta time since last update.
|
|
506
|
+
:type dt: float
|
|
507
|
+
"""
|
|
508
|
+
if self._cooldown_timer > 0:
|
|
509
|
+
self._cooldown_timer = max(0.0, self._cooldown_timer - dt)
|
|
510
|
+
|
|
511
|
+
def can_move(self) -> bool:
|
|
512
|
+
"""
|
|
513
|
+
Check if the menu can move selection (cooldown elapsed).
|
|
514
|
+
|
|
515
|
+
:return: True if movement is allowed, False otherwise.
|
|
516
|
+
:rtype: bool
|
|
517
|
+
"""
|
|
518
|
+
return self._cooldown_timer <= 0.0
|
|
519
|
+
|
|
520
|
+
def consume_move(self):
|
|
521
|
+
"""Consume a move action and reset the cooldown timer."""
|
|
522
|
+
self._cooldown_timer = self.move_cooldown
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# TODO: Solve too-many-instance-attributes warning later
|
|
526
|
+
# Justification: Context for menu tick needs multiple attributes.
|
|
527
|
+
# pylint: disable=too-many-instance-attributes
|
|
528
|
+
@dataclass
|
|
529
|
+
class MenuTickContext:
|
|
530
|
+
"""
|
|
531
|
+
Context for a single tick of the menu scene.
|
|
532
|
+
|
|
533
|
+
:ivar input_frame (InputFrame): The current input frame.
|
|
534
|
+
:ivar dt (float): Delta time since last tick.
|
|
535
|
+
:ivar menu (Menu): The Menu instance.
|
|
536
|
+
:ivar model (MenuModel): The MenuModel instance.
|
|
537
|
+
:ivar commands (CommandQueue): The command queue for pushing commands.
|
|
538
|
+
:ivar intent (MenuIntent | None): The current menu intent.
|
|
539
|
+
:ivar quit_cmd_factory (callable | None): Factory for quit command.
|
|
540
|
+
:ivar packet (RenderPacket | None): The resulting render packet.
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
input_frame: InputFrame
|
|
544
|
+
dt: float
|
|
545
|
+
|
|
546
|
+
menu: "Menu"
|
|
547
|
+
model: "MenuModel"
|
|
548
|
+
commands: CommandQueue
|
|
549
|
+
|
|
550
|
+
# computed data that systems can write into
|
|
551
|
+
intent: "MenuIntent | None" = None
|
|
552
|
+
|
|
553
|
+
# scene hook: a callable to produce quit cmd (per scene override)
|
|
554
|
+
quit_cmd_factory: callable | None = None
|
|
555
|
+
|
|
556
|
+
# final output
|
|
557
|
+
packet: RenderPacket | None = None
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# pylint: enable=too-many-instance-attributes
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@dataclass(frozen=True)
|
|
564
|
+
class MenuIntent:
|
|
565
|
+
"""
|
|
566
|
+
Represents the user's intent in the menu for the current tick.
|
|
567
|
+
|
|
568
|
+
:ivar move_up (bool): Whether the user intends to move up.
|
|
569
|
+
:ivar move_down (bool): Whether the user intends to move down.
|
|
570
|
+
:ivar select (bool): Whether the user intends to select the current item.
|
|
571
|
+
:ivar quit (bool): Whether the user intends to quit the menu.
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
move_up: bool = False
|
|
575
|
+
move_down: bool = False
|
|
576
|
+
select: bool = False
|
|
577
|
+
quit: bool = False
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@dataclass
|
|
581
|
+
class MenuInputSystem:
|
|
582
|
+
"""Converts InputFrame -> MenuIntent."""
|
|
583
|
+
|
|
584
|
+
name: str = "menu_input"
|
|
585
|
+
order: int = 10
|
|
586
|
+
|
|
587
|
+
def step(self, ctx: MenuTickContext):
|
|
588
|
+
"""Step the input system to extract menu intent."""
|
|
589
|
+
pressed = ctx.input_frame.keys_pressed
|
|
590
|
+
ctx.intent = MenuIntent(
|
|
591
|
+
move_up=Key.UP in pressed,
|
|
592
|
+
move_down=Key.DOWN in pressed,
|
|
593
|
+
select=(Key.ENTER in pressed) or (Key.SPACE in pressed),
|
|
594
|
+
quit=Key.ESCAPE in pressed,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@dataclass
|
|
599
|
+
class MenuNavigationSystem:
|
|
600
|
+
"""Menu navigation system."""
|
|
601
|
+
|
|
602
|
+
name: str = "menu_nav"
|
|
603
|
+
order: int = 20
|
|
604
|
+
|
|
605
|
+
def step(self, ctx: MenuTickContext):
|
|
606
|
+
"""Update menu selection based on intent."""
|
|
607
|
+
intent = ctx.intent
|
|
608
|
+
if intent is None:
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
ctx.model.step_timer(ctx.dt)
|
|
612
|
+
|
|
613
|
+
if not ctx.model.can_move():
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
if intent.move_up:
|
|
617
|
+
ctx.menu.move_up()
|
|
618
|
+
ctx.model.selected = ctx.menu.selected_index
|
|
619
|
+
ctx.model.consume_move()
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
if intent.move_down:
|
|
623
|
+
ctx.menu.move_down()
|
|
624
|
+
ctx.model.selected = ctx.menu.selected_index
|
|
625
|
+
ctx.model.consume_move()
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@dataclass
|
|
629
|
+
class MenuActionSystem:
|
|
630
|
+
"""Menu action execution system."""
|
|
631
|
+
|
|
632
|
+
name: str = "menu_actions"
|
|
633
|
+
order: int = 30
|
|
634
|
+
|
|
635
|
+
def step(self, ctx: MenuTickContext):
|
|
636
|
+
"""Execute actions based on menu intent."""
|
|
637
|
+
intent = ctx.intent
|
|
638
|
+
if intent is None:
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
if intent.select and ctx.menu.items:
|
|
642
|
+
item = ctx.menu.items[ctx.menu.selected_index]
|
|
643
|
+
ctx.commands.push(item.command_factory())
|
|
644
|
+
|
|
645
|
+
if intent.quit and ctx.quit_cmd_factory is not None:
|
|
646
|
+
cmd = ctx.quit_cmd_factory()
|
|
647
|
+
if cmd is not None:
|
|
648
|
+
ctx.commands.push(cmd)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@dataclass
|
|
652
|
+
class MenuRenderSystem:
|
|
653
|
+
"""Menu rendering system."""
|
|
654
|
+
|
|
655
|
+
name: str = "menu_render"
|
|
656
|
+
order: int = 100
|
|
657
|
+
|
|
658
|
+
def step(self, ctx: MenuTickContext):
|
|
659
|
+
"""Set the render packet to draw the menu."""
|
|
660
|
+
ctx.packet = RenderPacket.from_ops([ctx.menu.draw])
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class BaseMenuScene(SimScene):
|
|
664
|
+
"""
|
|
665
|
+
Base scene class for menu-based scenes.
|
|
666
|
+
|
|
667
|
+
:ivar model (MenuModel): The data model for the menu scene.
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
menu: Menu
|
|
671
|
+
systems: SystemPipeline[MenuTickContext]
|
|
672
|
+
|
|
673
|
+
def __init__(self, ctx: RuntimeContext):
|
|
674
|
+
super().__init__(ctx)
|
|
675
|
+
self.model = MenuModel()
|
|
676
|
+
|
|
677
|
+
# ---- hooks (same spirit as before) ----
|
|
678
|
+
|
|
679
|
+
@property
|
|
680
|
+
def menu_title(self) -> str | None:
|
|
681
|
+
"""
|
|
682
|
+
Get the title of the menu.
|
|
683
|
+
|
|
684
|
+
:return: The menu title string, or None for no title.
|
|
685
|
+
:rtype: str | None
|
|
686
|
+
"""
|
|
687
|
+
return None
|
|
688
|
+
|
|
689
|
+
def menu_style(self) -> MenuStyle:
|
|
690
|
+
"""
|
|
691
|
+
Get the style configuration for the menu.
|
|
692
|
+
|
|
693
|
+
:return: The MenuStyle instance for styling the menu.
|
|
694
|
+
:rtype: MenuStyle
|
|
695
|
+
"""
|
|
696
|
+
return MenuStyle()
|
|
697
|
+
|
|
698
|
+
def menu_items(self) -> list[MenuItem]:
|
|
699
|
+
"""
|
|
700
|
+
Get the list of menu items for the menu.
|
|
701
|
+
|
|
702
|
+
:return: List of MenuItem instances for the menu.
|
|
703
|
+
:rtype: list[MenuItem]
|
|
704
|
+
"""
|
|
705
|
+
raise NotImplementedError
|
|
706
|
+
|
|
707
|
+
def quit_command(self):
|
|
708
|
+
"""
|
|
709
|
+
Get the command to execute when quitting the menu.
|
|
710
|
+
|
|
711
|
+
:return: The command to execute on quit.
|
|
712
|
+
:rtype: Command
|
|
713
|
+
"""
|
|
714
|
+
# default behavior: quit game
|
|
715
|
+
return QuitCommand()
|
|
716
|
+
|
|
717
|
+
def on_enter(self):
|
|
718
|
+
self.menu = Menu(
|
|
719
|
+
self._build_display_items(),
|
|
720
|
+
viewport=self.context.services.window.size,
|
|
721
|
+
title=self.menu_title,
|
|
722
|
+
style=self.menu_style(),
|
|
723
|
+
)
|
|
724
|
+
self.menu.selected_index = self.model.selected
|
|
725
|
+
self.systems = SystemPipeline()
|
|
726
|
+
self.systems.extend(
|
|
727
|
+
[
|
|
728
|
+
MenuInputSystem(),
|
|
729
|
+
MenuNavigationSystem(),
|
|
730
|
+
MenuActionSystem(),
|
|
731
|
+
MenuRenderSystem(),
|
|
732
|
+
]
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
|
|
736
|
+
items = self.menu_items()
|
|
737
|
+
if not items:
|
|
738
|
+
return RenderPacket()
|
|
739
|
+
|
|
740
|
+
self.model.step_timer(dt)
|
|
741
|
+
|
|
742
|
+
self.menu.set_items(self._build_display_items())
|
|
743
|
+
self.menu.set_selected_index(self.model.selected)
|
|
744
|
+
|
|
745
|
+
ctx = MenuTickContext(
|
|
746
|
+
input_frame=input_frame,
|
|
747
|
+
dt=dt,
|
|
748
|
+
menu=self.menu,
|
|
749
|
+
model=self.model,
|
|
750
|
+
commands=self.context.command_queue,
|
|
751
|
+
quit_cmd_factory=self.quit_command,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
self.systems.step(ctx)
|
|
755
|
+
|
|
756
|
+
# always return packet from pipeline
|
|
757
|
+
return ctx.packet or RenderPacket()
|
|
758
|
+
|
|
759
|
+
def _build_display_items(self) -> list[MenuItem]:
|
|
760
|
+
"""
|
|
761
|
+
Resolve dynamic labels (label_fn(ctx)) into the label field the Menu draws.
|
|
762
|
+
Keeps command_factory intact.
|
|
763
|
+
"""
|
|
764
|
+
src = self.menu_items()
|
|
765
|
+
out: list[MenuItem] = []
|
|
766
|
+
for it in src:
|
|
767
|
+
out.append(
|
|
768
|
+
MenuItem(
|
|
769
|
+
id=it.id,
|
|
770
|
+
label=it.resolved_label(self.context),
|
|
771
|
+
command_factory=it.command_factory,
|
|
772
|
+
label_fn=it.label_fn,
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
return out
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deprecated utilities for the mini-arcade-core package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.utils.logging import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def deprecated(
|
|
13
|
+
reason: str | None = None,
|
|
14
|
+
version: str | None = None,
|
|
15
|
+
alternative: str | None = None,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Mark a function as deprecated.
|
|
19
|
+
|
|
20
|
+
:param reason: Optional reason for deprecation
|
|
21
|
+
:type reason: str | None
|
|
22
|
+
|
|
23
|
+
:param version: Optional version when it will be removed
|
|
24
|
+
:type version: str | None
|
|
25
|
+
|
|
26
|
+
:param alternative: Optional alternative function to use
|
|
27
|
+
:type alternative: str | None
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def decorator(func):
|
|
31
|
+
@functools.wraps(func)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
message = f"The function {func.__name__} is deprecated"
|
|
34
|
+
if version:
|
|
35
|
+
message += f" and will be removed in version {version}"
|
|
36
|
+
if reason:
|
|
37
|
+
message += f". {reason}"
|
|
38
|
+
if alternative:
|
|
39
|
+
message += f" Use {alternative} instead."
|
|
40
|
+
logger.warning(message)
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
return wrapper
|
|
44
|
+
|
|
45
|
+
return decorator
|