mima-engine 0.4.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.
- mima/__init__.py +4 -0
- mima/backend/__init__.py +1 -0
- mima/backend/pygame_assets.py +401 -0
- mima/backend/pygame_audio.py +78 -0
- mima/backend/pygame_backend.py +603 -0
- mima/backend/pygame_camera.py +63 -0
- mima/backend/pygame_events.py +695 -0
- mima/backend/touch_control_scheme_a.py +126 -0
- mima/backend/touch_control_scheme_b.py +132 -0
- mima/core/__init__.py +0 -0
- mima/core/collision.py +325 -0
- mima/core/database.py +58 -0
- mima/core/engine.py +367 -0
- mima/core/mode_engine.py +81 -0
- mima/core/scene_engine.py +81 -0
- mima/integrated/__init__.py +0 -0
- mima/integrated/entity.py +183 -0
- mima/integrated/layered_map.py +351 -0
- mima/integrated/sprite.py +156 -0
- mima/layered/__init__.py +0 -0
- mima/layered/assets.py +56 -0
- mima/layered/scene.py +415 -0
- mima/layered/shape.py +99 -0
- mima/layered/shaped_sprite.py +78 -0
- mima/layered/virtual_input.py +302 -0
- mima/maps/__init__.py +0 -0
- mima/maps/template.py +71 -0
- mima/maps/tile.py +20 -0
- mima/maps/tile_animation.py +7 -0
- mima/maps/tile_info.py +10 -0
- mima/maps/tile_layer.py +52 -0
- mima/maps/tiled/__init__.py +0 -0
- mima/maps/tiled/tiled_layer.py +48 -0
- mima/maps/tiled/tiled_map.py +95 -0
- mima/maps/tiled/tiled_object.py +79 -0
- mima/maps/tiled/tiled_objectgroup.py +25 -0
- mima/maps/tiled/tiled_template.py +49 -0
- mima/maps/tiled/tiled_tile.py +90 -0
- mima/maps/tiled/tiled_tileset.py +51 -0
- mima/maps/tilemap.py +216 -0
- mima/maps/tileset.py +39 -0
- mima/maps/tileset_info.py +9 -0
- mima/maps/transition_map.py +146 -0
- mima/objects/__init__.py +0 -0
- mima/objects/animated_sprite.py +217 -0
- mima/objects/attribute_effect.py +26 -0
- mima/objects/attributes.py +126 -0
- mima/objects/creature.py +384 -0
- mima/objects/dynamic.py +206 -0
- mima/objects/effects/__init__.py +0 -0
- mima/objects/effects/colorize_screen.py +60 -0
- mima/objects/effects/debug_box.py +133 -0
- mima/objects/effects/light.py +103 -0
- mima/objects/effects/show_sprite.py +50 -0
- mima/objects/effects/walking_on_grass.py +70 -0
- mima/objects/effects/walking_on_water.py +57 -0
- mima/objects/loader.py +111 -0
- mima/objects/projectile.py +111 -0
- mima/objects/sprite.py +116 -0
- mima/objects/world/__init__.py +0 -0
- mima/objects/world/color_gate.py +67 -0
- mima/objects/world/color_switch.py +101 -0
- mima/objects/world/container.py +175 -0
- mima/objects/world/floor_switch.py +109 -0
- mima/objects/world/gate.py +178 -0
- mima/objects/world/light_source.py +121 -0
- mima/objects/world/logic_gate.py +157 -0
- mima/objects/world/movable.py +399 -0
- mima/objects/world/oneway.py +195 -0
- mima/objects/world/pickup.py +157 -0
- mima/objects/world/switch.py +179 -0
- mima/objects/world/teleport.py +308 -0
- mima/py.typed +0 -0
- mima/scripts/__init__.py +2 -0
- mima/scripts/command.py +38 -0
- mima/scripts/commands/__init__.py +0 -0
- mima/scripts/commands/add_quest.py +19 -0
- mima/scripts/commands/change_map.py +34 -0
- mima/scripts/commands/close_dialog.py +9 -0
- mima/scripts/commands/equip_weapon.py +23 -0
- mima/scripts/commands/give_item.py +26 -0
- mima/scripts/commands/give_resource.py +51 -0
- mima/scripts/commands/move_map.py +152 -0
- mima/scripts/commands/move_to.py +49 -0
- mima/scripts/commands/oneway_move.py +58 -0
- mima/scripts/commands/parallel.py +66 -0
- mima/scripts/commands/play_sound.py +13 -0
- mima/scripts/commands/present_item.py +53 -0
- mima/scripts/commands/progress_quest.py +12 -0
- mima/scripts/commands/quit_game.py +8 -0
- mima/scripts/commands/save_game.py +14 -0
- mima/scripts/commands/screen_fade.py +83 -0
- mima/scripts/commands/serial.py +69 -0
- mima/scripts/commands/set_facing_direction.py +21 -0
- mima/scripts/commands/set_spawn_map.py +17 -0
- mima/scripts/commands/show_choices.py +52 -0
- mima/scripts/commands/show_dialog.py +118 -0
- mima/scripts/commands/take_coins.py +23 -0
- mima/scripts/script_processor.py +61 -0
- mima/standalone/__init__.py +0 -0
- mima/standalone/camera.py +153 -0
- mima/standalone/geometry.py +1318 -0
- mima/standalone/multicolumn_list.py +54 -0
- mima/standalone/pixel_font.py +84 -0
- mima/standalone/scripting.py +145 -0
- mima/standalone/spatial.py +186 -0
- mima/standalone/sprite.py +158 -0
- mima/standalone/tiled_map.py +1247 -0
- mima/standalone/transformed_view.py +433 -0
- mima/standalone/user_input.py +563 -0
- mima/states/__init__.py +0 -0
- mima/states/game_state.py +189 -0
- mima/states/memory.py +28 -0
- mima/states/quest.py +71 -0
- mima/types/__init__.py +0 -0
- mima/types/alignment.py +7 -0
- mima/types/blend.py +8 -0
- mima/types/damage.py +42 -0
- mima/types/direction.py +44 -0
- mima/types/gate_color.py +7 -0
- mima/types/graphic_state.py +23 -0
- mima/types/keys.py +64 -0
- mima/types/mode.py +9 -0
- mima/types/nature.py +12 -0
- mima/types/object.py +22 -0
- mima/types/player.py +9 -0
- mima/types/position.py +13 -0
- mima/types/start.py +7 -0
- mima/types/terrain.py +9 -0
- mima/types/tile_collision.py +11 -0
- mima/types/weapon_slot.py +6 -0
- mima/types/window.py +44 -0
- mima/usables/__init__.py +0 -0
- mima/usables/item.py +51 -0
- mima/usables/weapon.py +68 -0
- mima/util/__init__.py +1 -0
- mima/util/colors.py +50 -0
- mima/util/constants.py +55 -0
- mima/util/functions.py +38 -0
- mima/util/input_defaults.py +170 -0
- mima/util/logging.py +51 -0
- mima/util/property.py +8 -0
- mima/util/runtime_config.py +327 -0
- mima/util/trading_item.py +23 -0
- mima/view/__init__.py +0 -0
- mima/view/camera.py +192 -0
- mima/view/mima_mode.py +618 -0
- mima/view/mima_scene.py +231 -0
- mima/view/mima_view.py +12 -0
- mima/view/mima_window.py +244 -0
- mima_engine-0.4.0.dist-info/METADATA +47 -0
- mima_engine-0.4.0.dist-info/RECORD +153 -0
- mima_engine-0.4.0.dist-info/WHEEL +4 -0
mima/layered/scene.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import pygame
|
|
7
|
+
from typing_extensions import Any, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
from mima.standalone.transformed_view import TileTransformedView
|
|
10
|
+
|
|
11
|
+
LOG = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
V = TypeVar("V")
|
|
14
|
+
S = TypeVar("S")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Position(Enum):
|
|
18
|
+
FULL = 0 # One Window
|
|
19
|
+
TOP = 1 # Two or three Windows
|
|
20
|
+
BOTTOM = 2 # Two or three Windows
|
|
21
|
+
LEFT = 3 # Two or three Windows
|
|
22
|
+
RIGHT = 4 # Two or three Windows
|
|
23
|
+
TOP_LEFT = 5 # Three or four Windows
|
|
24
|
+
TOP_RIGHT = 6 # Three or four Windows
|
|
25
|
+
BOTTOM_LEFT = 7 # Three or four Windows
|
|
26
|
+
BOTTOM_RIGHT = 8 # Three or four Windows
|
|
27
|
+
CUSTOM = 9 # Custom size and position
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class View(Generic[V]):
|
|
31
|
+
"""A view displays things on certain areas of the screen."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
view_type: V,
|
|
36
|
+
screen: pygame.Surface,
|
|
37
|
+
position: Position = Position.FULL,
|
|
38
|
+
tile_size: pygame.Vector2 | None = None,
|
|
39
|
+
*,
|
|
40
|
+
pixel_pos: pygame.Vector2 | None = None,
|
|
41
|
+
pixel_size: pygame.Vector2 | None = None,
|
|
42
|
+
pixel_scale: float = 1.0,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._view_type = view_type
|
|
45
|
+
self._screen = screen
|
|
46
|
+
self._position = position
|
|
47
|
+
self._tile_size = pygame.Vector2(1, 1) if tile_size is None else tile_size
|
|
48
|
+
|
|
49
|
+
if position == Position.CUSTOM:
|
|
50
|
+
self._pos = pygame.Vector2(0, 0) if pixel_pos is None else pixel_pos
|
|
51
|
+
self._size = (
|
|
52
|
+
pygame.Vector2(screen.get_size()) if pixel_size is None else pixel_size
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
self._pos, self._size = position_to_screen_rect(
|
|
56
|
+
position, pygame.Vector2(0, 0), pygame.Vector2(screen.get_size())
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.ttv = TileTransformedView(
|
|
60
|
+
self._screen, self._size, self._tile_size, pixel_scale
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.ttv._pos = self._pos
|
|
64
|
+
self._window: Window | None = None
|
|
65
|
+
|
|
66
|
+
def enter_focus(self, data: dict[str, Any]) -> None:
|
|
67
|
+
"""Prepare this view for presentation.
|
|
68
|
+
|
|
69
|
+
This function is called when this view enters the screen. Data provided
|
|
70
|
+
from the previous view is stored in ``data``.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
data: Data from previous view (provided via ``push_view``)
|
|
74
|
+
"""
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def _enter_focus(self, data: dict[str, Any] | None) -> None:
|
|
78
|
+
LOG.debug("%s (%s) enter focus", self, self._view_type)
|
|
79
|
+
self.enter_focus(data or {})
|
|
80
|
+
|
|
81
|
+
def exit_focus(self) -> dict[str, Any] | None:
|
|
82
|
+
"""Clean-up when this view leaves the screen.
|
|
83
|
+
|
|
84
|
+
This function is called before a new view is presented. Additional data
|
|
85
|
+
can be passed to potential following views.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
A dict with data for a following scene or None. The other scene has
|
|
89
|
+
to explicitly call ``get_data_from_previous_view()`` to retrieve
|
|
90
|
+
the data. For most cases, it is recommended to use ``push_view()``
|
|
91
|
+
to pass data directly to the ``enter_focus()`` because it gives more
|
|
92
|
+
control over what data to pass to which following view.
|
|
93
|
+
"""
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
def _exit_focus(self) -> dict[str, Any]:
|
|
97
|
+
LOG.debug("%s (%s) exit focus.", self, self._view_type)
|
|
98
|
+
return self.exit_focus() or {}
|
|
99
|
+
|
|
100
|
+
def handle_input(
|
|
101
|
+
self, events: list[pygame.event.Event] | None
|
|
102
|
+
) -> list[pygame.event.Event]:
|
|
103
|
+
events = self.ttv.handle_pan_and_zoom(events=events)
|
|
104
|
+
|
|
105
|
+
return events
|
|
106
|
+
|
|
107
|
+
def update(self, elapsed_time: float) -> bool:
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def draw(self) -> None:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def get_type(self) -> V:
|
|
114
|
+
return self._view_type
|
|
115
|
+
|
|
116
|
+
def set_window(self, window: Window, position: Position = Position.FULL) -> None:
|
|
117
|
+
self._window = window
|
|
118
|
+
self._position = position
|
|
119
|
+
self._pos, self._size = position_to_screen_rect(
|
|
120
|
+
position, pygame.Vector2(0, 0), pygame.Vector2(self._screen.get_size())
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self.ttv._view_area = self._size
|
|
124
|
+
self.ttv.set_pos(self._pos)
|
|
125
|
+
|
|
126
|
+
def get_window(self) -> Window[V]:
|
|
127
|
+
if self._window is None:
|
|
128
|
+
msg = "Window is not initialized yet!"
|
|
129
|
+
raise ValueError(msg)
|
|
130
|
+
return self._window
|
|
131
|
+
|
|
132
|
+
def get_scene(self) -> Scene[V]:
|
|
133
|
+
return self.get_window().get_scene()
|
|
134
|
+
|
|
135
|
+
def push_scene(
|
|
136
|
+
self, key: Any, data_for_scene: dict[str, Any] | None = None
|
|
137
|
+
) -> None:
|
|
138
|
+
self.get_window().get_scene().get_manager().push_scene(key, data_for_scene)
|
|
139
|
+
|
|
140
|
+
def pop_scene(self, empty_ok: bool = False) -> Any | None:
|
|
141
|
+
return self.get_window().get_scene().get_manager().pop_scene(empty_ok)
|
|
142
|
+
|
|
143
|
+
def push_view(self, key: Any, data_for_view: dict[str, Any] | None = None) -> None:
|
|
144
|
+
self.get_window().push_view(key, data_for_view)
|
|
145
|
+
|
|
146
|
+
def pop_view(self, empty_ok: bool = False) -> V | None:
|
|
147
|
+
return self.get_window().pop_view(empty_ok)
|
|
148
|
+
|
|
149
|
+
def get_screen_area(self) -> tuple[pygame.Vector2, pygame.Vector2]:
|
|
150
|
+
return self.ttv.get_pos(), self.ttv.get_view_area()
|
|
151
|
+
|
|
152
|
+
def get_data_from_previous_view(self) -> dict[str, Any] | None:
|
|
153
|
+
return self.get_window().get_data_from_previous_view()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Window(Generic[V]):
|
|
157
|
+
"""A class that manages different views."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, pos: Position) -> None:
|
|
160
|
+
self._views: dict[V, View] = {}
|
|
161
|
+
self._view: View[V] | None = None
|
|
162
|
+
self._view_stack: list[V] = []
|
|
163
|
+
self._stack_changed: bool = False
|
|
164
|
+
self._scene: Scene | None = None
|
|
165
|
+
self._pos: Position = pos
|
|
166
|
+
self._data_for_view: dict[str, Any] = {}
|
|
167
|
+
self._data_from_view: dict[str, Any] = {}
|
|
168
|
+
|
|
169
|
+
def add_view(self, view: View[V]) -> None:
|
|
170
|
+
view.set_window(self, self._pos)
|
|
171
|
+
self._views[view.get_type()] = view
|
|
172
|
+
|
|
173
|
+
def push_view(self, view: V, data_for_view: dict[str, Any] | None = None) -> None:
|
|
174
|
+
self._view_stack.append(view)
|
|
175
|
+
self._stack_changed = True
|
|
176
|
+
self._data_for_view = {} if data_for_view is None else data_for_view
|
|
177
|
+
|
|
178
|
+
def pop_view(self, empty_ok: bool = False) -> V | None:
|
|
179
|
+
self._stack_changed = True
|
|
180
|
+
if self._view_stack:
|
|
181
|
+
return self._view_stack.pop()
|
|
182
|
+
elif empty_ok:
|
|
183
|
+
return None
|
|
184
|
+
else:
|
|
185
|
+
msg = "Stack is empty. Set `empty_ok` to True if you don't mind."
|
|
186
|
+
raise IndexError(msg)
|
|
187
|
+
|
|
188
|
+
def enter_focus(self, data: dict[str, Any]) -> None:
|
|
189
|
+
LOG.debug("%s enter focus", self)
|
|
190
|
+
if self._view_stack and self._view is not None:
|
|
191
|
+
self._view._enter_focus(data)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
def exit_focus(self) -> dict[str, Any]:
|
|
195
|
+
LOG.debug("%s exit focus", self)
|
|
196
|
+
data = {}
|
|
197
|
+
if not self._view_stack:
|
|
198
|
+
return data
|
|
199
|
+
if self._view is not None:
|
|
200
|
+
data = self._view._exit_focus()
|
|
201
|
+
|
|
202
|
+
return {} if data is None else data
|
|
203
|
+
|
|
204
|
+
def get_data_from_previous_view(self) -> dict[str, Any] | None:
|
|
205
|
+
return self._data_from_view
|
|
206
|
+
|
|
207
|
+
def handle_input(
|
|
208
|
+
self, events: list[pygame.event.Event]
|
|
209
|
+
) -> list[pygame.event.Event]:
|
|
210
|
+
if self._view is not None:
|
|
211
|
+
return self._view.handle_input(events)
|
|
212
|
+
return events
|
|
213
|
+
|
|
214
|
+
def update(self, elapsed_time: float) -> bool:
|
|
215
|
+
if not self._view_stack:
|
|
216
|
+
return True
|
|
217
|
+
old_view = self._view
|
|
218
|
+
self._view = self._views[self._view_stack[-1]]
|
|
219
|
+
|
|
220
|
+
if self._view != old_view or self._stack_changed:
|
|
221
|
+
self._stack_changed = False
|
|
222
|
+
if old_view is not None:
|
|
223
|
+
self._data_from_view = old_view._exit_focus()
|
|
224
|
+
|
|
225
|
+
self._view._enter_focus(self._data_for_view)
|
|
226
|
+
|
|
227
|
+
if self._view is not None:
|
|
228
|
+
return self._view.update(elapsed_time)
|
|
229
|
+
else:
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
def draw(self) -> None:
|
|
233
|
+
if self._view is not None:
|
|
234
|
+
self._view._screen.set_clip(self._view.get_screen_area())
|
|
235
|
+
self._view.draw()
|
|
236
|
+
self._view._screen.set_clip(None)
|
|
237
|
+
|
|
238
|
+
def set_scene(self, scene: Scene[V]) -> None:
|
|
239
|
+
self._scene = scene
|
|
240
|
+
|
|
241
|
+
def get_scene(self) -> Scene[V]:
|
|
242
|
+
if self._scene is None:
|
|
243
|
+
msg = "Scene is not initialized yet!"
|
|
244
|
+
raise ValueError(msg)
|
|
245
|
+
return self._scene
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Scene(Generic[V]):
|
|
249
|
+
"""A scene can have multiple windows aka split-screen."""
|
|
250
|
+
|
|
251
|
+
def __init__(self) -> None:
|
|
252
|
+
self._windows: dict[int, Window[V]] = {}
|
|
253
|
+
self._manager: SceneManager | None = None
|
|
254
|
+
|
|
255
|
+
def add_window(self, win: Window[V], key: int = 0) -> None:
|
|
256
|
+
win.set_scene(self)
|
|
257
|
+
self._windows[key] = win
|
|
258
|
+
|
|
259
|
+
def enter_focus(self, data: dict[str, Any]) -> None:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
def _enter_focus(self, data: dict[str, Any]) -> None:
|
|
263
|
+
LOG.debug("%s enter focus", self)
|
|
264
|
+
self.enter_focus(data)
|
|
265
|
+
for win in self._windows.values():
|
|
266
|
+
win.enter_focus(data)
|
|
267
|
+
|
|
268
|
+
def exit_focus(self) -> dict[str, Any]:
|
|
269
|
+
return {}
|
|
270
|
+
|
|
271
|
+
def _exit_focus(self) -> dict[str, Any]:
|
|
272
|
+
LOG.debug("%s exit focus", self)
|
|
273
|
+
data = {}
|
|
274
|
+
for key, win in self._windows.items():
|
|
275
|
+
data[key] = win.exit_focus()
|
|
276
|
+
|
|
277
|
+
return self.exit_focus()
|
|
278
|
+
|
|
279
|
+
def handle_input(
|
|
280
|
+
self, events: list[pygame.event.Event] | None = None
|
|
281
|
+
) -> list[pygame.event.Event]:
|
|
282
|
+
if events is None:
|
|
283
|
+
events = [e for e in pygame.event.get()]
|
|
284
|
+
|
|
285
|
+
for window in self._windows.values():
|
|
286
|
+
events = window.handle_input(events)
|
|
287
|
+
|
|
288
|
+
return events
|
|
289
|
+
|
|
290
|
+
def update(self, elapsed_time: float) -> bool:
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
def _update(self, elapsed_time: float) -> bool:
|
|
294
|
+
success = self.update(elapsed_time)
|
|
295
|
+
for window in self._windows.values():
|
|
296
|
+
success = window.update(elapsed_time) and success
|
|
297
|
+
return success
|
|
298
|
+
|
|
299
|
+
def draw(self) -> None:
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
def _draw(self) -> None:
|
|
303
|
+
for window in self._windows.values():
|
|
304
|
+
window.draw()
|
|
305
|
+
|
|
306
|
+
self.draw()
|
|
307
|
+
|
|
308
|
+
def set_manager(self, manager: SceneManager) -> None:
|
|
309
|
+
self._manager = manager
|
|
310
|
+
|
|
311
|
+
def get_manager(self) -> SceneManager:
|
|
312
|
+
if self._manager is None:
|
|
313
|
+
msg = "Manager is not initialized yet!"
|
|
314
|
+
raise ValueError(msg)
|
|
315
|
+
return self._manager
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class SceneManager(Generic[S, V]):
|
|
319
|
+
"""A class that manages different scenes."""
|
|
320
|
+
|
|
321
|
+
def __init__(self) -> None:
|
|
322
|
+
self._scenes: dict[S, Scene[V]] = {}
|
|
323
|
+
self._scene: Scene[V] | None = None
|
|
324
|
+
self._stack: list[S] = []
|
|
325
|
+
self._stack_changed: bool = False
|
|
326
|
+
self._data_for_scene: dict[str, Any] = {}
|
|
327
|
+
self._data_from_scene: dict[str, Any] = {}
|
|
328
|
+
|
|
329
|
+
def add_scene(self, scene: Scene[V], key: S) -> None:
|
|
330
|
+
scene.set_manager(self)
|
|
331
|
+
self._scenes[key] = scene
|
|
332
|
+
|
|
333
|
+
def push_scene(self, key: S, data_for_scene: dict[str, Any] | None = None) -> None:
|
|
334
|
+
self._stack.append(key)
|
|
335
|
+
self._stack_changed = True
|
|
336
|
+
self._data_for_scene = {} if data_for_scene is None else data_for_scene
|
|
337
|
+
|
|
338
|
+
def pop_scene(self, empty_ok: bool = False) -> S | None:
|
|
339
|
+
self._stack_changed = True
|
|
340
|
+
if self._stack:
|
|
341
|
+
return self._stack.pop()
|
|
342
|
+
elif empty_ok:
|
|
343
|
+
return None
|
|
344
|
+
else:
|
|
345
|
+
msg = "Stack is empty. Set `empty_ok` to True if you don't mind."
|
|
346
|
+
raise IndexError(msg)
|
|
347
|
+
|
|
348
|
+
def get_scene(self) -> Scene[V]:
|
|
349
|
+
if self._scene is None:
|
|
350
|
+
msg = "No scene initialized yet."
|
|
351
|
+
raise ValueError(msg)
|
|
352
|
+
|
|
353
|
+
return self._scene
|
|
354
|
+
|
|
355
|
+
def handle_input(
|
|
356
|
+
self, events: list[pygame.event.Event] | None = None
|
|
357
|
+
) -> list[pygame.event.Event]:
|
|
358
|
+
if events is None:
|
|
359
|
+
events = [e for e in pygame.event.get()]
|
|
360
|
+
|
|
361
|
+
if self._scene is not None:
|
|
362
|
+
return self._scene.handle_input(events)
|
|
363
|
+
|
|
364
|
+
return events
|
|
365
|
+
|
|
366
|
+
def update(self, elapsed_time: float) -> bool:
|
|
367
|
+
if not self._stack:
|
|
368
|
+
# There is nothing to update
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
old_scene = self._scene
|
|
372
|
+
self._scene = self._scenes[self._stack[-1]]
|
|
373
|
+
|
|
374
|
+
if self._scene != old_scene or self._stack_changed:
|
|
375
|
+
self._stack_changed = False
|
|
376
|
+
if old_scene is not None:
|
|
377
|
+
self._data_from_scene = old_scene._exit_focus()
|
|
378
|
+
|
|
379
|
+
self._scene._enter_focus(self._data_for_scene)
|
|
380
|
+
|
|
381
|
+
if self._scene is not None:
|
|
382
|
+
return self._scene._update(elapsed_time)
|
|
383
|
+
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
def get_data_from_previous_scene(self) -> dict[str, Any]:
|
|
387
|
+
return self._data_from_scene
|
|
388
|
+
|
|
389
|
+
def draw(self) -> None:
|
|
390
|
+
if self._scene is None:
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
self._scene._draw()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def position_to_screen_rect(
|
|
397
|
+
position: Position, screen_pos: pygame.Vector2, screen_size: pygame.Vector2
|
|
398
|
+
) -> tuple[pygame.Vector2, pygame.Vector2]:
|
|
399
|
+
v_pos = screen_pos
|
|
400
|
+
v_size = screen_size
|
|
401
|
+
if position in (Position.TOP, Position.TOP_LEFT, Position.TOP_RIGHT):
|
|
402
|
+
v_size.y /= 2
|
|
403
|
+
|
|
404
|
+
if position in (Position.BOTTOM, Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT):
|
|
405
|
+
v_size.y /= 2
|
|
406
|
+
v_pos.y = v_size.y
|
|
407
|
+
|
|
408
|
+
if position in (Position.LEFT, Position.TOP_LEFT, Position.BOTTOM_LEFT):
|
|
409
|
+
v_size.x /= 2
|
|
410
|
+
|
|
411
|
+
if position in (Position.RIGHT, Position.TOP_RIGHT, Position.BOTTOM_RIGHT):
|
|
412
|
+
v_size.x /= 2
|
|
413
|
+
v_pos.x = v_size.x
|
|
414
|
+
|
|
415
|
+
return v_pos, v_size
|
mima/layered/shape.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from pygame import Vector2
|
|
2
|
+
|
|
3
|
+
from mima.standalone.geometry import (
|
|
4
|
+
Circle,
|
|
5
|
+
Rect,
|
|
6
|
+
Shape,
|
|
7
|
+
contains,
|
|
8
|
+
copy_shape,
|
|
9
|
+
overlaps,
|
|
10
|
+
resolve_collision,
|
|
11
|
+
vmax,
|
|
12
|
+
vmin,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
VZERO = Vector2()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ShapeCollection:
|
|
19
|
+
def __init__(self, pos: Vector2 | None = None, *shapes: Shape):
|
|
20
|
+
self.pos: Vector2 = Vector2() if pos is None else pos
|
|
21
|
+
self._ori_shapes: list[Shape] = list(shapes)
|
|
22
|
+
self.shapes: list[Shape] = []
|
|
23
|
+
|
|
24
|
+
self.tl = Vector2()
|
|
25
|
+
self.br = Vector2()
|
|
26
|
+
|
|
27
|
+
for shape in self._ori_shapes:
|
|
28
|
+
if isinstance(shape, Rect):
|
|
29
|
+
if self.tl == VZERO:
|
|
30
|
+
self.tl = shape.pos
|
|
31
|
+
else:
|
|
32
|
+
self.tl = vmin(self.tl, shape.pos)
|
|
33
|
+
self.br = vmax(self.br, shape.pos + shape.size)
|
|
34
|
+
if isinstance(shape, Circle):
|
|
35
|
+
shape_tl = shape.pos.elementwise() - shape.radius
|
|
36
|
+
if self.tl == VZERO:
|
|
37
|
+
self.tl = shape_tl
|
|
38
|
+
else:
|
|
39
|
+
self.tl = vmin(self.tl, shape_tl)
|
|
40
|
+
self.br = vmax(self.br, shape_tl.elementwise() + 2 * shape.radius)
|
|
41
|
+
for shape in self._ori_shapes:
|
|
42
|
+
new_shape = copy_shape(shape)
|
|
43
|
+
new_shape.pos = shape.pos + self.pos
|
|
44
|
+
self.shapes.append(new_shape)
|
|
45
|
+
self.bounding_box = Rect(self.pos + self.tl, self.br - self.tl)
|
|
46
|
+
|
|
47
|
+
def update(self, pos: Vector2) -> None:
|
|
48
|
+
self.pos = pos
|
|
49
|
+
self.bounding_box.pos = self.pos + self.tl
|
|
50
|
+
for i, s in enumerate(self.shapes):
|
|
51
|
+
s.pos = self.pos + self._ori_shapes[i].pos
|
|
52
|
+
|
|
53
|
+
def contains(self, shape: Vector2 | Shape) -> bool:
|
|
54
|
+
self.bounding_box.pos = self.pos + self.tl
|
|
55
|
+
if not contains(self.bounding_box, shape):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
for i, s in enumerate(self.shapes):
|
|
59
|
+
s.pos = self.pos + self._ori_shapes[i].pos
|
|
60
|
+
if contains(s, shape):
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def overlaps(self, shape: Vector2 | Shape) -> bool:
|
|
66
|
+
self.bounding_box.pos = self.pos + self.tl
|
|
67
|
+
if not overlaps(self.bounding_box, shape):
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
for i, s in enumerate(self.shapes):
|
|
71
|
+
s.pos = self.pos + self._ori_shapes[i].pos
|
|
72
|
+
if overlaps(s, shape):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def resolve_collision(
|
|
78
|
+
self, shape: Shape, move_both: bool = False
|
|
79
|
+
) -> tuple[Vector2, Vector2]:
|
|
80
|
+
delta = Vector2()
|
|
81
|
+
shape = copy_shape(shape)
|
|
82
|
+
for i, s in enumerate(self.shapes):
|
|
83
|
+
s.pos = self.pos + self._ori_shapes[i].pos + delta
|
|
84
|
+
res = resolve_collision(s, shape, move_both)
|
|
85
|
+
|
|
86
|
+
delta = res[0] - s.pos + delta
|
|
87
|
+
if move_both:
|
|
88
|
+
shape.pos = res[1]
|
|
89
|
+
|
|
90
|
+
return self.pos + delta, shape.pos
|
|
91
|
+
|
|
92
|
+
def get_tl_pos(self) -> Vector2:
|
|
93
|
+
return self.pos + self.tl
|
|
94
|
+
|
|
95
|
+
def get_br_pos(self) -> Vector2:
|
|
96
|
+
return self.pos + self.br
|
|
97
|
+
|
|
98
|
+
def get_bounding_size(self) -> Vector2:
|
|
99
|
+
return self.bounding_box.size
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from pygame import Vector2
|
|
2
|
+
from typing_extensions import Any, Generic
|
|
3
|
+
|
|
4
|
+
from mima.layered.shape import ShapeCollection
|
|
5
|
+
from mima.standalone.geometry import shape_from_dict
|
|
6
|
+
from mima.standalone.sprite import GS, AnimatedSprite, D
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpriteWithShape(AnimatedSprite[GS, D], Generic[GS, D]):
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
super().__init__()
|
|
12
|
+
|
|
13
|
+
self.hitbox: ShapeCollection | None = None
|
|
14
|
+
self.shapes: dict[tuple[GS, D, int], ShapeCollection] = {}
|
|
15
|
+
|
|
16
|
+
def add_frame(
|
|
17
|
+
self, graphic_state: GS, direction: D, frame_data: dict[str, Any]
|
|
18
|
+
) -> None:
|
|
19
|
+
super().add_frame(graphic_state, direction, frame_data)
|
|
20
|
+
|
|
21
|
+
sprite_data = self.sprites[graphic_state][direction]
|
|
22
|
+
for i, hb in enumerate(sprite_data.hitboxes):
|
|
23
|
+
shapes = []
|
|
24
|
+
for s in hb:
|
|
25
|
+
shapes.append(shape_from_dict(s))
|
|
26
|
+
|
|
27
|
+
if not shapes:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
sc = ShapeCollection(Vector2(), *shapes)
|
|
31
|
+
|
|
32
|
+
self.shapes[(graphic_state, direction, i)] = sc
|
|
33
|
+
if self.hitbox is None:
|
|
34
|
+
self.hitbox = sc
|
|
35
|
+
|
|
36
|
+
def update(self, elapsed_time: float, graphic_state: GS, direction: D) -> bool:
|
|
37
|
+
if self._last_graphic_state is None:
|
|
38
|
+
self._last_graphic_state = graphic_state
|
|
39
|
+
if self._last_direction is None:
|
|
40
|
+
self._last_direction = direction
|
|
41
|
+
if not self.sprites:
|
|
42
|
+
return False
|
|
43
|
+
gs = self.sprites.get(graphic_state)
|
|
44
|
+
if gs is None:
|
|
45
|
+
msg = (
|
|
46
|
+
f"Sprite has no {graphic_state=}. Available states are "
|
|
47
|
+
f"{self.sprites.keys()}"
|
|
48
|
+
)
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
data = gs[direction]
|
|
51
|
+
update_vals = True
|
|
52
|
+
if (
|
|
53
|
+
graphic_state == self._last_graphic_state
|
|
54
|
+
and direction == self._last_direction
|
|
55
|
+
):
|
|
56
|
+
self._timer -= elapsed_time
|
|
57
|
+
if self._timer <= 0.0:
|
|
58
|
+
self._frame = (self._frame + 1) % data.n_frames()
|
|
59
|
+
self._timer += data.duration[self._frame]
|
|
60
|
+
else:
|
|
61
|
+
update_vals = False
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
# Something changed
|
|
65
|
+
self._frame = 0
|
|
66
|
+
self._timer = data.duration[self._frame]
|
|
67
|
+
|
|
68
|
+
if update_vals:
|
|
69
|
+
self._src_pos = data.offset[self._frame]
|
|
70
|
+
self._src_size = data.size[self._frame]
|
|
71
|
+
self._image = data.image[self._frame]
|
|
72
|
+
new_hitbox = self.shapes.get((graphic_state, direction, self._frame))
|
|
73
|
+
self.hitbox = new_hitbox if new_hitbox is not None else self.hitbox
|
|
74
|
+
|
|
75
|
+
self._last_graphic_state = graphic_state
|
|
76
|
+
self._last_direction = direction
|
|
77
|
+
|
|
78
|
+
return update_vals
|