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
@@ -9,7 +9,8 @@ import traceback
9
9
  from importlib.metadata import PackageNotFoundError, version
10
10
  from typing import Callable, Type, Union
11
11
 
12
- from mini_arcade_core.engine.game import Game, GameConfig, WindowConfig
12
+ from mini_arcade_core.engine.game import Game
13
+ from mini_arcade_core.engine.game_config import GameConfig
13
14
  from mini_arcade_core.scenes.registry import SceneRegistry
14
15
  from mini_arcade_core.scenes.sim_scene import SimScene
15
16
  from mini_arcade_core.utils import logger
@@ -17,59 +18,31 @@ from mini_arcade_core.utils import logger
17
18
  SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
18
19
 
19
20
 
20
- # TODO: Improve exception handling and logging in run_game
21
- # TODO: Consider reducing parameters by using a single config object
22
- # TODO: Delegate responsibilities to Game class where appropriate
21
+ # TODO: Improve exception handliers by usingng and logging in run_game
23
22
  def run_game(
24
- scene: SceneFactoryLike | None = None,
25
- config: GameConfig | None = None,
26
- registry: SceneRegistry | None = None,
27
- initial_scene: str = "main",
23
+ game_config: GameConfig | None = None,
24
+ scene_registry: SceneRegistry | None = None,
28
25
  ):
29
26
  """
30
27
  Convenience helper to bootstrap and run a game with a single scene.
31
28
 
32
- Supports both:
33
- - run_game(SceneClass, cfg) # legacy
34
- - run_game(config=cfg, initial_scene="main", registry=...) # registry-based
35
- - run_game(cfg) # config-only
29
+ :param game_config: Optional GameConfig to customize game settings.
30
+ :type game_config: GameConfig | None
36
31
 
37
- :param scene: Optional SimScene factory/class to register
38
- :type scene: SceneFactoryLike | None
32
+ :param scene_registry: Optional SceneRegistry for scene management.
33
+ :type scene_registry: SceneRegistry | None
39
34
 
40
- :param initial_scene: The SimScene ID to start the game with.
41
- :type initial_scene: str
42
-
43
- :param config: Optional GameConfig to customize game settings.
44
- :type config: GameConfig | None
45
-
46
- :param registry: Optional SceneRegistry for scene management.
47
- :type registry: SceneRegistry | None
48
-
49
- :raises ValueError: If the provided config does not have a valid Backend.
35
+ :raises ValueError: If the provided game_config does not have a valid Backend.
50
36
  """
51
37
  try:
52
- # Handle run_game(cfg) where the first arg is actually a GameConfig
53
- if isinstance(scene, GameConfig) and config is None:
54
- config = scene
55
- scene = None
56
-
57
- cfg = config or GameConfig()
38
+ cfg = game_config or GameConfig()
58
39
  if cfg.backend is None:
59
40
  raise ValueError(
60
41
  "GameConfig.backend must be set to a Backend instance"
61
42
  )
62
43
 
63
- # If user provided a SimScene factory/class, ensure it's registered
64
- if scene is not None:
65
- if registry is None:
66
- registry = SceneRegistry(_factories={})
67
- registry.register(
68
- initial_scene, scene
69
- ) # SimScene class is callable(game) -> SimScene
70
-
71
- game = Game(cfg, registry=registry)
72
- game.run(initial_scene)
44
+ game = Game(cfg, scene_registry=scene_registry)
45
+ game.run()
73
46
  # Justification: We need to catch all exceptions while we improve error handling.
74
47
  # pylint: disable=broad-exception-caught
75
48
  except Exception as e:
