mini-arcade-core 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mini_arcade_core/__init__.py +100 -0
- mini_arcade_core/backend.py +225 -0
- mini_arcade_core/boundaries2d.py +107 -0
- mini_arcade_core/collision2d.py +71 -0
- mini_arcade_core/entity.py +68 -0
- mini_arcade_core/game.py +243 -0
- mini_arcade_core/geometry2d.py +69 -0
- mini_arcade_core/kinematics2d.py +78 -0
- mini_arcade_core/physics2d.py +73 -0
- mini_arcade_core/scene.py +147 -0
- mini_arcade_core/ui/__init__.py +0 -0
- mini_arcade_core/ui/menu.py +135 -0
- mini_arcade_core-0.9.0.dist-info/METADATA +288 -0
- mini_arcade_core-0.9.0.dist-info/RECORD +16 -0
- mini_arcade_core-0.9.0.dist-info/WHEEL +4 -0
- mini_arcade_core-0.9.0.dist-info/licenses/LICENSE +19 -0
mini_arcade_core/game.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from time import perf_counter, sleep
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from PIL import Image # type: ignore[import]
|
|
15
|
+
|
|
16
|
+
from .backend import Backend
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # avoid runtime circular import
|
|
19
|
+
from .scene import Scene
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class GameConfig:
|
|
24
|
+
"""
|
|
25
|
+
Configuration options for the Game.
|
|
26
|
+
|
|
27
|
+
:ivar width: Width of the game window in pixels.
|
|
28
|
+
:ivar height: Height of the game window in pixels.
|
|
29
|
+
:ivar title: Title of the game window.
|
|
30
|
+
:ivar fps: Target frames per second.
|
|
31
|
+
:ivar background_color: RGB background color.
|
|
32
|
+
:ivar backend: Optional Backend instance to use for rendering and input.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
width: int = 800
|
|
36
|
+
height: int = 600
|
|
37
|
+
title: str = "Mini Arcade Game"
|
|
38
|
+
fps: int = 60
|
|
39
|
+
background_color: tuple[int, int, int] = (0, 0, 0)
|
|
40
|
+
backend: Backend | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class _StackEntry:
|
|
45
|
+
scene: "Scene"
|
|
46
|
+
as_overlay: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Game:
|
|
50
|
+
"""Core game object responsible for managing the main loop and active scene."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: GameConfig):
|
|
53
|
+
"""
|
|
54
|
+
:param config: Game configuration options.
|
|
55
|
+
:type config: GameConfig
|
|
56
|
+
|
|
57
|
+
:raises ValueError: If the provided config does not have a valid Backend.
|
|
58
|
+
"""
|
|
59
|
+
self.config = config
|
|
60
|
+
self._current_scene: Scene | None = None
|
|
61
|
+
self._running: bool = False
|
|
62
|
+
|
|
63
|
+
if config.backend is None:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"GameConfig.backend must be set to a Backend instance"
|
|
66
|
+
)
|
|
67
|
+
self.backend: Backend = config.backend
|
|
68
|
+
self._scene_stack: list[_StackEntry] = []
|
|
69
|
+
|
|
70
|
+
def current_scene(self) -> "Scene | None":
|
|
71
|
+
"""
|
|
72
|
+
Get the currently active scene.
|
|
73
|
+
|
|
74
|
+
:return: The active Scene instance, or None if no scene is active.
|
|
75
|
+
:rtype: Scene | None
|
|
76
|
+
"""
|
|
77
|
+
return self._scene_stack[-1].scene if self._scene_stack else None
|
|
78
|
+
|
|
79
|
+
def change_scene(self, scene: Scene):
|
|
80
|
+
"""
|
|
81
|
+
Swap the active scene. Concrete implementations should call
|
|
82
|
+
``on_exit``/``on_enter`` appropriately.
|
|
83
|
+
|
|
84
|
+
:param scene: The new scene to activate.
|
|
85
|
+
:type scene: Scene
|
|
86
|
+
"""
|
|
87
|
+
while self._scene_stack:
|
|
88
|
+
entry = self._scene_stack.pop()
|
|
89
|
+
entry.scene.on_exit()
|
|
90
|
+
|
|
91
|
+
self._scene_stack.append(_StackEntry(scene=scene, as_overlay=False))
|
|
92
|
+
scene.on_enter()
|
|
93
|
+
|
|
94
|
+
def push_scene(self, scene: "Scene", as_overlay: bool = False):
|
|
95
|
+
"""
|
|
96
|
+
Push a scene on top of the current one.
|
|
97
|
+
If as_overlay=True, underlying scene(s) may still be drawn but never updated.
|
|
98
|
+
"""
|
|
99
|
+
top = self.current_scene()
|
|
100
|
+
if top is not None:
|
|
101
|
+
top.on_pause()
|
|
102
|
+
|
|
103
|
+
self._scene_stack.append(
|
|
104
|
+
_StackEntry(scene=scene, as_overlay=as_overlay)
|
|
105
|
+
)
|
|
106
|
+
scene.on_enter()
|
|
107
|
+
|
|
108
|
+
def pop_scene(self) -> "Scene | None":
|
|
109
|
+
"""Pop the top scene. If stack becomes empty, quit."""
|
|
110
|
+
if not self._scene_stack:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
popped = self._scene_stack.pop()
|
|
114
|
+
popped.scene.on_exit()
|
|
115
|
+
|
|
116
|
+
top = self.current_scene()
|
|
117
|
+
if top is None:
|
|
118
|
+
self.quit()
|
|
119
|
+
return popped.scene
|
|
120
|
+
|
|
121
|
+
top.on_resume()
|
|
122
|
+
return popped.scene
|
|
123
|
+
|
|
124
|
+
def _visible_stack(self) -> list["Scene"]:
|
|
125
|
+
"""
|
|
126
|
+
Return the list of scenes that should be drawn (base + overlays).
|
|
127
|
+
We draw from the top-most non-overlay scene upward.
|
|
128
|
+
"""
|
|
129
|
+
if not self._scene_stack:
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
# find top-most base scene (as_overlay=False)
|
|
133
|
+
base_idx = 0
|
|
134
|
+
for i in range(len(self._scene_stack) - 1, -1, -1):
|
|
135
|
+
if not self._scene_stack[i].as_overlay:
|
|
136
|
+
base_idx = i
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
return [e.scene for e in self._scene_stack[base_idx:]]
|
|
140
|
+
|
|
141
|
+
def quit(self):
|
|
142
|
+
"""Request that the main loop stops."""
|
|
143
|
+
self._running = False
|
|
144
|
+
|
|
145
|
+
def run(self, initial_scene: Scene):
|
|
146
|
+
"""
|
|
147
|
+
Run the main loop starting with the given scene.
|
|
148
|
+
|
|
149
|
+
This is intentionally left abstract so you can plug pygame, pyglet,
|
|
150
|
+
or another backend.
|
|
151
|
+
|
|
152
|
+
:param initial_scene: The scene to start the game with.
|
|
153
|
+
:type initial_scene: Scene
|
|
154
|
+
"""
|
|
155
|
+
backend = self.backend
|
|
156
|
+
backend.init(self.config.width, self.config.height, self.config.title)
|
|
157
|
+
|
|
158
|
+
br, bg, bb = self.config.background_color
|
|
159
|
+
backend.set_clear_color(br, bg, bb)
|
|
160
|
+
|
|
161
|
+
self.change_scene(initial_scene)
|
|
162
|
+
|
|
163
|
+
self._running = True
|
|
164
|
+
target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
|
|
165
|
+
last_time = perf_counter()
|
|
166
|
+
|
|
167
|
+
while self._running:
|
|
168
|
+
now = perf_counter()
|
|
169
|
+
dt = now - last_time
|
|
170
|
+
last_time = now
|
|
171
|
+
|
|
172
|
+
top = self.current_scene()
|
|
173
|
+
if top is None:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
for ev in backend.poll_events():
|
|
177
|
+
top.handle_event(ev)
|
|
178
|
+
|
|
179
|
+
top.update(dt)
|
|
180
|
+
|
|
181
|
+
backend.begin_frame()
|
|
182
|
+
for scene in self._visible_stack():
|
|
183
|
+
scene.draw(backend)
|
|
184
|
+
backend.end_frame()
|
|
185
|
+
|
|
186
|
+
if target_dt > 0 and dt < target_dt:
|
|
187
|
+
sleep(target_dt - dt)
|
|
188
|
+
|
|
189
|
+
# exit remaining scenes
|
|
190
|
+
while self._scene_stack:
|
|
191
|
+
entry = self._scene_stack.pop()
|
|
192
|
+
entry.scene.on_exit()
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _convert_bmp_to_image(bmp_path: str, out_path: str) -> bool:
|
|
196
|
+
"""
|
|
197
|
+
Convert a BMP file to another image format using Pillow.
|
|
198
|
+
|
|
199
|
+
:param bmp_path: Path to the input BMP file.
|
|
200
|
+
:type bmp_path: str
|
|
201
|
+
|
|
202
|
+
:param out_path: Path to the output image file.
|
|
203
|
+
:type out_path: str
|
|
204
|
+
|
|
205
|
+
:return: True if conversion was successful, False otherwise.
|
|
206
|
+
:rtype: bool
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
img = Image.open(bmp_path)
|
|
210
|
+
img.save(out_path) # Pillow chooses format from extension
|
|
211
|
+
return True
|
|
212
|
+
# Justification: Pillow can raise various exceptions on failure
|
|
213
|
+
# pylint: disable=broad-exception-caught
|
|
214
|
+
except Exception:
|
|
215
|
+
return False
|
|
216
|
+
# pylint: enable=broad-exception-caught
|
|
217
|
+
|
|
218
|
+
def screenshot(
|
|
219
|
+
self, label: str | None = None, directory: str = "screenshots"
|
|
220
|
+
) -> str | None:
|
|
221
|
+
"""
|
|
222
|
+
Ask backend to save a screenshot. Returns the file path or None.
|
|
223
|
+
|
|
224
|
+
:param label: Optional label to include in the filename.
|
|
225
|
+
:type label: str | None
|
|
226
|
+
|
|
227
|
+
:param directory: Directory to save screenshots in.
|
|
228
|
+
:type directory: str
|
|
229
|
+
|
|
230
|
+
:return: The file path of the saved screenshot, or None on failure.
|
|
231
|
+
:rtype: str | None
|
|
232
|
+
"""
|
|
233
|
+
os.makedirs(directory, exist_ok=True)
|
|
234
|
+
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
235
|
+
label = label or "shot"
|
|
236
|
+
filename = f"{stamp}_{label}"
|
|
237
|
+
bmp_path = os.path.join(directory, f"{filename}.bmp")
|
|
238
|
+
|
|
239
|
+
if self.backend.capture_frame(bmp_path):
|
|
240
|
+
out_path = Path(directory) / f"{filename}.png"
|
|
241
|
+
self._convert_bmp_to_image(bmp_path, str(out_path))
|
|
242
|
+
return str(out_path)
|
|
243
|
+
return None
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
2D geometry data structures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Position2D:
|
|
12
|
+
"""
|
|
13
|
+
Simple 2D position.
|
|
14
|
+
|
|
15
|
+
:ivar x (float): X coordinate.
|
|
16
|
+
:ivar y (float): Y coordinate.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
x: float
|
|
20
|
+
y: float
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Size2D:
|
|
25
|
+
"""
|
|
26
|
+
Simple 2D size.
|
|
27
|
+
|
|
28
|
+
:ivar width (int): Width.
|
|
29
|
+
:ivar height (int): Height.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
width: int
|
|
33
|
+
height: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Bounds2D:
|
|
38
|
+
"""
|
|
39
|
+
Axis-aligned rectangular bounds in world space.
|
|
40
|
+
(left, top) .. (right, bottom)
|
|
41
|
+
|
|
42
|
+
:ivar left (float): Left boundary.
|
|
43
|
+
:ivar top (float): Top boundary.
|
|
44
|
+
:ivar right (float): Right boundary.
|
|
45
|
+
:ivar bottom (float): Bottom boundary.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
left: float
|
|
49
|
+
top: float
|
|
50
|
+
right: float
|
|
51
|
+
bottom: float
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_size(cls, size: "Size2D") -> "Bounds2D":
|
|
55
|
+
"""
|
|
56
|
+
Convenience factory for screen/world bounds starting at (0, 0).
|
|
57
|
+
|
|
58
|
+
:param size: Size2D defining the bounds.
|
|
59
|
+
:type size: Size2D
|
|
60
|
+
|
|
61
|
+
:return: Bounds2D from (0,0) to (size.width, size.height).
|
|
62
|
+
:rtype: Bounds2D
|
|
63
|
+
"""
|
|
64
|
+
return cls(
|
|
65
|
+
left=0.0,
|
|
66
|
+
top=0.0,
|
|
67
|
+
right=float(size.width),
|
|
68
|
+
bottom=float(size.height),
|
|
69
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kinematic helpers for mini_arcade_core (position + size + velocity).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .backend import Color
|
|
11
|
+
from .geometry2d import Position2D, Size2D
|
|
12
|
+
from .physics2d import Velocity2D
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class KinematicData:
|
|
17
|
+
"""
|
|
18
|
+
Simple data structure to hold position, size, and velocity.
|
|
19
|
+
|
|
20
|
+
:ivar position (Position2D): Top-left position of the object.
|
|
21
|
+
:ivar size (Size2D): Size of the object.
|
|
22
|
+
:ivar velocity (Velocity2D): Velocity of the object.
|
|
23
|
+
:ivar color (Optional[Color]): Optional color for rendering.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
position: Position2D
|
|
27
|
+
size: Size2D
|
|
28
|
+
velocity: Velocity2D
|
|
29
|
+
color: Optional[Color] = None # future use
|
|
30
|
+
|
|
31
|
+
# Justification: Convenience factory with many params.
|
|
32
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
33
|
+
@classmethod
|
|
34
|
+
def rect(
|
|
35
|
+
cls,
|
|
36
|
+
x: float,
|
|
37
|
+
y: float,
|
|
38
|
+
width: int,
|
|
39
|
+
height: int,
|
|
40
|
+
vx: float = 0.0,
|
|
41
|
+
vy: float = 0.0,
|
|
42
|
+
color: Optional[Color] = None,
|
|
43
|
+
) -> "KinematicData":
|
|
44
|
+
"""
|
|
45
|
+
Convenience factory for rectangular kinematic data.
|
|
46
|
+
|
|
47
|
+
:param x: Top-left X position.
|
|
48
|
+
:type x: float
|
|
49
|
+
|
|
50
|
+
:param y: Top-left Y position.
|
|
51
|
+
:type y: float
|
|
52
|
+
|
|
53
|
+
:param width: Width of the object.
|
|
54
|
+
:type width: int
|
|
55
|
+
|
|
56
|
+
:param height: Height of the object.
|
|
57
|
+
:type height: int
|
|
58
|
+
|
|
59
|
+
:param vx: Velocity in the X direction.
|
|
60
|
+
:type vx: float
|
|
61
|
+
|
|
62
|
+
:param vy: Velocity in the Y direction.
|
|
63
|
+
:type vy: float
|
|
64
|
+
|
|
65
|
+
:param color: Optional color for rendering.
|
|
66
|
+
:type color: Optional[Color]
|
|
67
|
+
|
|
68
|
+
:return: KinematicData instance with the specified parameters.
|
|
69
|
+
:rtype: KinematicData
|
|
70
|
+
"""
|
|
71
|
+
return cls(
|
|
72
|
+
position=Position2D(float(x), float(y)),
|
|
73
|
+
size=Size2D(int(width), int(height)),
|
|
74
|
+
velocity=Velocity2D(float(vx), float(vy)),
|
|
75
|
+
color=color,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple 2D physics utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Velocity2D:
|
|
12
|
+
"""
|
|
13
|
+
Simple 2D velocity vector.
|
|
14
|
+
|
|
15
|
+
:ivar vx (float): Velocity in the X direction.
|
|
16
|
+
:ivar vy (float): Velocity in the Y direction.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
vx: float = 0.0
|
|
20
|
+
vy: float = 0.0
|
|
21
|
+
|
|
22
|
+
def advance(self, x: float, y: float, dt: float) -> tuple[float, float]:
|
|
23
|
+
"""Return new (x, y) after dt seconds."""
|
|
24
|
+
return x + self.vx * dt, y + self.vy * dt
|
|
25
|
+
|
|
26
|
+
def stop(self):
|
|
27
|
+
"""Stop movement in both axes."""
|
|
28
|
+
self.vx = 0.0
|
|
29
|
+
self.vy = 0.0
|
|
30
|
+
|
|
31
|
+
def stop_x(self):
|
|
32
|
+
"""Stop horizontal movement."""
|
|
33
|
+
self.vx = 0.0
|
|
34
|
+
|
|
35
|
+
def stop_y(self):
|
|
36
|
+
"""Stop vertical movement."""
|
|
37
|
+
self.vy = 0.0
|
|
38
|
+
|
|
39
|
+
def move_up(self, speed: float):
|
|
40
|
+
"""
|
|
41
|
+
Set vertical velocity upwards (negative Y).
|
|
42
|
+
|
|
43
|
+
:param speed: Speed to set.
|
|
44
|
+
:type speed: float
|
|
45
|
+
"""
|
|
46
|
+
self.vy = -abs(speed)
|
|
47
|
+
|
|
48
|
+
def move_down(self, speed: float):
|
|
49
|
+
"""
|
|
50
|
+
Set vertical velocity downwards (positive Y).
|
|
51
|
+
|
|
52
|
+
:param speed: Speed to set.
|
|
53
|
+
:type speed: float
|
|
54
|
+
"""
|
|
55
|
+
self.vy = abs(speed)
|
|
56
|
+
|
|
57
|
+
def move_left(self, speed: float):
|
|
58
|
+
"""
|
|
59
|
+
Set horizontal velocity to the left (negative X)."
|
|
60
|
+
|
|
61
|
+
:param speed: Speed to set.
|
|
62
|
+
:type speed: float
|
|
63
|
+
"""
|
|
64
|
+
self.vx = -abs(speed)
|
|
65
|
+
|
|
66
|
+
def move_right(self, speed: float):
|
|
67
|
+
"""
|
|
68
|
+
Set horizontal velocity to the right (positive X).
|
|
69
|
+
|
|
70
|
+
:param speed: Speed to set.
|
|
71
|
+
:type speed: float
|
|
72
|
+
"""
|
|
73
|
+
self.vx = abs(speed)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for game scenes (states/screens).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Callable, List
|
|
9
|
+
|
|
10
|
+
from .backend import Backend, Event
|
|
11
|
+
from .entity import Entity
|
|
12
|
+
from .game import Game
|
|
13
|
+
from .geometry2d import Size2D
|
|
14
|
+
|
|
15
|
+
OverlayFunc = Callable[[Backend], None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Scene(ABC):
|
|
19
|
+
"""Base class for game scenes (states/screens)."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, game: Game):
|
|
22
|
+
"""
|
|
23
|
+
:param game: Reference to the main Game object.
|
|
24
|
+
:type game: Game
|
|
25
|
+
"""
|
|
26
|
+
self.game = game
|
|
27
|
+
self.entities: List[Entity] = []
|
|
28
|
+
self.size: Size2D = Size2D(game.config.width, game.config.height)
|
|
29
|
+
# overlays drawn on top of the scene
|
|
30
|
+
self._overlays: List[OverlayFunc] = []
|
|
31
|
+
|
|
32
|
+
def add_entity(self, *entities: Entity):
|
|
33
|
+
"""
|
|
34
|
+
Register one or more entities in this scene.
|
|
35
|
+
|
|
36
|
+
:param entities: One or more Entity instances to add.
|
|
37
|
+
:type entities: Entity
|
|
38
|
+
"""
|
|
39
|
+
self.entities.extend(entities)
|
|
40
|
+
|
|
41
|
+
def remove_entity(self, entity: Entity):
|
|
42
|
+
"""
|
|
43
|
+
Unregister a single entity, if present.
|
|
44
|
+
|
|
45
|
+
:param entity: The Entity instance to remove.
|
|
46
|
+
:type entity: Entity
|
|
47
|
+
"""
|
|
48
|
+
if entity in self.entities:
|
|
49
|
+
self.entities.remove(entity)
|
|
50
|
+
|
|
51
|
+
def clear_entities(self):
|
|
52
|
+
"""Remove all entities from the scene."""
|
|
53
|
+
self.entities.clear()
|
|
54
|
+
|
|
55
|
+
def update_entities(self, dt: float):
|
|
56
|
+
"""
|
|
57
|
+
Default update loop for all entities.
|
|
58
|
+
|
|
59
|
+
:param dt: Time delta in seconds.
|
|
60
|
+
:type dt: float
|
|
61
|
+
"""
|
|
62
|
+
for ent in self.entities:
|
|
63
|
+
ent.update(dt)
|
|
64
|
+
|
|
65
|
+
def draw_entities(self, surface: Backend):
|
|
66
|
+
"""
|
|
67
|
+
Default draw loop for all entities.
|
|
68
|
+
|
|
69
|
+
:param surface: The backend surface to draw on.
|
|
70
|
+
:type surface: Backend
|
|
71
|
+
"""
|
|
72
|
+
for ent in self.entities:
|
|
73
|
+
ent.draw(surface)
|
|
74
|
+
|
|
75
|
+
def add_overlay(self, overlay: OverlayFunc):
|
|
76
|
+
"""
|
|
77
|
+
Register an overlay (drawn every frame, after entities).
|
|
78
|
+
|
|
79
|
+
:param overlay: A callable that takes a Backend and draws on it.
|
|
80
|
+
:type overlay: OverlayFunc
|
|
81
|
+
"""
|
|
82
|
+
self._overlays.append(overlay)
|
|
83
|
+
|
|
84
|
+
def remove_overlay(self, overlay: OverlayFunc):
|
|
85
|
+
"""
|
|
86
|
+
Unregister a previously added overlay.
|
|
87
|
+
|
|
88
|
+
:param overlay: The overlay to remove.
|
|
89
|
+
:type overlay: OverlayFunc
|
|
90
|
+
"""
|
|
91
|
+
if overlay in self._overlays:
|
|
92
|
+
self._overlays.remove(overlay)
|
|
93
|
+
|
|
94
|
+
def clear_overlays(self):
|
|
95
|
+
"""Clear all registered overlays."""
|
|
96
|
+
self._overlays.clear()
|
|
97
|
+
|
|
98
|
+
def draw_overlays(self, surface: Backend):
|
|
99
|
+
"""
|
|
100
|
+
Call all overlays. Scenes should call this at the end of draw().
|
|
101
|
+
|
|
102
|
+
:param surface: The backend surface to draw on.
|
|
103
|
+
:type surface: Backend
|
|
104
|
+
"""
|
|
105
|
+
for overlay in self._overlays:
|
|
106
|
+
overlay(surface)
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def on_enter(self):
|
|
110
|
+
"""Called when the scene becomes active."""
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def on_exit(self):
|
|
114
|
+
"""Called when the scene is replaced."""
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def handle_event(self, event: Event):
|
|
118
|
+
"""
|
|
119
|
+
Handle input / events (e.g. pygame.Event).
|
|
120
|
+
|
|
121
|
+
:param event: The event to handle.
|
|
122
|
+
:type event: Event
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def update(self, dt: float):
|
|
127
|
+
"""
|
|
128
|
+
Update game logic. ``dt`` is the delta time in seconds.
|
|
129
|
+
|
|
130
|
+
:param dt: Time delta in seconds.
|
|
131
|
+
:type dt: float
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def draw(self, surface: Backend):
|
|
136
|
+
"""
|
|
137
|
+
Render to the main surface.
|
|
138
|
+
|
|
139
|
+
:param surface: The backend surface to draw on.
|
|
140
|
+
:type surface: Backend
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def on_pause(self):
|
|
144
|
+
"""Called when the game is paused."""
|
|
145
|
+
|
|
146
|
+
def on_resume(self):
|
|
147
|
+
"""Called when the game is resumed."""
|
|
File without changes
|