mini-arcade-native-backend 0.5.0__cp310-cp310-win_amd64.whl → 0.6.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.
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import os
8
8
  import sys
9
+ from dataclasses import dataclass
9
10
  from pathlib import Path
10
11
  from typing import Optional, Union
11
12
 
@@ -42,6 +43,7 @@ if sys.platform == "win32":
42
43
  # pylint: disable=import-error
43
44
  from mini_arcade_core.backend import ( # pyright: ignore[reportMissingImports]
44
45
  Backend,
46
+ WindowSettings,
45
47
  )
46
48
  from mini_arcade_core.backend.events import ( # pyright: ignore[reportMissingImports]
47
49
  Event,
@@ -79,23 +81,49 @@ _NATIVE_TO_CORE = {
79
81
  }
80
82
 
81
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
82
102
  class NativeBackend(Backend):
83
103
  """Adapter that makes the C++ Engine usable as a mini-arcade backend."""
84
104
 
85
- def __init__(self, font_path: str | None = None, font_size: int = 24):
105
+ def __init__(self, backend_settings: BackendSettings | None = None):
86
106
  """
87
- :param font_path: Optional path to a TTF font file to load.
88
- :type font_path: str | None
89
-
90
- :param font_size: Font size in points to use when loading the font.
91
- :type font_size: int
107
+ :param backend_settings: Optional settings for the backend.
108
+ :type backend_settings: BackendSettings | None
92
109
  """
93
110
  self._engine = native.Engine()
94
- self._font_path = font_path
95
- self._font_size = font_size
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
+ )
96
118
  self._default_font_id: int | None = None
97
119
  self._fonts_by_size: dict[int, int] = {}
98
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
+
99
127
  def _get_font_id(self, font_size: int | None) -> int:
100
128
  # No font loaded -> keep current “no-op” behavior
101
129
  if self._font_path is None:
@@ -122,22 +150,15 @@ class NativeBackend(Backend):
122
150
  self._fonts_by_size[font_size] = font_id
123
151
  return font_id
124
152
 
125
- def init(self, width: int, height: int, title: Optional[str] = None):
153
+ def init(self, window_settings: WindowSettings):
126
154
  """
127
155
  Initialize the backend with a window of given width, height, and title.
128
156
 
129
- :param width: Width of the window in pixels.
130
- :type width: int
131
-
132
- :param height: Height of the window in pixels.
133
- :type height: int
134
-
135
- :param title: Title of the window.
136
- :type title: Optional[str]
157
+ :param window_settings: Settings for the backend window.
158
+ :type window_settings: WindowSettings
137
159
  """
138
- if title is None:
139
- title = ""
140
- self._engine.init(width, height, title)
160
+ title = ""
161
+ self._engine.init(window_settings.width, window_settings.height, title)
141
162
 
142
163
  # Load font if provided
143
164
  if self._font_path is not None:
@@ -146,6 +167,11 @@ class NativeBackend(Backend):
146
167
  )
147
168
  self._fonts_by_size[self._font_size] = self._default_font_id
148
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
+
149
175
  def set_window_title(self, title: str):
150
176
  """
151
177
  Set the window title.
@@ -273,12 +299,18 @@ class NativeBackend(Backend):
273
299
  if isinstance(alpha, bool):
274
300
  raise TypeError("alpha must be a float in [0,1], not bool")
275
301
 
276
- a = float(alpha)
302
+ # If it's an int-like value, treat as 0..255
303
+ if isinstance(alpha, int):
304
+ if alpha < 0 or alpha > 255:
305
+ raise ValueError(
306
+ f"int alpha must be in [0, 255], got {alpha!r}"
307
+ )
308
+ return int(alpha)
277
309
 
278
- # Enforce “percentage only”
310
+ # Otherwise treat as float 0..1
311
+ a = float(alpha)
279
312
  if a < 0.0 or a > 1.0:
280
- raise ValueError(f"alpha must be in [0, 1], got {alpha!r}")
281
-
313
+ raise ValueError(f"float alpha must be in [0, 1], got {alpha!r}")
282
314
  return int(round(a * 255))
283
315
 
284
316
  @staticmethod
@@ -328,7 +360,12 @@ class NativeBackend(Backend):
328
360
  :type color: tuple[int, ...]
329
361
  """
