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.
- mini_arcade_core/__init__.py +14 -42
- mini_arcade_core/backend/__init__.py +1 -2
- mini_arcade_core/backend/backend.py +185 -154
- mini_arcade_core/backend/types.py +5 -1
- mini_arcade_core/engine/commands.py +26 -7
- mini_arcade_core/engine/game.py +79 -319
- mini_arcade_core/engine/game_config.py +40 -0
- mini_arcade_core/engine/gameplay_settings.py +24 -0
- mini_arcade_core/engine/loop/config.py +20 -0
- mini_arcade_core/engine/loop/hooks.py +77 -0
- mini_arcade_core/engine/loop/runner.py +272 -0
- mini_arcade_core/engine/loop/state.py +32 -0
- mini_arcade_core/engine/managers.py +24 -0
- mini_arcade_core/engine/render/context.py +0 -2
- mini_arcade_core/engine/render/effects/base.py +88 -0
- mini_arcade_core/engine/render/effects/crt.py +68 -0
- mini_arcade_core/engine/render/effects/registry.py +50 -0
- mini_arcade_core/engine/render/effects/vignette.py +79 -0
- mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
- mini_arcade_core/engine/render/passes/end_frame.py +1 -1
- mini_arcade_core/engine/render/passes/postfx.py +25 -4
- mini_arcade_core/engine/render/passes/ui.py +1 -1
- mini_arcade_core/engine/render/passes/world.py +6 -6
- mini_arcade_core/engine/render/pipeline.py +7 -6
- mini_arcade_core/engine/render/viewport.py +10 -4
- mini_arcade_core/engine/scenes/__init__.py +0 -0
- mini_arcade_core/engine/scenes/models.py +54 -0
- mini_arcade_core/engine/scenes/scene_manager.py +213 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
- mini_arcade_core/runtime/audio/audio_port.py +0 -4
- mini_arcade_core/runtime/capture/capture_adapter.py +13 -6
- mini_arcade_core/runtime/capture/capture_port.py +0 -4
- mini_arcade_core/runtime/context.py +8 -6
- mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
- mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
- mini_arcade_core/runtime/services.py +3 -2
- mini_arcade_core/runtime/window/window_adapter.py +43 -41
- mini_arcade_core/runtime/window/window_port.py +3 -17
- mini_arcade_core/scenes/debug_overlay.py +5 -4
- mini_arcade_core/scenes/registry.py +11 -1
- mini_arcade_core/scenes/sim_scene.py +14 -14
- mini_arcade_core/ui/menu.py +54 -16
- mini_arcade_core/utils/__init__.py +2 -1
- mini_arcade_core/utils/logging.py +47 -18
- mini_arcade_core/utils/profiler.py +283 -0
- {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/METADATA +1 -1
- mini_arcade_core-1.2.0.dist-info/RECORD +92 -0
- {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/WHEEL +1 -1
- mini_arcade_core/managers/inputs.py +0 -284
- mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
- mini_arcade_core/runtime/scene/scene_port.py +0 -170
- mini_arcade_core/sim/protocols.py +0 -41
- mini_arcade_core/sim/runner.py +0 -222
- mini_arcade_core-1.1.0.dist-info/RECORD +0 -80
- /mini_arcade_core/{managers → engine}/cheats.py +0 -0
- /mini_arcade_core/{managers → engine/loop}/__init__.py +0 -0
- /mini_arcade_core/{sim → engine/render/effects}/__init__.py +0 -0
- {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/licenses/LICENSE +0 -0
mini_arcade_core/engine/game.py
CHANGED
|
@@ -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
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from mini_arcade_core.
|
|
12
|
-
from mini_arcade_core.
|
|
13
|
-
from mini_arcade_core.
|
|
14
|
-
from mini_arcade_core.engine.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
21
|
-
from mini_arcade_core.engine.render.
|
|
22
|
-
from mini_arcade_core.engine.render.
|
|
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.
|
|
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.
|
|
31
|
-
|
|
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
|
|
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,
|
|
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
|
|
164
|
-
:type
|
|
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.
|
|
180
|
-
self.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
124
|
+
self._running = True
|
|
369
125
|
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|