mini-arcade-core 1.1.0__tar.gz → 1.1.1__tar.gz
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-1.1.0 → mini_arcade_core-1.1.1}/PKG-INFO +1 -1
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/pyproject.toml +1 -1
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/backend/backend.py +33 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/commands.py +19 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/game.py +62 -2
- mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/effects/base.py +88 -0
- mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/effects/crt.py +68 -0
- mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/effects/registry.py +50 -0
- mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/effects/vignette.py +79 -0
- mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/passes/postfx.py +49 -0
- mini_arcade_core-1.1.1/src/mini_arcade_core/ui/__init__.py +0 -0
- mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/postfx.py +0 -28
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/LICENSE +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/README.md +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/backend/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/backend/events.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/backend/keys.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/backend/sdl_map.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/backend/types.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/bus.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/context.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes → mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/effects}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/frame_packet.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/packet.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/managers → mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/passes}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/base.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/begin_frame.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/end_frame.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/lighting.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/ui.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/world.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/pipeline.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/render_service.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/viewport.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime → mini_arcade_core-1.1.1/src/mini_arcade_core/managers}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/managers/cheats.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/managers/inputs.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/audio → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime}/__init__.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/capture → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/audio}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/audio/audio_adapter.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/audio/audio_port.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/file → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/capture}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/capture/capture_adapter.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/capture/capture_port.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/context.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/input → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/file}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/file/file_adapter.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/file/file_port.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/render → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/input}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/input/input_adapter.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/input/input_port.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/input_frame.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/scene → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/render}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/render/render_port.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/window → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/scene}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/scene/scene_adapter.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/scene/scene_port.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/services.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/scenes → mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/window}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/window/window_adapter.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/window/window_port.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/scenes/systems → mini_arcade_core-1.1.1/src/mini_arcade_core/scenes}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/autoreg.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/debug_overlay.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/registry.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/sim_scene.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/sim → mini_arcade_core-1.1.1/src/mini_arcade_core/scenes/systems}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/systems/base_system.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/systems/system_pipeline.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/spaces → mini_arcade_core-1.1.1/src/mini_arcade_core/sim}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/sim/protocols.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/sim/runner.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/spaces/d2 → mini_arcade_core-1.1.1/src/mini_arcade_core/spaces}/__init__.py +0 -0
- {mini_arcade_core-1.1.0/src/mini_arcade_core/ui → mini_arcade_core-1.1.1/src/mini_arcade_core/spaces/d2}/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/boundaries2d.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/collision2d.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/geometry2d.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/kinematics2d.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/physics2d.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/ui/menu.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/utils/__init__.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/utils/deprecated_decorator.py +0 -0
- {mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/utils/logging.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mini-arcade-core"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.1"
|
|
8
8
|
description = "Tiny scene-based game loop core for small arcade games."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Santiago Rincon", email = "rincores@gmail.com" },
|
|
@@ -289,3 +289,36 @@ class Backend(Protocol):
|
|
|
289
289
|
def clear_clip_rect(self):
|
|
290
290
|
"""Clear any clipping rectangle."""
|
|
291
291
|
raise NotImplementedError
|
|
292
|
+
|
|
293
|
+
# Justification: Simple drawing API for now
|
|
294
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
295
|
+
def draw_line(
|
|
296
|
+
self,
|
|
297
|
+
x1: int,
|
|
298
|
+
y1: int,
|
|
299
|
+
x2: int,
|
|
300
|
+
y2: int,
|
|
301
|
+
color: tuple[int, ...] = (255, 255, 255),
|
|
302
|
+
):
|
|
303
|
+
"""
|
|
304
|
+
Draw a line between two points in some default color.
|
|
305
|
+
|
|
306
|
+
:param x1: X position of the start point.
|
|
307
|
+
:type x1: int
|
|
308
|
+
|
|
309
|
+
:param y1: Y position of the start point.
|
|
310
|
+
:type y1: int
|
|
311
|
+
|
|
312
|
+
:param x2: X position of the end point.
|
|
313
|
+
:type x2: int
|
|
314
|
+
|
|
315
|
+
:param y2: Y position of the end point.
|
|
316
|
+
:type y2: int
|
|
317
|
+
|
|
318
|
+
:param color: RGB color tuple.
|
|
319
|
+
:type color: tuple[int, ...]
|
|
320
|
+
"""
|
|
321
|
+
raise NotImplementedError
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
@@ -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) -> None:
|
|
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)
|
|
@@ -16,8 +16,16 @@ from mini_arcade_core.engine.commands import (
|
|
|
16
16
|
CommandQueue,
|
|
17
17
|
QuitCommand,
|
|
18
18
|
ToggleDebugOverlayCommand,
|
|
19
|
+
ToggleEffectCommand,
|
|
19
20
|
)
|
|
20
21
|
from mini_arcade_core.engine.render.context import RenderContext
|
|
22
|
+
from mini_arcade_core.engine.render.effects.base import (
|
|
23
|
+
EffectParams,
|
|
24
|
+
EffectStack,
|
|
25
|
+
)
|
|
26
|
+
from mini_arcade_core.engine.render.effects.crt import CRTEffect
|
|
27
|
+
from mini_arcade_core.engine.render.effects.registry import EffectRegistry
|
|
28
|
+
from mini_arcade_core.engine.render.effects.vignette import VignetteNoiseEffect
|
|
21
29
|
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
22
30
|
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
23
31
|
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
@@ -52,6 +60,19 @@ class WindowConfig:
|
|
|
52
60
|
title: str
|
|
53
61
|
|
|
54
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
|
+
|
|
55
76
|
@dataclass
|
|
56
77
|
class GameConfig:
|
|
57
78
|
"""
|
|
@@ -65,6 +86,7 @@ class GameConfig:
|
|
|
65
86
|
window: WindowConfig | None = None
|
|
66
87
|
fps: int = 60
|
|
67
88
|
backend: Backend | None = None
|
|
89
|
+
postfx: PostFXConfig = field(default_factory=PostFXConfig)
|
|
68
90
|
|
|
69
91
|
|
|
70
92
|
Difficulty = Literal["easy", "normal", "hard", "insane"]
|
|
@@ -79,6 +101,7 @@ class GameSettings:
|
|
|
79
101
|
"""
|
|
80
102
|
|
|
81
103
|
difficulty: Difficulty = "normal"
|
|
104
|
+
effects_stack: EffectStack | None = None
|
|
82
105
|
|
|
83
106
|
|
|
84
107
|
def _neutral_input(frame_index: int, dt: float) -> InputFrame:
|
|
@@ -204,6 +227,9 @@ class Game:
|
|
|
204
227
|
# TODO: Fix too-many-statements and too-many-locals warnings
|
|
205
228
|
# Justification: Main game loop with multiple responsibilities.
|
|
206
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
|
|
207
233
|
def run(self, initial_scene_id: str):
|
|
208
234
|
"""
|
|
209
235
|
Run the main loop starting with the given scene.
|
|
@@ -222,6 +248,26 @@ class Game:
|
|
|
222
248
|
|
|
223
249
|
pipeline = RenderPipeline()
|
|
224
250
|
|
|
251
|
+
effects_registry = EffectRegistry()
|
|
252
|
+
effects_registry.register(CRTEffect())
|
|
253
|
+
effects_registry.register(VignetteNoiseEffect())
|
|
254
|
+
|
|
255
|
+
effects_stack = EffectStack(
|
|
256
|
+
enabled=self.config.postfx.enabled,
|
|
257
|
+
active=list(self.config.postfx.active),
|
|
258
|
+
params={
|
|
259
|
+
"crt": EffectParams(intensity=0.35, wobble_speed=1.0),
|
|
260
|
+
"vignette_noise": EffectParams(
|
|
261
|
+
intensity=0.25, wobble_speed=1.0
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
self.settings.effects_stack = effects_stack
|
|
266
|
+
|
|
267
|
+
for p in pipeline.passes:
|
|
268
|
+
if getattr(p, "name", "") == "PostFXPass":
|
|
269
|
+
p.registry = effects_registry
|
|
270
|
+
|
|
225
271
|
self._running = True
|
|
226
272
|
target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
|
|
227
273
|
last_time = perf_counter()
|
|
@@ -238,6 +284,7 @@ class Game:
|
|
|
238
284
|
# & self.services.scenes.input_entry
|
|
239
285
|
# Justification: These methods are expected to return values.
|
|
240
286
|
# pylint: disable=assignment-from-no-return
|
|
287
|
+
time_s = 0.0
|
|
241
288
|
|
|
242
289
|
while self._running:
|
|
243
290
|
timer.clear()
|
|
@@ -255,8 +302,17 @@ class Game:
|
|
|
255
302
|
logger.debug(f"Window resized event: {w}x{h}")
|
|
256
303
|
self.services.window.on_window_resized(w, h)
|
|
257
304
|
# if F1 pressed, toggle debug overlay
|
|
258
|
-
if e.type == EventType.KEYDOWN
|
|
259
|
-
|
|
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
|
|
260
316
|
timer.mark("events_polled")
|
|
261
317
|
|
|
262
318
|
input_frame = self.services.input.build(events, frame_index, dt)
|
|
@@ -334,6 +390,10 @@ class Game:
|
|
|
334
390
|
debug_overlay=getattr(self.settings, "debug_overlay", False),
|
|
335
391
|
frame_ms=dt * 1000.0,
|
|
336
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
|
|
337
397
|
|
|
338
398
|
self.services.render.last_frame_ms = render_ctx.frame_ms
|
|
339
399
|
self.services.render.last_stats = render_ctx.stats
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Screen-space post effects base classes and protocols.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.backend import Backend
|
|
11
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class Effect(Protocol):
|
|
16
|
+
"""
|
|
17
|
+
Screen-space post effect.
|
|
18
|
+
|
|
19
|
+
IMPORTANT: Effects should draw ONLY using ctx.viewport (screen-space),
|
|
20
|
+
and must not assume anything about world-space transforms.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
effect_id: str
|
|
24
|
+
|
|
25
|
+
def apply(self, backend: Backend, ctx: RenderContext) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Apply the effect to the current framebuffer.
|
|
28
|
+
|
|
29
|
+
:param backend: Backend to use for rendering.
|
|
30
|
+
:type backend: Backend
|
|
31
|
+
|
|
32
|
+
:param ctx: Render context with viewport info.
|
|
33
|
+
:type ctx: RenderContext
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class EffectParams:
|
|
39
|
+
"""
|
|
40
|
+
Shared params (Material-ish controls) for v1.
|
|
41
|
+
|
|
42
|
+
:ivar intensity (float): Effect intensity.
|
|
43
|
+
:ivar wobble_speed (float): Speed factor for animated distortion.
|
|
44
|
+
:ivar tint (tuple[int, int, int, int] | None): Optional RGBA tint.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
intensity: float = 1.0
|
|
48
|
+
wobble_speed: float = 1.0
|
|
49
|
+
tint: tuple[int, int, int, int] | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class EffectStack:
|
|
54
|
+
"""
|
|
55
|
+
Runtime state: what effects are enabled + their params.
|
|
56
|
+
|
|
57
|
+
Zero-overhead path:
|
|
58
|
+
- if enabled=False OR active is empty => PostFXPass returns immediately.
|
|
59
|
+
|
|
60
|
+
:ivar enabled (bool): Master toggle for post effects.
|
|
61
|
+
:ivar active (list[str]): List of active effect IDs.
|
|
62
|
+
:ivar params (dict[str, EffectParams]): Per-effect parameters.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
enabled: bool = False
|
|
66
|
+
active: list[str] = field(default_factory=list)
|
|
67
|
+
params: dict[str, EffectParams] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def is_active(self) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Check if any effects are active.
|
|
72
|
+
|
|
73
|
+
:return: True if effects are enabled and at least one is active.
|
|
74
|
+
:rtype: bool
|
|
75
|
+
"""
|
|
76
|
+
return self.enabled and bool(self.active)
|
|
77
|
+
|
|
78
|
+
def toggle(self, effect_id: str) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Toggle an effect on/off.
|
|
81
|
+
|
|
82
|
+
:param effect_id: ID of the effect to toggle.
|
|
83
|
+
:type effect_id: str
|
|
84
|
+
"""
|
|
85
|
+
if effect_id in self.active:
|
|
86
|
+
self.active.remove(effect_id)
|
|
87
|
+
else:
|
|
88
|
+
self.active.append(effect_id)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CRT screen-space post effect.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Justification: PoC code for v1.
|
|
6
|
+
# pylint: disable=duplicate-code
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from math import sin
|
|
12
|
+
|
|
13
|
+
from mini_arcade_core.backend import Backend
|
|
14
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
15
|
+
from mini_arcade_core.engine.render.effects.base import EffectParams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CRTEffect:
|
|
20
|
+
"""
|
|
21
|
+
CRT screen-space post effect.
|
|
22
|
+
Simulates CRT scanlines with optional wobble.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
effect_id: str = "crt"
|
|
26
|
+
|
|
27
|
+
# Justification: This is PoC code for v1.
|
|
28
|
+
# pylint: disable=too-many-locals
|
|
29
|
+
def apply(self, backend: Backend, ctx: RenderContext) -> None:
|
|
30
|
+
"""Apply the CRT effect to the current render context."""
|
|
31
|
+
vp = ctx.viewport
|
|
32
|
+
x0, y0 = vp.offset_x, vp.offset_y
|
|
33
|
+
w, h = vp.viewport_w, vp.viewport_h
|
|
34
|
+
|
|
35
|
+
stack = ctx.meta.get("effects_stack")
|
|
36
|
+
params: EffectParams = (
|
|
37
|
+
stack.params.get(self.effect_id, EffectParams())
|
|
38
|
+
if stack
|
|
39
|
+
else EffectParams()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
intensity = max(0.0, min(1.0, params.intensity))
|
|
43
|
+
if intensity <= 0.0:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Use a time value from ctx.meta (added in Game.run below)
|
|
47
|
+
t = float(ctx.meta.get("time_s", 0.0))
|
|
48
|
+
wobble = float(params.wobble_speed)
|
|
49
|
+
|
|
50
|
+
# Clip to viewport so it works with all viewport modes/resolutions
|
|
51
|
+
backend.set_clip_rect(x0, y0, w, h)
|
|
52
|
+
|
|
53
|
+
# Scanlines: draw every N lines with low alpha
|
|
54
|
+
# Note: assumes Backend supports alpha in color tuples.
|
|
55
|
+
spacing = 2 # tweakable
|
|
56
|
+
base_alpha = 120 # int(40 * intensity) # subtle
|
|
57
|
+
line_color = (255, 255, 255, base_alpha)
|
|
58
|
+
|
|
59
|
+
# "Wobble": tiny horizontal shift that animates over time
|
|
60
|
+
# Keep it tiny to avoid looking like a bug.
|
|
61
|
+
for y in range(y0, y0 + h, spacing):
|
|
62
|
+
# shift in pixels, -2..2-ish
|
|
63
|
+
shift = int(2.0 * intensity * sin((y * 0.05) + (t * wobble)))
|
|
64
|
+
backend.draw_line(
|
|
65
|
+
x0 + shift, y, x0 + w + shift, y, color=line_color
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
backend.clear_clip_rect()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Screen-space post effects registry.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.effects.base import Effect
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class EffectRegistry:
|
|
14
|
+
"""
|
|
15
|
+
Registry of available screen-space post effects.
|
|
16
|
+
|
|
17
|
+
:ivar _effects: dict[str, Effect]: Internal mapping of effect IDs to effects.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_effects: dict[str, Effect] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def register(self, effect: Effect) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Register a new effect in the registry.
|
|
25
|
+
|
|
26
|
+
:param effect: Effect to register.
|
|
27
|
+
:type effect: Effect
|
|
28
|
+
"""
|
|
29
|
+
self._effects[effect.effect_id] = effect
|
|
30
|
+
|
|
31
|
+
def get(self, effect_id: str) -> Effect | None:
|
|
32
|
+
"""
|
|
33
|
+
Get an effect by its ID.
|
|
34
|
+
|
|
35
|
+
:param effect_id: ID of the effect to retrieve.
|
|
36
|
+
:type effect_id: str
|
|
37
|
+
|
|
38
|
+
:return: Effect instance or None if not found.
|
|
39
|
+
:rtype: Effect | None
|
|
40
|
+
"""
|
|
41
|
+
return self._effects.get(effect_id)
|
|
42
|
+
|
|
43
|
+
def all_ids(self) -> list[str]:
|
|
44
|
+
"""
|
|
45
|
+
Get a list of all registered effect IDs.
|
|
46
|
+
|
|
47
|
+
:return: List of effect IDs.
|
|
48
|
+
:rtype: list[str]
|
|
49
|
+
"""
|
|
50
|
+
return list(self._effects.keys())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vignette noise screen-space post effect.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Justification: PoC code for v1.
|
|
6
|
+
# pylint: disable=duplicate-code
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from mini_arcade_core.backend import Backend
|
|
14
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
15
|
+
from mini_arcade_core.engine.render.effects.base import EffectParams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class VignetteNoiseEffect:
|
|
20
|
+
"""
|
|
21
|
+
Vignette + noise screen-space post effect.
|
|
22
|
+
Simulates a vignette effect with added noise/grain.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
effect_id: str = "vignette_noise"
|
|
26
|
+
|
|
27
|
+
# Justification: This is PoC code for v1.
|
|
28
|
+
# pylint: disable=too-many-locals
|
|
29
|
+
def apply(self, backend: Backend, ctx: RenderContext) -> None:
|
|
30
|
+
"""Apply the Vignette + Noise effect to the current render context."""
|
|
31
|
+
vp = ctx.viewport
|
|
32
|
+
x0, y0 = vp.offset_x, vp.offset_y
|
|
33
|
+
w, h = vp.viewport_w, vp.viewport_h
|
|
34
|
+
|
|
35
|
+
stack = ctx.meta.get("effects_stack")
|
|
36
|
+
params: EffectParams = (
|
|
37
|
+
stack.params.get(self.effect_id, EffectParams())
|
|
38
|
+
if stack
|
|
39
|
+
else EffectParams()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
intensity = max(0.0, min(1.0, params.intensity))
|
|
43
|
+
if intensity <= 0.0:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
backend.set_clip_rect(x0, y0, w, h)
|
|
47
|
+
|
|
48
|
+
# Vignette approximation: draw edge rectangles with increasing alpha.
|
|
49
|
+
# Not a true radial gradient, but good enough for v1.
|
|
50
|
+
steps = 10
|
|
51
|
+
max_alpha = int(110 * intensity) # subtle
|
|
52
|
+
for i in range(steps):
|
|
53
|
+
# thickness grows inward
|
|
54
|
+
t = i + 1
|
|
55
|
+
alpha = int(max_alpha * (t / steps))
|
|
56
|
+
color = (0, 0, 0, alpha)
|
|
57
|
+
|
|
58
|
+
# top
|
|
59
|
+
backend.draw_rect(x0, y0, w, t, color=color)
|
|
60
|
+
# bottom
|
|
61
|
+
backend.draw_rect(x0, y0 + h - t, w, t, color=color)
|
|
62
|
+
# left
|
|
63
|
+
backend.draw_rect(x0, y0, t, h, color=color)
|
|
64
|
+
# right
|
|
65
|
+
backend.draw_rect(x0 + w - t, y0, t, h, color=color)
|
|
66
|
+
|
|
67
|
+
# Noise: sprinkle a few pixels (or tiny 1x1 rects).
|
|
68
|
+
# Use deterministic-ish seed per frame so it doesn't “swim” too much.
|
|
69
|
+
frame = int(ctx.meta.get("frame_index", 0))
|
|
70
|
+
random.seed(frame * 1337)
|
|
71
|
+
|
|
72
|
+
dots = int(200 * intensity) # tweak
|
|
73
|
+
for _ in range(dots):
|
|
74
|
+
px = x0 + random.randint(0, max(0, w - 1))
|
|
75
|
+
py = y0 + random.randint(0, max(0, h - 1))
|
|
76
|
+
a = random.randint(10, int(50 * intensity) + 10)
|
|
77
|
+
backend.draw_rect(px, py, 1, 1, color=(255, 255, 255, a))
|
|
78
|
+
|
|
79
|
+
backend.clear_clip_rect()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Post-processing effects render pass implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.backend import Backend
|
|
10
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
11
|
+
from mini_arcade_core.engine.render.effects.registry import EffectRegistry
|
|
12
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PostFXPass:
|
|
17
|
+
"""
|
|
18
|
+
PostFX Render Pass.
|
|
19
|
+
This pass handles post-processing effects like CRT simulation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str = "PostFXPass"
|
|
23
|
+
registry: EffectRegistry | None = None
|
|
24
|
+
|
|
25
|
+
# Justification: No implementation yet
|
|
26
|
+
# pylint: disable=unused-argument
|
|
27
|
+
def run(
|
|
28
|
+
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
|
|
29
|
+
):
|
|
30
|
+
"""Run the post-processing effects render pass."""
|
|
31
|
+
# Zero overhead path (no effects configured)
|
|
32
|
+
stack = ctx.meta.get("effects_stack")
|
|
33
|
+
if stack is None or not stack.is_active():
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Screen space: no transforms
|
|
37
|
+
backend.clear_viewport_transform()
|
|
38
|
+
backend.clear_clip_rect()
|
|
39
|
+
|
|
40
|
+
reg = self.registry
|
|
41
|
+
if reg is None:
|
|
42
|
+
# no registry => nothing to do
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
for effect_id in list(stack.active):
|
|
46
|
+
effect = reg.get(effect_id)
|
|
47
|
+
if effect is None:
|
|
48
|
+
continue
|
|
49
|
+
effect.apply(backend, ctx)
|
|
File without changes
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Post-processing effects render pass implementation.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
from mini_arcade_core.backend import Backend
|
|
8
|
-
from mini_arcade_core.engine.render.context import RenderContext
|
|
9
|
-
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass
|
|
13
|
-
class PostFXPass:
|
|
14
|
-
"""
|
|
15
|
-
PostFX Render Pass.
|
|
16
|
-
This pass handles post-processing effects like CRT simulation.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
name: str = "PostFXPass"
|
|
20
|
-
|
|
21
|
-
# Justification: No implementation yet
|
|
22
|
-
# pylint: disable=unused-argument
|
|
23
|
-
def run(
|
|
24
|
-
self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
|
|
25
|
-
):
|
|
26
|
-
"""Run the post-processing effects render pass."""
|
|
27
|
-
# hook/no-op for now (CRT later)
|
|
28
|
-
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/__init__.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/context.py
RENAMED
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/frame_packet.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/packet.py
RENAMED
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/ui.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/passes/world.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/engine/render/viewport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/audio/audio_port.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/file/file_adapter.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/file/file_port.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/input/input_port.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/input_frame.py
RENAMED
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/render/render_port.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/scene/scene_port.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/runtime/window/window_port.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/debug_overlay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/scenes/systems/base_system.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/boundaries2d.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/collision2d.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/geometry2d.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/kinematics2d.py
RENAMED
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/spaces/d2/physics2d.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mini_arcade_core-1.1.0 → mini_arcade_core-1.1.1}/src/mini_arcade_core/utils/deprecated_decorator.py
RENAMED
|
File without changes
|
|
File without changes
|