@@ -78,7 +51,7 @@ def run_game(
78
51
  # pylint: enable=broad-exception-caught
79
52
 
80
53
 
81
- PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
54
+ PACKAGE_NAME = "mini-arcade-core"
82
55
 
83
56
 
84
57
  def get_version() -> str:
@@ -107,7 +80,6 @@ def get_version() -> str:
107
80
  __all__ = [
108
81
  "Game",
109
82
  "GameConfig",
110
- "WindowConfig",
111
83
  "run_game",
112
84
  ]
113
85
 
@@ -6,9 +6,8 @@ This is the only part of the code that talks to SDL/pygame directly.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from .backend import Backend, WindowSettings
9
+ from .backend import Backend
10
10
 
11
11
  __all__ = [
12
12
  "Backend",
13
- "WindowSettings",
14
13
  ]
@@ -5,287 +5,318 @@ This is the only part of the code that talks to SDL/pygame directly.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from dataclasses import dataclass
9
8
  from typing import Iterable, Protocol
10
9
 
11
10
  from .events import Event
12
- from .types import Color
13
11
 
12
+ # Justification: Many positional and keyword arguments needed for some backend methods.
13
+ # Might be refactored later.
14
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
14
15
 
15
- @dataclass
16
- class WindowSettings:
17
- """
18
- Settings for the backend window.
19
16
 
20
- :ivar width (int): Width of the window in pixels.
21
- :ivar height (int): Height of the window in pixels.
17
+ class WindowProtocol(Protocol):
18
+ """
19
+ Represents a game window.
22
20
  """
23
21
 
24
22
  width: int
25
23
  height: int
26
24
 
25
+ def set_title(self, title: str):
26
+ """
27
+ Set the window title.
27
28
 
28
- # TODO: Refactor backend interface into smaller protocols?
29
- # Justification: Many public methods needed for backend interface
30
- # pylint: disable=too-many-public-methods
31
- class Backend(Protocol):
32
- """
33
- Interface that any rendering/input backend must implement.
29
+ :param title: New window title.
30
+ :type title: str
31
+ """
34
32
 
35
- mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
36
- """
33
+ def resize(self, width: int, height: int):
34
+ """
35
+ Resize the window.
37
36
 
38
- def init(self, window_settings: WindowSettings):
37
+ :param width: New width in pixels.
38
+ :type width: int
39
+ :param height: New height in pixels.
40
+ :type height: int
39
41
  """
40
- Initialize the backend and open a window.
41
- Should be called once before the main loop.
42
42
 
43
- :param window_settings: Settings for the backend window.
44
- :type window_settings: WindowSettings
43
+ def size(self) -> tuple[int, int]:
45
44
  """
45
+ Get the window size.
46
46
 
47
- def set_window_title(self, title: str):
47
+ :return: Tuple of (width, height) in pixels.
48
+ :rtype: tuple[int, int]
48
49
  """
49
- Set the window title.
50
50
 
51
- :param title: The new title for the window.
52
- :type title: str
51
+ def drawable_size(self) -> tuple[int, int]:
53
52
  """
54
- raise NotImplementedError
53
+ Get the drawable size of the window.
55
54
 
56
- def poll_events(self) -> Iterable[Event]:
55
+ :return: Tuple of (width, height) in pixels.
56
+ :rtype: tuple[int, int]
57
57
  """
58
- Return all pending events since last call.
59
- Concrete backends will translate their native events into core Event objects.
60
58
 
61
- :return: An iterable of Event objects.
59
+
60
+ class InputProtocol(Protocol):
61
+ """
62
+ Interface for input operations.
63
+ """
64
+
65
+ def poll(self) -> Iterable[Event]:
66
+ """
67
+ Get the list of input events since the last call.
68
+
69
+ :return: Iterable of Event instances.
62
70
  :rtype: Iterable[Event]
63
71
  """
64
72
 
73
+
74
+ class RenderProtocol(Protocol):
75
+ """
76
+ Interface for rendering operations.
77
+ """
78
+
65
79
  def set_clear_color(self, r: int, g: int, b: int):
66
80
  """
67
- Set the background/clear color used by begin_frame.
81
+ Set the clear color for the renderer.
68
82
 
69
83
  :param r: Red component (0-255).
70
84
  :type r: int
71
-
72
85
  :param g: Green component (0-255).
73
86
  :type g: int
74
-
75
87
  :param b: Blue component (0-255).
76
88
  :type b: int
77
89
  """
78
90
 
79
91
  def begin_frame(self):
80
- """
81
- Prepare for drawing a new frame (e.g. clear screen).
82
- """
92
+ """Begin a new rendering frame."""
83
93
 
84
94
  def end_frame(self):
85
- """
86
- Present the frame to the user (swap buffers).
87
- """
95
+ """End the current rendering frame."""
88
96
 
89
- # Justification: Simple drawing API for now
90
- # pylint: disable=too-many-arguments,too-many-positional-arguments
91
- def draw_rect(
92
- self,
93
- x: int,
94
- y: int,
95
- w: int,
96
- h: int,
97
- color: Color = (255, 255, 255),
98
- ):
97
+ def draw_rect(self, x: int, y: int, w: int, h: int, color=(255, 255, 255)):
99
98
  """
100
- Draw a filled rectangle in some default color.
101
- We'll keep this minimal for now; later we can extend with colors/sprites.
99
+ Draw a filled rectangle.
102
100
 
103
- :param x: X position of the rectangle's top-left corner.
101
+ :param x: The x-coordinate of the rectangle.
104
102
  :type x: int
105
-
106
- :param y: Y position of the rectangle's top-left corner.
103
+ :param y: The y-coordinate of the rectangle.
107
104
  :type y: int
108
-
109
- :param w: Width of the rectangle.
105
+ :param w: The width of the rectangle.
110
106
  :type w: int
111
-
112
- :param h: Height of the rectangle.
107
+ :param h: The height of the rectangle.
113
108
  :type h: int
114
-
115
- :param color: RGB color tuple.
116
- :type color: Color
109
+ :param color: The color of the rectangle as an (R, G, B) or (R, G, B, A) tuple.
110
+ :type color: tuple[int, int, int] | tuple[int, int, int, int]
117
111
  """
118
112
 
119
- def draw_text(
120
- self,
121
- x: int,
122
- y: int,
123
- text: str,
124
- color: Color = (255, 255, 255),
125
- font_size: int | None = None,
113
+ def draw_line(
114
+ self, x1: int, y1: int, x2: int, y2: int, color=(255, 255, 255)
126
115
  ):
127
116
  """
128
- Draw text at the given position in a default font and color.
117
+ Draw a line between two points.
129
118
 
130
- Backends may ignore advanced styling for now; this is just to render
131
- simple labels like menu items, scores, etc.
119
+ :param x1: The x-coordinate of the start point.
120
+ :type x1: int
121
+ :param y1: The y-coordinate of the start point.
122
+ :type y1: int
123
+ :param x2: The x-coordinate of the end point.
124
+ :type x2: int
125
+ :param y2: The y-coordinate of the end point.
126
+ :type y2: int
127
+ :param color: The color of the line as an (R, G, B) or (R, G, B, A) tuple.
128
+ :type color: tuple[int, int, int] | tuple[int, int, int, int]
129
+ """
132
130
 
133
- :param x: X position of the text's top-left corner.
134
- :type x: int
131
+ def set_clip_rect(self, x: int, y: int, w: int, h: int):
132
+ """
133
+ Set the clipping rectangle.
135
134
 
136
- :param y: Y position of the text's top-left corner.
135
+ :param x: The x-coordinate of the clipping rectangle.
136
+ :type x: int
137
+ :param y: The y-coordinate of the clipping rectangle.
137
138
  :type y: int
139
+ :param w: The width of the clipping rectangle.
140
+ :type w: int
141
+ :param h: The height of the clipping rectangle.
142
+ :type h: int
143
+ """
138
144
 
139
- :param text: The text string to draw.
140
- :type text: str
145
+ def clear_clip_rect(self):
146
+ """Clear the clipping rectangle."""
141
147
 
142
- :param color: RGB color tuple.
143
- :type color: Color
144
- """
145
148
 
146
- # pylint: enable=too-many-arguments,too-many-positional-arguments
149
+ class TextProtocol(Protocol):
150
+ """
151
+ Interface for text rendering operations.
152
+ """
147
153
 
148
- def measure_text(self, text: str) -> tuple[int, int]:
154
+ def measure(
155
+ self, text: str, font_size: int | None = None
156
+ ) -> tuple[int, int]:
149
157
  """
150
- Measure the width and height of the given text string in pixels.
158
+ Measure the width and height of the given text.
151
159
 
152
- :param text: The text string to measure.
160
+ :param text: The text to measure.
153
161
  :type text: str
154
-
155
- :return: A tuple (width, height) in pixels.
162
+ :param font_size: The font size to use for measurement.
163
+ :type font_size: int | None
164
+ :return: A tuple containing the width and height of the text.
156
165
  :rtype: tuple[int, int]
157
166
  """
158
- raise NotImplementedError
159
167
 
160
- def capture_frame(self, path: str | None = None) -> bytes | None:
168
+ def draw(
169
+ self,
170
+ x: int,
171
+ y: int,
172
+ text: str,
173
+ color=(255, 255, 255),
174
+ font_size: int | None = None,
175
+ ):
176
+ """
177
+ Draw the given text at the specified position.
178
+
179
+ :param x: The x-coordinate to draw the text.
180
+ :type x: int
181
+ :param y: The y-coordinate to draw the text.
182
+ :type y: int
183
+ :param text: The text to draw.
184
+ :type text: str
185
+ :param color: The color of the text as an (R, G, B) or (R, G, B, A) tuple.
186
+ :type color: tuple[int, int, int] | tuple[int, int, int, int]
187
+ :param font_size: The font size to use for drawing.
188
+ :type font_size: int | None
161
189
  """
162
- Capture the current frame.
163
- If `path` is provided, save to that file (e.g. PNG).
164
- Returns raw bytes (PNG) or None if unsupported.
165
190
 
166
- :param path: Optional file path to save the screenshot.
167
- :type path: str | None
168
191
 
169
- :return: Raw image bytes if no path given, else None.
170
- :rtype: bytes | None
171
- """
172
- raise NotImplementedError
192
+ class AudioProtocol(Protocol):
193
+ """
194
+ Interface for audio operations.
195
+ """
173
196
 
174
- def init_audio(
197
+ def init(
175
198
  self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
176
199
  ):
177
200
  """
178
- Initialize SDL_mixer audio.
201
+ Initialize audio subsystem.
179
202
 
180
203
  :param frequency: Audio frequency in Hz.
181
204
  :type frequency: int
182
-
183
205
  :param channels: Number of audio channels (1=mono, 2=stereo).
184
206
  :type channels: int
185
-
186
207
  :param chunk_size: Size of audio chunks.
187
208
  :type chunk_size: int
188
209
  """
189
210
 
190
- def shutdown_audio(self):
191
- """Shutdown SDL_mixer audio and free loaded sounds."""
211
+ def shutdown(self):
212
+ """
213
+ Shutdown the audio subsystem.
214
+ """
192
215
 
193
216
  def load_sound(self, sound_id: str, path: str):
194
217
  """
195
- Load a WAV sound and store it by ID.
196
- Example: backend.load_sound("hit", "assets/sfx/hit.wav")
218
+ Load a sound file.
197
219
 
198
220
  :param sound_id: Unique identifier for the sound.
199
221
  :type sound_id: str
200
-
201
- :param path: File path to the WAV sound.
222
+ :param path: File path to the sound.
202
223
  :type path: str
203
224
  """
204
225
 
205
226
  def play_sound(self, sound_id: str, loops: int = 0):
206
227
  """
207
228
  Play a loaded sound.
208
- loops=0 => play once
209
- loops=-1 => infinite loop
210
- loops=1 => play twice (SDL convention)
211
229
 
212
230
  :param sound_id: Unique identifier for the sound.
213
231
  :type sound_id: str
214
-
215
232
  :param loops: Number of times to loop the sound.
216
233
  :type loops: int
217
234
  """
218
235
 
219
236
  def set_master_volume(self, volume: int):
220
237
  """
221
- Master volume: 0..128
238
+ Set the master volume.
239
+
240
+ :param volume: Volume level (0-128).
241
+ :type volume: int
222
242
  """
223
243
 
224
244
  def set_sound_volume(self, sound_id: str, volume: int):
225
245
  """
226
- Per-sound volume: 0..128
246
+ Set volume for a specific sound.
227
247
 
228
248
  :param sound_id: Unique identifier for the sound.
229
249
  :type sound_id: str
230
-
231
250
  :param volume: Volume level (0-128).
232
251
  :type volume: int
233
252
  """
234
253
 
235
- def stop_all_sounds(self):
236
- """Stop all channels."""
237
-
238
- def set_viewport_transform(
239
- self, offset_x: int, offset_y: int, scale: float
240
- ):
254
+ def stop_all(self):
255
+ """
256
+ Stop all currently playing sounds.
241
257
  """
242
- Apply a transform so draw_* receives VIRTUAL coords and backend maps to screen.
243
258
 
244
- :param offset_x: X offset in pixels.
245
- :type offset_x: int
246
259
 
247
- :param offset_y: Y offset in pixels.
248
- :type offset_y: int
260
+ class CaptureProtocol(Protocol):
261
+ """
262
+ Interface for frame capture operations.
263
+ """
249
264
 
250
- :param scale: Scale factor.
251
- :type scale: float
265
+ def bmp(self, path: str | None = None) -> bool:
252
266
  """
253
- raise NotImplementedError
254
-
255
- def clear_viewport_transform(self):
256
- """Reset any viewport transform back to identity."""
257
- raise NotImplementedError
267
+ Capture the current frame as a BMP file.
258
268
 
259
- def resize_window(self, width: int, height: int):
269
+ :param path: Optional file path to save the BMP. If None, returns bytes.
270
+ :type path: str | None
271
+ :return: Whether the capture was successful.
272
+ :rtype: bool
260
273
  """
261
- Resize the actual OS window (SDL_SetWindowSize in native backend).
262
274
 
263
- :param width: New width in pixels.
264
- :type width: int
265
275
 
266
- :param height: New height in pixels.
267
- :type height: int
268
- """
269
- raise NotImplementedError
276
+ # TODO: Refactor backend interface into smaller protocols?
277
+ # Justification: Many public methods needed for backend interface
278
+ # pylint: disable=too-many-public-methods
279
+ class Backend(Protocol):
280
+ """
281
+ Interface that any rendering/input backend must implement.
282
+ mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
270
283
 
271
- def set_clip_rect(self, x: int, y: int, w: int, h: int):
272
- """
273
- Set a clipping rectangle for rendering.
284
+ :ivar window (WindowProtocol): Window management interface.
285
+ :ivar audio (AudioProtocol): Audio management interface.
286
+ :ivar input (InputProtocol): Input management interface.
287
+ :ivar render (RenderProtocol): Rendering interface.
288
+ :ivar text (TextProtocol): Text rendering interface.
289
+ :ivar capture (CaptureProtocol): Frame capture interface.
290
+ """
274
291
 
275
- :param x: X position of the rectangle's top-left corner.
276
- :type x: int
292
+ window: WindowProtocol
293
+ audio: AudioProtocol
294
+ input: InputProtocol
295
+ render: RenderProtocol
296
+ text: TextProtocol
297
+ capture: CaptureProtocol
277
298
 
278
- :param y: Y position of the rectangle's top-left corner.
279
- :type y: int
299
+ def init(self):
300
+ """
301
+ Initialize the backend and open a window.
302
+ Should be called once before the main loop.
303
+ """
280
304
 
281
- :param w: Width of the rectangle.
282
- :type w: int
305
+ def set_viewport_transform(
306
+ self, offset_x: int, offset_y: int, scale: float
307
+ ):
308
+ """
309
+ Set the viewport transformation.
283
310
 
284
- :param h: Height of the rectangle.
285
- :type h: int
311
+ :param offset_x: Horizontal offset.
312
+ :type offset_x: int
313
+ :param offset_y: Vertical offset.
314
+ :type offset_y: int
315
+ :param scale: Scaling factor.
316
+ :type scale: float
286
317
  """
287
- raise NotImplementedError
288
318
 
289
- def clear_clip_rect(self):
290
- """Clear any clipping rectangle."""
291
- raise NotImplementedError
319
+ def clear_viewport_transform(self):
320
+ """
321
+ Clear the viewport transformation (reset to defaults).
322
+ """
@@ -6,4 +6,8 @@ from __future__ import annotations
6
6
 
7
7
  from typing import Tuple, Union
8
8
 
9
- Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
9
+ ColorRGB = Tuple[int, int, int]
10
+ ColorRGBA = Tuple[int, int, int, int]
11
+
12
+ Color = Union[ColorRGB, ColorRGBA]
13
+ Alpha = Union[float, int]
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
  from dataclasses import dataclass, field
8
8
  from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
9
9
 
10
- from mini_arcade_core.runtime.scene.scene_port import ScenePolicy
10
+ from mini_arcade_core.engine.scenes.models import ScenePolicy
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from mini_arcade_core.runtime.services import RuntimeServices
@@ -30,7 +30,7 @@ class CommandContext:
30
30
  """
31
31
 
32
32
  services: RuntimeServices
33
- commands: Optional["CommandQueue"] = None
33
+ managers: object
34
34
  settings: Optional[object] = None
35
35
  world: Optional[object] = None
36
36
 
@@ -104,7 +104,7 @@ class QuitCommand(Command):
104
104
  self,
105
105
  context: CommandContext,
106
106
  ):
