mini-arcade-native-backend 0.5.3__cp310-cp310-win_amd64.whl → 1.0.0__cp310-cp310-win_amd64.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_native_backend/__init__.py +51 -495
- mini_arcade_native_backend/_native.cp310-win_amd64.pyd +0 -0
- mini_arcade_native_backend/_native.pyi +441 -0
- mini_arcade_native_backend/config.py +187 -0
- mini_arcade_native_backend/dlls.py +33 -0
- mini_arcade_native_backend/mapping/__init__.py +0 -0
- mini_arcade_native_backend/mapping/events.py +152 -0
- mini_arcade_native_backend/native_backend.py +127 -0
- mini_arcade_native_backend/ports/__init__.py +0 -0
- mini_arcade_native_backend/ports/audio.py +90 -0
- mini_arcade_native_backend/ports/capture.py +33 -0
- mini_arcade_native_backend/ports/input.py +41 -0
- mini_arcade_native_backend/ports/render.py +134 -0
- mini_arcade_native_backend/ports/text.py +114 -0
- mini_arcade_native_backend/ports/window.py +69 -0
- mini_arcade_native_backend/viewport.py +51 -0
- {mini_arcade_native_backend-0.5.3.dist-info → mini_arcade_native_backend-1.0.0.dist-info}/METADATA +5 -5
- mini_arcade_native_backend-1.0.0.dist-info/RECORD +20 -0
- mini_arcade_native_backend-0.5.3.dist-info/RECORD +0 -6
- {mini_arcade_native_backend-0.5.3.dist-info → mini_arcade_native_backend-1.0.0.dist-info}/WHEEL +0 -0
- {mini_arcade_native_backend-0.5.3.dist-info → mini_arcade_native_backend-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,501 +4,57 @@ mini-arcade native backend package.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Justification: Importing the native extension module
|
|
57
|
-
# pylint: disable=import-self,no-name-in-module
|
|
58
|
-
from . import _native as native
|
|
59
|
-
|
|
60
|
-
# pylint: enable=import-error
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# --- 2) Now import core + define NativeBackend as before ---
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
__all__ = ["NativeBackend", "native"]
|
|
67
|
-
|
|
68
|
-
Alpha = Union[float, int]
|
|
69
|
-
|
|
70
|
-
_NATIVE_TO_CORE = {
|
|
71
|
-
native.EventType.Unknown: EventType.UNKNOWN,
|
|
72
|
-
native.EventType.Quit: EventType.QUIT,
|
|
73
|
-
native.EventType.KeyDown: EventType.KEYDOWN,
|
|
74
|
-
native.EventType.KeyUp: EventType.KEYUP,
|
|
75
|
-
native.EventType.MouseMotion: EventType.MOUSEMOTION,
|
|
76
|
-
native.EventType.MouseButtonDown: EventType.MOUSEBUTTONDOWN,
|
|
77
|
-
native.EventType.MouseButtonUp: EventType.MOUSEBUTTONUP,
|
|
78
|
-
native.EventType.MouseWheel: EventType.MOUSEWHEEL,
|
|
79
|
-
native.EventType.WindowResized: EventType.WINDOWRESIZED,
|
|
80
|
-
native.EventType.TextInput: EventType.TEXTINPUT,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@dataclass
|
|
85
|
-
class BackendSettings:
|
|
86
|
-
"""
|
|
87
|
-
Settings for the NativeBackend.
|
|
88
|
-
|
|
89
|
-
:ivar font_path (Optional[str]): Optional path to a TTF font file to load.
|
|
90
|
-
:ivar font_size (int): Font size in points to use when loading the font.
|
|
91
|
-
:ivar sounds (Optional[dict[str, str]]): Optional dictionary mapping sound IDs to file paths.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
font_path: Optional[str] = None
|
|
95
|
-
font_size: int = 24
|
|
96
|
-
sounds: Optional[dict[str, str]] = None # sound_id -> path
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# TODO: Refactor backend interface into smaller protocols?
|
|
100
|
-
# Justification: Many public methods needed for backend interface
|
|
101
|
-
# pylint: disable=too-many-public-methods,too-many-instance-attributes
|
|
102
|
-
class NativeBackend(Backend):
|
|
103
|
-
"""Adapter that makes the C++ Engine usable as a mini-arcade backend."""
|
|
104
|
-
|
|
105
|
-
def __init__(self, backend_settings: BackendSettings | None = None):
|
|
106
|
-
"""
|
|
107
|
-
:param backend_settings: Optional settings for the backend.
|
|
108
|
-
:type backend_settings: BackendSettings | None
|
|
109
|
-
"""
|
|
110
|
-
self._engine = native.Engine()
|
|
111
|
-
|
|
112
|
-
self._font_path = (
|
|
113
|
-
backend_settings.font_path if backend_settings else None
|
|
114
|
-
)
|
|
115
|
-
self._font_size = (
|
|
116
|
-
backend_settings.font_size if backend_settings else 24
|
|
117
|
-
)
|
|
118
|
-
self._default_font_id: int | None = None
|
|
119
|
-
self._fonts_by_size: dict[int, int] = {}
|
|
120
|
-
|
|
121
|
-
self._sounds = backend_settings.sounds if backend_settings else None
|
|
122
|
-
|
|
123
|
-
self._vp_offset_x = 0
|
|
124
|
-
self._vp_offset_y = 0
|
|
125
|
-
self._vp_scale = 1.0
|
|
126
|
-
|
|
127
|
-
def _get_font_id(self, font_size: int | None) -> int:
|
|
128
|
-
# No font loaded -> keep current “no-op” behavior
|
|
129
|
-
if self._font_path is None:
|
|
130
|
-
return -1
|
|
131
|
-
|
|
132
|
-
# Default font
|
|
133
|
-
if font_size is None:
|
|
134
|
-
return (
|
|
135
|
-
self._default_font_id
|
|
136
|
-
if self._default_font_id is not None
|
|
137
|
-
else -1
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
if font_size <= 0:
|
|
141
|
-
raise ValueError(f"font_size must be > 0, got {font_size}")
|
|
142
|
-
|
|
143
|
-
# Cached
|
|
144
|
-
cached = self._fonts_by_size.get(font_size)
|
|
145
|
-
if cached is not None:
|
|
146
|
-
return cached
|
|
147
|
-
|
|
148
|
-
# Lazily load and cache
|
|
149
|
-
font_id = self._engine.load_font(self._font_path, int(font_size))
|
|
150
|
-
self._fonts_by_size[font_size] = font_id
|
|
151
|
-
return font_id
|
|
152
|
-
|
|
153
|
-
def init(self, window_settings: WindowSettings):
|
|
154
|
-
"""
|
|
155
|
-
Initialize the backend with a window of given width, height, and title.
|
|
156
|
-
|
|
157
|
-
:param window_settings: Settings for the backend window.
|
|
158
|
-
:type window_settings: WindowSettings
|
|
159
|
-
"""
|
|
160
|
-
title = ""
|
|
161
|
-
self._engine.init(window_settings.width, window_settings.height, title)
|
|
162
|
-
|
|
163
|
-
# Load font if provided
|
|
164
|
-
if self._font_path is not None:
|
|
165
|
-
self._default_font_id = self._engine.load_font(
|
|
166
|
-
self._font_path, self._font_size
|
|
167
|
-
)
|
|
168
|
-
self._fonts_by_size[self._font_size] = self._default_font_id
|
|
169
|
-
|
|
170
|
-
# Load sounds if provided
|
|
171
|
-
if self._sounds is not None:
|
|
172
|
-
for sound_id, path in self._sounds.items():
|
|
173
|
-
self.load_sound(sound_id, path)
|
|
174
|
-
|
|
175
|
-
def set_window_title(self, title: str):
|
|
176
|
-
"""
|
|
177
|
-
Set the window title.
|
|
178
|
-
|
|
179
|
-
:param title: Title of the window.
|
|
180
|
-
:type title: str
|
|
181
|
-
"""
|
|
182
|
-
self._engine.set_window_title(title)
|
|
183
|
-
|
|
184
|
-
def set_clear_color(self, r: int, g: int, b: int):
|
|
185
|
-
"""
|
|
186
|
-
Set the background/clear color used by begin_frame.
|
|
187
|
-
|
|
188
|
-
:param r: Red component (0-255).
|
|
189
|
-
:type r: int
|
|
190
|
-
|
|
191
|
-
:param g: Green component (0-255).
|
|
192
|
-
:type g: int
|
|
193
|
-
|
|
194
|
-
:param b: Blue component (0-255).
|
|
195
|
-
:type b: int
|
|
196
|
-
"""
|
|
197
|
-
self._engine.set_clear_color(int(r), int(g), int(b))
|
|
198
|
-
|
|
199
|
-
# Justification: Many local variables needed for event mapping
|
|
200
|
-
# pylint: disable=too-many-locals
|
|
201
|
-
def poll_events(self) -> list[Event]:
|
|
202
|
-
"""
|
|
203
|
-
Poll for events from the backend and return them as a list of Event objects.
|
|
204
|
-
|
|
205
|
-
:return: List of Event objects representing the polled events.
|
|
206
|
-
:rtype: list[Event]
|
|
207
|
-
"""
|
|
208
|
-
out: list[Event] = []
|
|
209
|
-
for ev in self._engine.poll_events():
|
|
210
|
-
etype = _NATIVE_TO_CORE.get(ev.type, EventType.UNKNOWN)
|
|
211
|
-
|
|
212
|
-
key = None
|
|
213
|
-
key_code = None
|
|
214
|
-
scancode = None
|
|
215
|
-
mod = None
|
|
216
|
-
repeat = None
|
|
217
|
-
|
|
218
|
-
x = y = dx = dy = None
|
|
219
|
-
button = None
|
|
220
|
-
wheel = None
|
|
221
|
-
size = None
|
|
222
|
-
text = None
|
|
223
|
-
|
|
224
|
-
if etype in (EventType.KEYDOWN, EventType.KEYUP):
|
|
225
|
-
raw_key = int(getattr(ev, "key", 0) or 0)
|
|
226
|
-
key_code = raw_key if raw_key != 0 else None
|
|
227
|
-
key = SDL_KEYCODE_TO_KEY.get(raw_key) if raw_key != 0 else None
|
|
228
|
-
|
|
229
|
-
scancode = (
|
|
230
|
-
int(ev.scancode) if getattr(ev, "scancode", 0) else None
|
|
231
|
-
)
|
|
232
|
-
mod = int(ev.mod) if getattr(ev, "mod", 0) else None
|
|
233
|
-
|
|
234
|
-
rep = int(getattr(ev, "repeat", 0) or 0)
|
|
235
|
-
repeat = bool(rep) if etype == EventType.KEYDOWN else None
|
|
236
|
-
|
|
237
|
-
elif etype == EventType.MOUSEMOTION:
|
|
238
|
-
x = int(ev.x)
|
|
239
|
-
y = int(ev.y)
|
|
240
|
-
dx = int(ev.dx)
|
|
241
|
-
dy = int(ev.dy)
|
|
242
|
-
|
|
243
|
-
elif etype in (EventType.MOUSEBUTTONDOWN, EventType.MOUSEBUTTONUP):
|
|
244
|
-
button = int(ev.button) if ev.button else None
|
|
245
|
-
x = int(ev.x)
|
|
246
|
-
y = int(ev.y)
|
|
247
|
-
|
|
248
|
-
elif etype == EventType.MOUSEWHEEL:
|
|
249
|
-
wx = int(ev.wheel_x)
|
|
250
|
-
wy = int(ev.wheel_y)
|
|
251
|
-
wheel = (wx, wy) if (wx or wy) else None
|
|
252
|
-
|
|
253
|
-
elif etype == EventType.WINDOWRESIZED:
|
|
254
|
-
w = int(ev.width)
|
|
255
|
-
h = int(ev.height)
|
|
256
|
-
size = (w, h) if (w and h) else None
|
|
257
|
-
|
|
258
|
-
elif etype == EventType.TEXTINPUT:
|
|
259
|
-
t = getattr(ev, "text", "")
|
|
260
|
-
text = t if t else None
|
|
261
|
-
|
|
262
|
-
out.append(
|
|
263
|
-
Event(
|
|
264
|
-
type=etype,
|
|
265
|
-
key=key,
|
|
266
|
-
key_code=key_code,
|
|
267
|
-
scancode=scancode,
|
|
268
|
-
mod=mod,
|
|
269
|
-
repeat=repeat,
|
|
270
|
-
x=x,
|
|
271
|
-
y=y,
|
|
272
|
-
dx=dx,
|
|
273
|
-
dy=dy,
|
|
274
|
-
button=button,
|
|
275
|
-
wheel=wheel,
|
|
276
|
-
size=size,
|
|
277
|
-
text=text,
|
|
278
|
-
)
|
|
279
|
-
)
|
|
280
|
-
return out
|
|
281
|
-
|
|
282
|
-
# pylint: enable=too-many-locals
|
|
283
|
-
|
|
284
|
-
def begin_frame(self):
|
|
285
|
-
"""Begin a new frame for rendering."""
|
|
286
|
-
self._engine.begin_frame()
|
|
287
|
-
|
|
288
|
-
def end_frame(self):
|
|
289
|
-
"""End the current frame for rendering."""
|
|
290
|
-
self._engine.end_frame()
|
|
291
|
-
|
|
292
|
-
@staticmethod
|
|
293
|
-
def _alpha_to_u8(alpha: Alpha | None) -> int:
|
|
294
|
-
"""Convert CSS-like alpha (0..1) to uint8 (0..255)."""
|
|
295
|
-
if alpha is None:
|
|
296
|
-
return 255
|
|
297
|
-
|
|
298
|
-
# disallow booleans (since bool is a subclass of int)
|
|
299
|
-
if isinstance(alpha, bool):
|
|
300
|
-
raise TypeError("alpha must be a float in [0,1], not bool")
|
|
301
|
-
|
|
302
|
-
a = float(alpha)
|
|
303
|
-
|
|
304
|
-
# Enforce “percentage only”
|
|
305
|
-
if a < 0.0 or a > 1.0:
|
|
306
|
-
raise ValueError(f"alpha must be in [0, 1], got {alpha!r}")
|
|
307
|
-
|
|
308
|
-
return int(round(a * 255))
|
|
309
|
-
|
|
310
|
-
@staticmethod
|
|
311
|
-
def _get_color_values(color: tuple[int, ...]) -> int:
|
|
312
|
-
"""
|
|
313
|
-
Extract alpha value from color tuple (r,g,b) or (r,g,b,a).
|
|
314
|
-
If missing, returns default.
|
|
315
|
-
"""
|
|
316
|
-
if len(color) == 3:
|
|
317
|
-
r, g, b = color
|
|
318
|
-
a_u8 = 255
|
|
319
|
-
elif len(color) == 4:
|
|
320
|
-
r, g, b, a = color
|
|
321
|
-
a_u8 = NativeBackend._alpha_to_u8(a)
|
|
322
|
-
else:
|
|
323
|
-
raise ValueError(
|
|
324
|
-
f"Color must be (r,g,b) or (r,g,b,a), got {color!r}"
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
return (int(r), int(g), int(b), a_u8)
|
|
328
|
-
|
|
329
|
-
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
330
|
-
def draw_rect(
|
|
331
|
-
self,
|
|
332
|
-
x: int,
|
|
333
|
-
y: int,
|
|
334
|
-
w: int,
|
|
335
|
-
h: int,
|
|
336
|
-
color: tuple[int, ...] = (255, 255, 255),
|
|
337
|
-
):
|
|
338
|
-
"""
|
|
339
|
-
Draw a rectangle at the specified position with given width and height.
|
|
340
|
-
|
|
341
|
-
:param x: X coordinate of the rectangle's top-left corner.
|
|
342
|
-
:type x: int
|
|
343
|
-
|
|
344
|
-
:param y: Y coordinate of the rectangle's top-left corner.
|
|
345
|
-
:type y: int
|
|
346
|
-
|
|
347
|
-
:param w: Width of the rectangle.
|
|
348
|
-
:type w: int
|
|
349
|
-
|
|
350
|
-
:param h: Height of the rectangle.
|
|
351
|
-
:type h: int
|
|
352
|
-
|
|
353
|
-
:param color: Color of the rectangle as (r, g, b) or (r, g, b, a).
|
|
354
|
-
:type color: tuple[int, ...]
|
|
355
|
-
"""
|
|
356
|
-
r, g, b, a = self._get_color_values(color)
|
|
357
|
-
sx = int(round(self._vp_offset_x + x * self._vp_scale)) # top-left x
|
|
358
|
-
sy = int(round(self._vp_offset_y + y * self._vp_scale)) # top-left y
|
|
359
|
-
sw = int(round(w * self._vp_scale)) # width
|
|
360
|
-
sh = int(round(h * self._vp_scale)) # height
|
|
361
|
-
self._engine.draw_rect(sx, sy, sw, sh, r, g, b, a)
|
|
362
|
-
# self._engine.draw_rect(x, y, w, h, r, g, b, a)
|
|
363
|
-
|
|
364
|
-
def draw_text(
|
|
365
|
-
self,
|
|
366
|
-
x: int,
|
|
367
|
-
y: int,
|
|
368
|
-
text: str,
|
|
369
|
-
color: tuple[int, int, int] = (255, 255, 255),
|
|
370
|
-
font_size: int | None = None,
|
|
371
|
-
):
|
|
372
|
-
"""
|
|
373
|
-
Draw text at the given position using the loaded font.
|
|
374
|
-
If no font is loaded, this is a no-op.
|
|
375
|
-
|
|
376
|
-
:param x: X coordinate for the text position.
|
|
377
|
-
:type x: int
|
|
378
|
-
|
|
379
|
-
:param y: Y coordinate for the text position.
|
|
380
|
-
:type y: int
|
|
381
|
-
|
|
382
|
-
:param text: The text string to draw.
|
|
383
|
-
:type text: str
|
|
384
|
-
|
|
385
|
-
:param color: Color of the text as (r, g, b).
|
|
386
|
-
:type color: tuple[int, int, int]
|
|
387
|
-
"""
|
|
388
|
-
r, g, b, a = self._get_color_values(color)
|
|
389
|
-
font_id = self._get_font_id(font_size)
|
|
390
|
-
sx = int(round(self._vp_offset_x + x * self._vp_scale))
|
|
391
|
-
sy = int(round(self._vp_offset_y + y * self._vp_scale))
|
|
392
|
-
|
|
393
|
-
# optional but recommended: scale font size too
|
|
394
|
-
if font_size is not None:
|
|
395
|
-
scaled = max(8, int(round(font_size * self._vp_scale)))
|
|
396
|
-
else:
|
|
397
|
-
scaled = None
|
|
398
|
-
|
|
399
|
-
font_id = self._get_font_id(scaled)
|
|
400
|
-
self._engine.draw_text(
|
|
401
|
-
text, sx, sy, int(r), int(g), int(b), int(a), font_id
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from .dlls import setup_windows_dll_search_paths
|
|
10
|
+
|
|
11
|
+
setup_windows_dll_search_paths()
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
# Justification: Need to import core after setting DLL path on Windows
|
|
15
|
+
# pylint: disable=wrong-import-position
|
|
16
|
+
from .config import (
|
|
17
|
+
AudioSettings,
|
|
18
|
+
BackendSettings,
|
|
19
|
+
FontSettings,
|
|
20
|
+
RendererSettings,
|
|
21
|
+
WindowSettings,
|
|
22
|
+
)
|
|
23
|
+
from .native_backend import NativeBackend
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"NativeBackend",
|
|
27
|
+
"BackendSettings",
|
|
28
|
+
"WindowSettings",
|
|
29
|
+
"RendererSettings",
|
|
30
|
+
"FontSettings",
|
|
31
|
+
"AudioSettings",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# NOTE: Momentary __getattr__ to avoid circular imports for type hints
|
|
36
|
+
# pylint: disable=import-outside-toplevel,possibly-unused-variable
|
|
37
|
+
def __getattr__(name: str):
|
|
38
|
+
if name == "NativeBackend":
|
|
39
|
+
from .native_backend import NativeBackend
|
|
40
|
+
|
|
41
|
+
return NativeBackend
|
|
42
|
+
|
|
43
|
+
if name in {
|
|
44
|
+
"AudioSettings",
|
|
45
|
+
"BackendSettings",
|
|
46
|
+
"FontSettings",
|
|
47
|
+
"RendererSettings",
|
|
48
|
+
"WindowSettings",
|
|
49
|
+
}:
|
|
50
|
+
from .config import (
|
|
51
|
+
AudioSettings,
|
|
52
|
+
BackendSettings,
|
|
53
|
+
FontSettings,
|
|
54
|
+
RendererSettings,
|
|
55
|
+
WindowSettings,
|
|
402
56
|
)
|
|
403
|
-
# self._engine.draw_text(
|
|
404
|
-
# text, x, y, int(r), int(g), int(b), int(a), font_id
|
|
405
|
-
# )
|
|
406
|
-
|
|
407
|
-
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
408
|
-
|
|
409
|
-
def capture_frame(self, path: str | None = None) -> bool:
|
|
410
|
-
"""
|
|
411
|
-
Capture the current frame.
|
|
412
|
-
|
|
413
|
-
:param path: Optional file path to save the captured frame (e.g., PNG).
|
|
414
|
-
:type path: str | None
|
|
415
|
-
|
|
416
|
-
:return: True if the frame was successfully captured (and saved if path provided),
|
|
417
|
-
False otherwise.
|
|
418
|
-
:rtype: bool
|
|
419
|
-
"""
|
|
420
|
-
if path is None:
|
|
421
|
-
raise ValueError("Path must be provided to capture frame.")
|
|
422
|
-
return self._engine.capture_frame(path)
|
|
423
|
-
|
|
424
|
-
def measure_text(
|
|
425
|
-
self, text: str, font_size: int | None = None
|
|
426
|
-
) -> tuple[int, int]:
|
|
427
|
-
"""
|
|
428
|
-
Measure text size (width, height) in pixels for the active font.
|
|
429
|
-
|
|
430
|
-
Returns (0,0) if no font is loaded (matches draw_text no-op behavior).
|
|
431
|
-
"""
|
|
432
|
-
font_id = self._get_font_id(font_size)
|
|
433
|
-
w, h = self._engine.measure_text(text, font_id)
|
|
434
|
-
return int(w), int(h)
|
|
435
|
-
|
|
436
|
-
def init_audio(
|
|
437
|
-
self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
|
|
438
|
-
):
|
|
439
|
-
"""Initialize SDL_mixer audio."""
|
|
440
|
-
self._engine.init_audio(int(frequency), int(channels), int(chunk_size))
|
|
441
|
-
|
|
442
|
-
def shutdown_audio(self):
|
|
443
|
-
"""Shutdown SDL_mixer audio and free loaded sounds."""
|
|
444
|
-
self._engine.shutdown_audio()
|
|
445
|
-
|
|
446
|
-
def load_sound(self, sound_id: str, path: str):
|
|
447
|
-
"""
|
|
448
|
-
Load a WAV sound and store it by ID.
|
|
449
|
-
Example: backend.load_sound("hit", "assets/sfx/hit.wav")
|
|
450
|
-
"""
|
|
451
|
-
if not sound_id:
|
|
452
|
-
raise ValueError("sound_id cannot be empty")
|
|
453
|
-
|
|
454
|
-
p = Path(path)
|
|
455
|
-
if not p.exists():
|
|
456
|
-
raise FileNotFoundError(f"Sound file not found: {p}")
|
|
457
|
-
|
|
458
|
-
self._engine.load_sound(sound_id, str(p))
|
|
459
|
-
|
|
460
|
-
def play_sound(self, sound_id: str, loops: int = 0):
|
|
461
|
-
"""
|
|
462
|
-
Play a loaded sound.
|
|
463
|
-
loops=0 => play once
|
|
464
|
-
loops=-1 => infinite loop
|
|
465
|
-
loops=1 => play twice (SDL convention)
|
|
466
|
-
"""
|
|
467
|
-
self._engine.play_sound(sound_id, int(loops))
|
|
468
|
-
|
|
469
|
-
def set_master_volume(self, volume: int):
|
|
470
|
-
"""
|
|
471
|
-
Master volume: 0..128
|
|
472
|
-
"""
|
|
473
|
-
self._engine.set_master_volume(int(volume))
|
|
474
|
-
|
|
475
|
-
def set_sound_volume(self, sound_id: str, volume: int):
|
|
476
|
-
"""
|
|
477
|
-
Per-sound volume: 0..128
|
|
478
|
-
"""
|
|
479
|
-
self._engine.set_sound_volume(sound_id, int(volume))
|
|
480
|
-
|
|
481
|
-
def stop_all_sounds(self):
|
|
482
|
-
"""Stop all channels."""
|
|
483
|
-
self._engine.stop_all_sounds()
|
|
484
|
-
|
|
485
|
-
def set_viewport_transform(
|
|
486
|
-
self, offset_x: int, offset_y: int, scale: float
|
|
487
|
-
) -> None:
|
|
488
|
-
self._vp_offset_x = int(offset_x)
|
|
489
|
-
self._vp_offset_y = int(offset_y)
|
|
490
|
-
self._vp_scale = float(scale)
|
|
491
|
-
|
|
492
|
-
def clear_viewport_transform(self) -> None:
|
|
493
|
-
self._vp_offset_x = 0
|
|
494
|
-
self._vp_offset_y = 0
|
|
495
|
-
self._vp_scale = 1.0
|
|
496
|
-
|
|
497
|
-
def resize_window(self, width: int, height: int) -> None:
|
|
498
|
-
self._engine.resize_window(int(width), int(height))
|
|
499
57
|
|
|
500
|
-
|
|
501
|
-
self._engine.set_clip_rect(int(x), int(y), int(w), int(h))
|
|
58
|
+
return locals()[name]
|
|
502
59
|
|
|
503
|
-
|
|
504
|
-
self._engine.clear_clip_rect()
|
|
60
|
+
raise AttributeError(name)
|
|
Binary file
|