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