330
362
  r, g, b, a = self._get_color_values(color)
331
- self._engine.draw_rect(x, y, w, h, r, g, b, a)
363
+ sx = int(round(self._vp_offset_x + x * self._vp_scale)) # top-left x
364
+ sy = int(round(self._vp_offset_y + y * self._vp_scale)) # top-left y
365
+ sw = int(round(w * self._vp_scale)) # width
366
+ sh = int(round(h * self._vp_scale)) # height
367
+ self._engine.draw_rect(sx, sy, sw, sh, r, g, b, a)
368
+ # self._engine.draw_rect(x, y, w, h, r, g, b, a)
332
369
 
333
370
  def draw_text(
334
371
  self,
@@ -356,9 +393,22 @@ class NativeBackend(Backend):
356
393
  """
357
394
  r, g, b, a = self._get_color_values(color)
358
395
  font_id = self._get_font_id(font_size)
396
+ sx = int(round(self._vp_offset_x + x * self._vp_scale))
397
+ sy = int(round(self._vp_offset_y + y * self._vp_scale))
398
+
399
+ # optional but recommended: scale font size too
400
+ if font_size is not None:
401
+ scaled = max(8, int(round(font_size * self._vp_scale)))
402
+ else:
403
+ scaled = None
404
+
405
+ font_id = self._get_font_id(scaled)
359
406
  self._engine.draw_text(
360
- text, x, y, int(r), int(g), int(b), int(a), font_id
407
+ text, sx, sy, int(r), int(g), int(b), int(a), font_id
361
408
  )
409
+ # self._engine.draw_text(
410
+ # text, x, y, int(r), int(g), int(b), int(a), font_id
411
+ # )
362
412
 
363
413
  # pylint: enable=too-many-arguments,too-many-positional-arguments
364
414
 
@@ -388,3 +438,97 @@ class NativeBackend(Backend):
388
438
  font_id = self._get_font_id(font_size)
389
439
  w, h = self._engine.measure_text(text, font_id)
390
440
  return int(w), int(h)
441
+
442
+ def init_audio(
443
+ self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
444
+ ):
445
+ """Initialize SDL_mixer audio."""
446
+ self._engine.init_audio(int(frequency), int(channels), int(chunk_size))
447
+
448
+ def shutdown_audio(self):
449
+ """Shutdown SDL_mixer audio and free loaded sounds."""
450
+ self._engine.shutdown_audio()
451
+
452
+ def load_sound(self, sound_id: str, path: str):
453
+ """
454
+ Load a WAV sound and store it by ID.
455
+ Example: backend.load_sound("hit", "assets/sfx/hit.wav")
456
+ """
457
+ if not sound_id:
458
+ raise ValueError("sound_id cannot be empty")
459
+
460
+ p = Path(path)
461
+ if not p.exists():
462
+ raise FileNotFoundError(f"Sound file not found: {p}")
463
+
464
+ self._engine.load_sound(sound_id, str(p))
465
+
466
+ def play_sound(self, sound_id: str, loops: int = 0):
467
+ """
468
+ Play a loaded sound.
469
+ loops=0 => play once
470
+ loops=-1 => infinite loop
471
+ loops=1 => play twice (SDL convention)
472
+ """
473
+ self._engine.play_sound(sound_id, int(loops))
474
+
475
+ def set_master_volume(self, volume: int):
476
+ """
477
+ Master volume: 0..128
478
+ """
479
+ self._engine.set_master_volume(int(volume))
480
+
481
+ def set_sound_volume(self, sound_id: str, volume: int):
482
+ """
483
+ Per-sound volume: 0..128
484
+ """
485
+ self._engine.set_sound_volume(sound_id, int(volume))
486
+
487
+ def stop_all_sounds(self):
488
+ """Stop all channels."""
489
+ self._engine.stop_all_sounds()
490
+
491
+ def set_viewport_transform(
492
+ self, offset_x: int, offset_y: int, scale: float
493
+ ) -> None:
494
+ self._vp_offset_x = int(offset_x)
495
+ self._vp_offset_y = int(offset_y)
496
+ self._vp_scale = float(scale)
497
+
498
+ def clear_viewport_transform(self) -> None:
499
+ self._vp_offset_x = 0
500
+ self._vp_offset_y = 0
501
+ self._vp_scale = 1.0
502
+
503
+ def resize_window(self, width: int, height: int) -> None:
504
+ self._engine.resize_window(int(width), int(height))
505
+
506
+ def set_clip_rect(self, x: int, y: int, w: int, h: int) -> None:
507
+ self._engine.set_clip_rect(int(x), int(y), int(w), int(h))
508
+
509
+ def clear_clip_rect(self) -> None:
510
+ self._engine.clear_clip_rect()
511
+
512
+ # Justification: Many arguments needed for line drawing
513
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
514
+ def draw_line(
515
+ self,
516
+ x1: int,
517
+ y1: int,
518
+ x2: int,
519
+ y2: int,
520
+ color: tuple[int, ...] = (255, 255, 255),
521
+ ) -> None:
522
+ r, g, b, a = self._get_color_values(color)
523
+
524
+ sx1 = int(round(self._vp_offset_x + x1 * self._vp_scale))
525
+ sy1 = int(round(self._vp_offset_y + y1 * self._vp_scale))
526
+ sx2 = int(round(self._vp_offset_x + x2 * self._vp_scale))
527
+ sy2 = int(round(self._vp_offset_y + y2 * self._vp_scale))
528
+
529
+ self._engine.draw_line(
530
+ sx1, sy1, sx2, sy2, int(r), int(g), int(b), int(a)
531
+ )
532
+
533
+
534
+ # pylint: enable=too-many-arguments,too-many-positional-arguments
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mini-arcade-native-backend
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Native SDL2 backend for mini-arcade-core using SDL2 + pybind11.
5
5
  Author-Email: Santiago Rincon <rincores@gmail.com>
6
6
  License: Copyright (c) 2025 Santiago Rincón
@@ -24,7 +24,7 @@ License: Copyright (c) 2025 Santiago Rincón
24
24
  SOFTWARE.
25
25
 
26
26
  Requires-Python: <3.12,>=3.9
27
- Requires-Dist: mini-arcade-core~=1.0
27
+ Requires-Dist: mini-arcade-core~=1.1
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest~=8.3; extra == "dev"
30
30
  Requires-Dist: pytest-cov~=6.0; extra == "dev"
@@ -0,0 +1,6 @@
1
+ mini_arcade_native_backend/__init__.py,sha256=olqX3fkFRvLndg0rAeCPmpxM2EXd2-tLpVlkc2_gSIw,17911
2
+ mini_arcade_native_backend/_native.cp310-win_amd64.pyd,sha256=Ut7acuvW5Vrg40sb12xH-N7Rt4ud5FuWMzocHMePQgk,228352
3
+ mini_arcade_native_backend-0.6.0.dist-info/METADATA,sha256=7RyY7NLksROfXcHxhMHbq9bauVSvJAtnmVKdVU1T2eI,10517
4
+ mini_arcade_native_backend-0.6.0.dist-info/WHEEL,sha256=hrGeChGtn46HBGmzasO9QQDSLelRN-tUarBSv4gFcsI,106
5
+ mini_arcade_native_backend-0.6.0.dist-info/licenses/LICENSE,sha256=cZRgTdRJ3YASekMxkGAvylB2nROh4ov228DxAogK3yY,1115
6
+ mini_arcade_native_backend-0.6.0.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- mini_arcade_native_backend/__init__.py,sha256=CtzaxGTyVhLBVDB6naaqR0LCCz3CSuUpJP2SXQGCz64,12704
2
- mini_arcade_native_backend/_native.cp310-win_amd64.pyd,sha256=yNkXT_iRcmgq792R_JD1B56_e_6_i9vDJkmwRU8nQ9g,213504
3
- mini_arcade_native_backend-0.5.0.dist-info/METADATA,sha256=BrvJBIkrJ0pxAxOSm1yV9_Ga88JFpOMB6G7mUgRSaZY,10517
4
- mini_arcade_native_backend-0.5.0.dist-info/WHEEL,sha256=hrGeChGtn46HBGmzasO9QQDSLelRN-tUarBSv4gFcsI,106
5
- mini_arcade_native_backend-0.5.0.dist-info/licenses/LICENSE,sha256=cZRgTdRJ3YASekMxkGAvylB2nROh4ov228DxAogK3yY,1115
6
- mini_arcade_native_backend-0.5.0.dist-info/RECORD,,