107
- context.services.scenes.quit()
107
+ context.managers.scenes.quit()
108
108
 
109
109
 
110
110
  @dataclass(frozen=True)
@@ -140,7 +140,7 @@ class PushSceneCommand(Command):
140
140
  self,
141
141
  context: CommandContext,
142
142
  ):
143
- context.services.scenes.push(self.scene_id, as_overlay=self.as_overlay)
143
+ context.managers.scenes.push(self.scene_id, as_overlay=self.as_overlay)
144
144
 
145
145
 
146
146
  @dataclass(frozen=True)
@@ -151,7 +151,7 @@ class PopSceneCommand(Command):
151
151
  self,
152
152
  context: CommandContext,
153
153
  ):
154
- context.services.scenes.pop()
154
+ context.managers.scenes.pop()
155
155
 
156
156
 
157
157
  @dataclass(frozen=True)
@@ -168,7 +168,7 @@ class ChangeSceneCommand(Command):
168
168
  self,
169
169
  context: CommandContext,
170
170
  ):
171
- context.services.scenes.change(self.scene_id)
171
+ context.managers.scenes.change(self.scene_id)
172
172
 
173
173
 
174
174
  @dataclass(frozen=True)
@@ -182,7 +182,7 @@ class ToggleDebugOverlayCommand(Command):
182
182
  DEBUG_OVERLAY_ID = "debug_overlay"
183
183
 
184
184
  def execute(self, context: CommandContext):
185
- scenes = context.services.scenes
185
+ scenes = context.managers.scenes
186
186
  if scenes.has_scene(self.DEBUG_OVERLAY_ID):
187
187
  scenes.remove_scene(self.DEBUG_OVERLAY_ID)
188
188
  return
@@ -197,3 +197,22 @@ class ToggleDebugOverlayCommand(Command):
197
197
  receives_input=False,
198
198
  ),
199
199
  )
200
+
201
+
202
+ @dataclass(frozen=True)
203
+ class ToggleEffectCommand(Command):
204
+ """
205
+ Toggle a post-processing effect on or off.
206
+
207
+ :ivar effect_id (str): Identifier of the effect to toggle.
208
+ """
209
+
210
+ effect_id: str
211
+
212
+ def execute(self, context: CommandContext):
213
+ # effects live in context.meta OR in a dedicated service/settings.
214
+ # v1 simplest: stash stack into context.settings or context.services.render
215
+ stack = getattr(context.settings, "effects_stack", None)
216
+ if stack is None:
217
+ return
218
+ stack.toggle(self.effect_id)