mini-arcade-core 1.1.0__py3-none-any.whl → 1.2.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.
Files changed (58) hide show
  1. mini_arcade_core/__init__.py +14 -42
  2. mini_arcade_core/backend/__init__.py +1 -2
  3. mini_arcade_core/backend/backend.py +185 -154
  4. mini_arcade_core/backend/types.py +5 -1
  5. mini_arcade_core/engine/commands.py +26 -7
  6. mini_arcade_core/engine/game.py +79 -319
  7. mini_arcade_core/engine/game_config.py +40 -0
  8. mini_arcade_core/engine/gameplay_settings.py +24 -0
  9. mini_arcade_core/engine/loop/config.py +20 -0
  10. mini_arcade_core/engine/loop/hooks.py +77 -0
  11. mini_arcade_core/engine/loop/runner.py +272 -0
  12. mini_arcade_core/engine/loop/state.py +32 -0
  13. mini_arcade_core/engine/managers.py +24 -0
  14. mini_arcade_core/engine/render/context.py +0 -2
  15. mini_arcade_core/engine/render/effects/base.py +88 -0
  16. mini_arcade_core/engine/render/effects/crt.py +68 -0
  17. mini_arcade_core/engine/render/effects/registry.py +50 -0
  18. mini_arcade_core/engine/render/effects/vignette.py +79 -0
  19. mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
  20. mini_arcade_core/engine/render/passes/end_frame.py +1 -1
  21. mini_arcade_core/engine/render/passes/postfx.py +25 -4
  22. mini_arcade_core/engine/render/passes/ui.py +1 -1
  23. mini_arcade_core/engine/render/passes/world.py +6 -6
  24. mini_arcade_core/engine/render/pipeline.py +7 -6
  25. mini_arcade_core/engine/render/viewport.py +10 -4
  26. mini_arcade_core/engine/scenes/__init__.py +0 -0
  27. mini_arcade_core/engine/scenes/models.py +54 -0
  28. mini_arcade_core/engine/scenes/scene_manager.py +213 -0
  29. mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
  30. mini_arcade_core/runtime/audio/audio_port.py +0 -4
  31. mini_arcade_core/runtime/capture/capture_adapter.py +13 -6
  32. mini_arcade_core/runtime/capture/capture_port.py +0 -4
  33. mini_arcade_core/runtime/context.py +8 -6
  34. mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
  35. mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
  36. mini_arcade_core/runtime/services.py +3 -2
  37. mini_arcade_core/runtime/window/window_adapter.py +43 -41
  38. mini_arcade_core/runtime/window/window_port.py +3 -17
  39. mini_arcade_core/scenes/debug_overlay.py +5 -4
  40. mini_arcade_core/scenes/registry.py +11 -1
  41. mini_arcade_core/scenes/sim_scene.py +14 -14
  42. mini_arcade_core/ui/menu.py +54 -16
  43. mini_arcade_core/utils/__init__.py +2 -1
  44. mini_arcade_core/utils/logging.py +47 -18
  45. mini_arcade_core/utils/profiler.py +283 -0
  46. {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/METADATA +1 -1
  47. mini_arcade_core-1.2.0.dist-info/RECORD +92 -0
  48. {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/WHEEL +1 -1
  49. mini_arcade_core/managers/inputs.py +0 -284
  50. mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
  51. mini_arcade_core/runtime/scene/scene_port.py +0 -170
  52. mini_arcade_core/sim/protocols.py +0 -41
  53. mini_arcade_core/sim/runner.py +0 -222
  54. mini_arcade_core-1.1.0.dist-info/RECORD +0 -80
  55. /mini_arcade_core/{managers → engine}/cheats.py +0 -0
  56. /mini_arcade_core/{managers → engine/loop}/__init__.py +0 -0
  57. /mini_arcade_core/{sim → engine/render/effects}/__init__.py +0 -0
  58. {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,207 +4,91 @@ Game core module defining the Game class and configuration.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from dataclasses import dataclass, field
8
- from time import perf_counter, sleep
9
- from typing import Dict, Literal
10
-
11
- from mini_arcade_core.backend import Backend, WindowSettings
12
- from mini_arcade_core.backend.events import EventType
13
- from mini_arcade_core.backend.keys import Key
14
- from mini_arcade_core.engine.commands import (
15
- CommandContext,
16
- CommandQueue,
17
- QuitCommand,
18
- ToggleDebugOverlayCommand,
7
+ from mini_arcade_core.backend import Backend
8
+ from mini_arcade_core.engine.cheats import CheatManager
9
+ from mini_arcade_core.engine.commands import CommandQueue
10
+ from mini_arcade_core.engine.game_config import GameConfig
11
+ from mini_arcade_core.engine.gameplay_settings import GamePlaySettings
12
+ from mini_arcade_core.engine.loop.config import RunnerConfig
13
+ from mini_arcade_core.engine.loop.hooks import DefaultGameHooks
14
+ from mini_arcade_core.engine.loop.runner import EngineRunner
15
+ from mini_arcade_core.engine.managers import EngineManagers
16
+ from mini_arcade_core.engine.render.effects.base import (
17
+ EffectParams,
18
+ EffectStack,
19
19
  )
20
- from mini_arcade_core.engine.render.context import RenderContext
21
- from mini_arcade_core.engine.render.frame_packet import FramePacket
22
- from mini_arcade_core.engine.render.packet import RenderPacket
20
+ from mini_arcade_core.engine.render.effects.crt import CRTEffect
21
+ from mini_arcade_core.engine.render.effects.registry import EffectRegistry
22
+ from mini_arcade_core.engine.render.effects.vignette import VignetteNoiseEffect
23
23
  from mini_arcade_core.engine.render.pipeline import RenderPipeline
24
24
  from mini_arcade_core.engine.render.render_service import RenderService
25
- from mini_arcade_core.managers.cheats import CheatManager
25
+ from mini_arcade_core.engine.scenes.scene_manager import SceneAdapter
26
26
  from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
27
27
  from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
28
28
  from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
29
29
  from mini_arcade_core.runtime.input.input_adapter import InputAdapter
30
- from mini_arcade_core.runtime.input_frame import InputFrame
31
- from mini_arcade_core.runtime.scene.scene_adapter import SceneAdapter
30
+ from mini_arcade_core.runtime.scene.scene_query_adapter import (
31
+ SceneQueryAdapter,
32
+ )
32
33
  from mini_arcade_core.runtime.services import RuntimeServices
33
34
  from mini_arcade_core.runtime.window.window_adapter import WindowAdapter
34
35
  from mini_arcade_core.scenes.registry import SceneRegistry
35
- from mini_arcade_core.utils import logger
36
-
37
-
38
- @dataclass
39
- class WindowConfig:
40
- """
41
- Configuration for a game window (not implemented).
42
-
43
- :ivar width (int): Width of the window in pixels.
44
- :ivar height (int): Height of the window in pixels.
45
- :ivar background_color (tuple[int, int, int]): RGB background color.
46
- :ivar title (str): Title of the window.
47
- """
48
-
49
- width: int
50
- height: int
51
- background_color: tuple[int, int, int]
52
- title: str
53
-
54
-
55
- @dataclass
56
- class GameConfig:
57
- """
58
- Configuration options for the Game.
59
-
60
- :ivar window (WindowConfig | None): Optional window configuration.
61
- :ivar fps (int): Target frames per second.
62
- :ivar backend (Backend | None): Optional Backend instance to use for rendering and input.
63
- """
64
-
65
- window: WindowConfig | None = None
66
- fps: int = 60
67
- backend: Backend | None = None
68
-
69
-
70
- Difficulty = Literal["easy", "normal", "hard", "insane"]
71
-
72
-
73
- @dataclass
74
- class GameSettings:
75
- """
76
- Game settings that can be modified during gameplay.
77
-
78
- :ivar difficulty (Difficulty): Current game difficulty level.
79
- """
80
-
81
- difficulty: Difficulty = "normal"
82
-
83
-
84
- def _neutral_input(frame_index: int, dt: float) -> InputFrame:
85
- """Create a neutral InputFrame with no input events."""
86
- return InputFrame(frame_index=frame_index, dt=dt)
87
-
88
-
89
- @dataclass
90
- class FrameTimer:
91
- """
92
- Simple frame timer for marking and reporting time intervals.
93
-
94
- :ivar enabled (bool): Whether timing is enabled.
95
- :ivar marks (Dict[str, float]): Recorded time marks.
96
- """
36
+ from mini_arcade_core.utils import FrameTimer
37
+ from mini_arcade_core.utils.profiler import FrameTimerConfig
97
38
 
98
- enabled: bool = False
99
- marks: Dict[str, float] = field(default_factory=dict)
100
39
 
101
- def mark(self, name: str):
102
- """
103
- Record a time mark with the given name.
104
-
105
- :param name: Name of the mark.
106
- :type name: str
107
- """
108
- if not self.enabled:
109
- return
110
- self.marks[name] = perf_counter()
111
-
112
- def diff_ms(self, start: str, end: str) -> float:
113
- """
114
- Get the time difference in milliseconds between two marks.
115
-
116
- :param start: Name of the start mark.
117
- :type start: str
118
-
119
- :param end: Name of the end mark.
120
- :type end: str
121
-
122
- :return: Time difference in milliseconds.
123
- :rtype: float
124
- """
125
- return (self.marks[end] - self.marks[start]) * 1000.0
126
-
127
- def report_ms(self) -> Dict[str, float]:
128
- """
129
- Returns diffs between consecutive marks in insertion order.
130
-
131
- :return: Dictionary mapping "start->end" to time difference in milliseconds.
132
- :rtype: Dict[str, float]
133
- """
134
- if not self.enabled:
135
- return {}
136
-
137
- keys = list(self.marks.keys())
138
- out: Dict[str, float] = {}
139
- for a, b in zip(keys, keys[1:]):
140
- out[f"{a}->{b}"] = self.diff_ms(a, b)
141
- return out
142
-
143
- def clear(self):
144
- """Clear all recorded marks."""
145
- if not self.enabled:
146
- return
147
- self.marks.clear()
148
-
149
-
150
- # TODO: Fix too-many-instance-attributes warning
151
- # Justification: Core game class with many dependencies.
152
- # pylint: disable=too-many-instance-attributes
153
40
  class Game:
154
41
  """Core game object responsible for managing the main loop and active scene."""
155
42
 
156
43
  def __init__(
157
- self, config: GameConfig, registry: SceneRegistry | None = None
44
+ self, config: GameConfig, scene_registry: SceneRegistry | None = None
158
45
  ):
159
46
  """
160
47
  :param config: Game configuration options.
161
48
  :type config: GameConfig
162
49
 
163
- :param registry: Optional SceneRegistry for scene management.
164
- :type registry: SceneRegistry | None
50
+ :param scene_registry: Optional SceneRegistry for scene management.
51
+ :type scene_registry: SceneRegistry | None
165
52
 
166
53
  :raises ValueError: If the provided config does not have a valid Backend.
167
54
  """
168
55
  self.config = config
169
56
  self._running: bool = False
170
57
 
171
- if config.backend is None:
58
+ if self.config.backend is None:
172
59
  raise ValueError(
173
60
  "GameConfig.backend must be set to a Backend instance"
174
61
  )
175
- if config.window is None:
176
- raise ValueError("GameConfig.window must be set")
177
62
 
178
- self.backend: Backend = config.backend
179
- self.registry = registry or SceneRegistry(_factories={})
180
- self.settings = GameSettings()
181
- self.services = RuntimeServices(
182
- window=WindowAdapter(
183
- self.backend,
184
- WindowSettings(
185
- width=self.config.window.width,
186
- height=self.config.window.height,
187
- ),
63
+ self.backend: Backend = self.config.backend
64
+ self.settings = GamePlaySettings()
65
+ self.managers = EngineManagers(
66
+ cheats=CheatManager(),
67
+ command_queue=CommandQueue(),
68
+ scenes=SceneAdapter(
69
+ scene_registry or SceneRegistry(_factories={}), self
188
70
  ),
189
- scenes=SceneAdapter(self.registry, self),
71
+ )
72
+ self.services = RuntimeServices(
73
+ window=WindowAdapter(self.backend), # Turn into a manager?
190
74
  audio=SDLAudioAdapter(self.backend),
191
75
  files=LocalFilesAdapter(),
192
76
  capture=CaptureAdapter(self.backend),
193
77
  input=InputAdapter(),
194
78
  render=RenderService(),
79
+ scenes=SceneQueryAdapter(self.managers.scenes),
195
80
  )
196
81
 
197
- self.command_queue = CommandQueue()
198
- self.cheat_manager = CheatManager()
82
+ @property
83
+ def running(self) -> bool:
84
+ """Check if the game is currently running."""
85
+ return self._running
199
86
 
200
87
  def quit(self):
201
88
  """Request that the main loop stops."""
202
89
  self._running = False
203
90
 
204
- # TODO: Fix too-many-statements and too-many-locals warnings
205
- # Justification: Main game loop with multiple responsibilities.
206
- # pylint: disable=too-many-statements,too-many-locals
207
- def run(self, initial_scene_id: str):
91
+ def run(self):
208
92
  """
209
93
  Run the main loop starting with the given scene.
210
94
 
@@ -214,181 +98,57 @@ class Game:
214
98
  :param initial_scene_id: The scene id to start the game with (must be registered).
215
99
  :type initial_scene_id: str
216
100
  """
217
- backend = self.backend
218
-
219
- self._initialize_window()
220
-
221
- self.services.scenes.change(initial_scene_id)
101
+ self.managers.scenes.change(self.config.initial_scene)
222
102
 
223
103
  pipeline = RenderPipeline()
104
+ effects_registry = EffectRegistry()
105
+ effects_registry.register(CRTEffect())
106
+ effects_registry.register(VignetteNoiseEffect())
107
+
108
+ effects_stack = EffectStack(
109
+ enabled=self.config.postfx.enabled,
110
+ active=list(self.config.postfx.active),
111
+ params={
112
+ "crt": EffectParams(intensity=0.35, wobble_speed=1.0),
113
+ "vignette_noise": EffectParams(
114
+ intensity=0.25, wobble_speed=1.0
115
+ ),
116
+ },
117
+ )
118
+ self.settings.effects_stack = effects_stack
224
119
 
225
- self._running = True
226
- target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
227
- last_time = perf_counter()
228
- frame_index = 0
229
-
230
- # cache packets so blocked-update scenes still render their last frame
231
- packet_cache: dict[int, RenderPacket] = {}
232
-
233
- timer = FrameTimer(enabled=True)
234
- # report_every = 60 # print once per second at 60fps
235
-
236
- # TODO: Integrate SimRunner for simulation stepping
237
- # TODO: Fix assignment-from-no-return warning in self.services.input.build
238
- # & self.services.scenes.input_entry
239
- # Justification: These methods are expected to return values.
240
- # pylint: disable=assignment-from-no-return
241
-
242
- while self._running:
243
- timer.clear()
244
- timer.mark("frame_start")
245
-
246
- now = perf_counter()
247
- dt = now - last_time
248
- last_time = now
249
-
250
- events = list(backend.poll_events())
251
-
252
- for e in events:
253
- if e.type == EventType.WINDOWRESIZED and e.size:
254
- w, h = e.size
255
- logger.debug(f"Window resized event: {w}x{h}")
256
- self.services.window.on_window_resized(w, h)
257
- # if F1 pressed, toggle debug overlay
258
- if e.type == EventType.KEYDOWN and e.key == Key.F1:
259
- self.command_queue.push(ToggleDebugOverlayCommand())
260
- timer.mark("events_polled")
261
-
262
- input_frame = self.services.input.build(events, frame_index, dt)
263
- timer.mark("input_built")
264
-
265
- # Window/OS quit (close button)
266
- if input_frame.quit:
267
- self.command_queue.push(QuitCommand())
268
-
269
- # who gets input?
270
- input_entry = self.services.scenes.input_entry()
271
- if input_entry is None:
272
- break
273
-
274
- # tick policy-aware scenes
275
- timer.mark("tick_start")
276
- for entry in self.services.scenes.update_entries():
277
- scene = entry.scene
278
- effective_input = (
279
- input_frame
280
- if entry is input_entry
281
- else _neutral_input(frame_index, dt)
282
- )
283
-
284
- packet = scene.tick(effective_input, dt)
285
- packet_cache[id(scene)] = packet
286
- timer.mark("tick_end")
287
-
288
- timer.mark("command_ctx_start")
289
- command_context = CommandContext(
290
- services=self.services,
291
- commands=self.command_queue,
292
- settings=self.settings,
293
- world=self._resolve_world(),
294
- )
295
- timer.mark("command_ctx_end")
296
-
297
- timer.mark("cheats_start")
298
- self.cheat_manager.process_frame(
299
- input_frame,
300
- context=command_context,
301
- queue=self.command_queue,
302
- )
303
- timer.mark("cheats_end")
304
-
305
- # Execute commands at the end of the frame (consistent write path)
306
- timer.mark("cmd_exec_start")
307
- for cmd in self.command_queue.drain():
308
- cmd.execute(command_context)
309
- timer.mark("cmd_exec_end")
310
-
311
- # ---------------- TO REPLACE WITH RENDERING PIPELINE ----------------
312
- timer.mark("render_start")
313
-
314
- vp = self.services.window.get_viewport()
315
-
316
- # gather visible packets
317
- frame_packets: list[RenderPacket] = []
318
- for entry in self.services.scenes.visible_entries():
319
- scene = entry.scene
320
- packet = packet_cache.get(id(scene))
321
- if packet is None:
322
- packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
323
- packet_cache[id(scene)] = packet
324
- frame_packets.append(
325
- FramePacket(
326
- scene_id=entry.scene_id,
327
- is_overlay=entry.is_overlay,
328
- packet=packet,
329
- )
330
- )
331
-
332
- render_ctx = RenderContext(
333
- viewport=vp,
334
- debug_overlay=getattr(self.settings, "debug_overlay", False),
335
- frame_ms=dt * 1000.0,
336
- )
337
-
338
- self.services.render.last_frame_ms = render_ctx.frame_ms
339
- self.services.render.last_stats = render_ctx.stats
340
- pipeline.render_frame(backend, render_ctx, frame_packets)
341
-
342
- timer.mark("render_done")
343
- # ---------------- END RENDERING PIPELINE ----------------------------
344
- timer.mark("end_frame_done")
345
-
346
- timer.mark("sleep_start")
347
- if target_dt > 0 and dt < target_dt:
348
- sleep(target_dt - dt)
349
- timer.mark("sleep_end")
350
-
351
- # --- report ---
352
- # if timer.enabled and (
353
- # frame_index % report_every == 0 and frame_index > 0
354
- # ):
355
- # ms = timer.report_ms()
356
- # total = (perf_counter() - timer.marks["frame_start"]) * 1000.0
357
- # logger.debug(
358
- # f"[Frame {frame_index}] total={total:.2f}ms | {ms}"
359
- # )
360
-
361
- frame_index += 1
362
-
363
- # pylint: enable=assignment-from-no-return
364
-
365
- # exit remaining scenes
366
- self.services.scenes.clean()
120
+ for p in pipeline.passes:
121
+ if getattr(p, "name", "") == "PostFXPass":
122
+ p.registry = effects_registry
367
123
 
368
- # pylint: enable=too-many-statements,too-many-locals
124
+ self._running = True
369
125
 
370
- def _initialize_window(self):
371
- """Initialize the game window based on the configuration."""
372
- self.services.window.set_window_size(
373
- self.config.window.width, self.config.window.height
126
+ timer = FrameTimer(
127
+ config=FrameTimerConfig(enabled=self.config.enable_profiler)
374
128
  )
375
- self.services.window.set_title(self.config.window.title)
129
+ hooks = DefaultGameHooks(self, effects_stack)
376
130
 
377
- br, bg, bb = self.config.window.background_color
378
- self.services.window.set_clear_color(br, bg, bb)
379
-
380
- # the “authoring resolution”
381
131
  self.services.window.set_virtual_resolution(800, 600)
132
+ runner = EngineRunner(
133
+ self,
134
+ pipeline=pipeline,
135
+ effects_stack=effects_stack,
136
+ hooks=hooks,
137
+ )
138
+ runner.run(cfg=RunnerConfig(fps=self.config.fps), timer=timer)
382
139
 
383
- def _resolve_world(self) -> object | None:
140
+ def resolve_world(self) -> object | None:
141
+ """
142
+ Resolve and return the current gameplay world.
143
+
144
+ :return: The current gameplay world, or None if not found.
145
+ :rtype: object | None
146
+ """
384
147
  # Prefer gameplay world underneath overlays:
385
148
  # scan from top to bottom and pick the first scene that has .world
386
- for entry in reversed(self.services.scenes.visible_entries()):
149
+ for entry in reversed(self.managers.scenes.visible_entries()):
387
150
  scene = entry.scene
388
151
  world = getattr(scene, "world", None)
389
152
  if world is not None:
390
153
  return world
391
154
  return None
392
-
393
-
394
- # pylint: enable=too-many-instance-attributes
@@ -0,0 +1,40 @@
1
+ """
2
+ Game configuration classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+
9
+ from mini_arcade_core.backend import Backend
10
+
11
+
12
+ @dataclass
13
+ class PostFXConfig:
14
+ """
15
+ Configuration for post-processing effects.
16
+
17
+ :ivar enabled (bool): Whether post effects are enabled by default.
18
+ :ivar active (list[str]): List of active effect IDs by default.
19
+ """
20
+
21
+ enabled: bool = True
22
+ active: list[str] = field(default_factory=list)
23
+
24
+
25
+ @dataclass
26
+ class GameConfig:
27
+ """
28
+ Configuration options for the Game.
29
+
30
+ :ivar initial_scene (str): Identifier of the initial scene to load.
31
+ :ivar fps (int): Target frames per second.
32
+ :ivar backend (Backend | None): Optional Backend instance to use for rendering and input.
33
+ :ivar postfx (PostFXConfig): Configuration for post-processing effects.
34
+ """
35
+
36
+ initial_scene: str = "main"
37
+ fps: int = 60
38
+ backend: Backend | None = None
39
+ postfx: PostFXConfig = field(default_factory=PostFXConfig)
40
+ enable_profiler: bool = False
@@ -0,0 +1,24 @@
1
+ """
2
+ Gameplay settings that can be modified during gameplay.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Literal
9
+
10
+ from mini_arcade_core.engine.render.effects.base import EffectStack
11
+
12
+ Difficulty = Literal["easy", "normal", "hard", "insane"]
13
+
14
+
15
+ @dataclass
16
+ class GamePlaySettings:
17
+ """
18
+ Game settings that can be modified during gameplay.
19
+
20
+ :ivar difficulty (Difficulty): Current game difficulty level.
21
+ """
22
+
23
+ difficulty: Difficulty = "normal"
24
+ effects_stack: EffectStack | None = None
@@ -0,0 +1,20 @@
1
+ """
2
+ Game core module defining the Game class and configuration.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RunnerConfig:
12
+ """
13
+ Configuration for the main loop runner.
14
+
15
+ :ivar fps (int): Target frames per second (0 for uncapped).
16
+ :ivar max_frames (int | None): Optional maximum number of frames to run (None for unlimited).
17
+ """
18
+
19
+ fps: int = 60
20
+ max_frames: int | None = None
@@ -0,0 +1,77 @@
1
+ """
2
+ Game core module defining the Game class and configuration.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Iterable, Protocol
8
+
9
+ from mini_arcade_core.backend.events import Event, EventType
10
+ from mini_arcade_core.backend.keys import Key
11
+ from mini_arcade_core.engine.commands import (
12
+ ToggleDebugOverlayCommand,
13
+ ToggleEffectCommand,
14
+ )
15
+ from mini_arcade_core.engine.render.effects.base import EffectStack
16
+ from mini_arcade_core.utils import logger
17
+
18
+ if TYPE_CHECKING:
19
+ from mini_arcade_core.engine.game import Game
20
+
21
+
22
+ class LoopHooks(Protocol):
23
+ """
24
+ Protocol for custom loop hooks to handle events.
25
+ """
26
+
27
+ def on_events(self, events: Iterable[object]):
28
+ """
29
+ Docstring for on_events
30
+
31
+ :param events: Iterable of input events.
32
+ :type events: Iterable[object]
33
+ """
34
+
35
+
36
+ class DefaultGameHooks:
37
+ """
38
+ Default implementation of LoopHooks for handling common events.
39
+
40
+ :param game: The Game instance.
41
+ :type game: Game
42
+ :param effects_stack: The EffectStack for post-processing effects.
43
+ :type effects_stack: EffectStack
44
+ """
45
+
46
+ def __init__(self, game: "Game", effects_stack: EffectStack):
47
+ self.game = game
48
+ self.effects_stack = effects_stack
49
+
50
+ def on_events(self, events: Iterable[Event]):
51
+ """
52
+ Handle common events such as window resize and debug toggles.
53
+
54
+ :param events: Iterable of input events.
55
+ :type events: Iterable[Event]
56
+ """
57
+ for e in events:
58
+ if e.type == EventType.WINDOWRESIZED and e.size:
59
+ w, h = e.size
60
+ logger.debug(f"Window resized event: {w}x{h}")
61
+ self.game.services.window.on_window_resized(w, h)
62
+
63
+ if e.type == EventType.KEYDOWN:
64
+ if e.key == Key.F1:
65
+ self.game.managers.command_queue.push(
66
+ ToggleDebugOverlayCommand()
67
+ )
68
+ elif e.key == Key.F2:
69
+ self.game.managers.command_queue.push(
70
+ ToggleEffectCommand("crt")
71
+ )
72
+ elif e.key == Key.F3:
73
+ self.game.managers.command_queue.push(
74
+ ToggleEffectCommand("vignette_noise")
75
+ )
76
+ elif e.key == Key.F4:
77
+ self.effects_stack.enabled = not self.effects_stack.enabled