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.
Files changed (153) hide show
  1. mima/__init__.py +4 -0
  2. mima/backend/__init__.py +1 -0
  3. mima/backend/pygame_assets.py +401 -0
  4. mima/backend/pygame_audio.py +78 -0
  5. mima/backend/pygame_backend.py +603 -0
  6. mima/backend/pygame_camera.py +63 -0
  7. mima/backend/pygame_events.py +695 -0
  8. mima/backend/touch_control_scheme_a.py +126 -0
  9. mima/backend/touch_control_scheme_b.py +132 -0
  10. mima/core/__init__.py +0 -0
  11. mima/core/collision.py +325 -0
  12. mima/core/database.py +58 -0
  13. mima/core/engine.py +367 -0
  14. mima/core/mode_engine.py +81 -0
  15. mima/core/scene_engine.py +81 -0
  16. mima/integrated/__init__.py +0 -0
  17. mima/integrated/entity.py +183 -0
  18. mima/integrated/layered_map.py +351 -0
  19. mima/integrated/sprite.py +156 -0
  20. mima/layered/__init__.py +0 -0
  21. mima/layered/assets.py +56 -0
  22. mima/layered/scene.py +415 -0
  23. mima/layered/shape.py +99 -0
  24. mima/layered/shaped_sprite.py +78 -0
  25. mima/layered/virtual_input.py +302 -0
  26. mima/maps/__init__.py +0 -0
  27. mima/maps/template.py +71 -0
  28. mima/maps/tile.py +20 -0
  29. mima/maps/tile_animation.py +7 -0
  30. mima/maps/tile_info.py +10 -0
  31. mima/maps/tile_layer.py +52 -0
  32. mima/maps/tiled/__init__.py +0 -0
  33. mima/maps/tiled/tiled_layer.py +48 -0
  34. mima/maps/tiled/tiled_map.py +95 -0
  35. mima/maps/tiled/tiled_object.py +79 -0
  36. mima/maps/tiled/tiled_objectgroup.py +25 -0
  37. mima/maps/tiled/tiled_template.py +49 -0
  38. mima/maps/tiled/tiled_tile.py +90 -0
  39. mima/maps/tiled/tiled_tileset.py +51 -0
  40. mima/maps/tilemap.py +216 -0
  41. mima/maps/tileset.py +39 -0
  42. mima/maps/tileset_info.py +9 -0
  43. mima/maps/transition_map.py +146 -0
  44. mima/objects/__init__.py +0 -0
  45. mima/objects/animated_sprite.py +217 -0
  46. mima/objects/attribute_effect.py +26 -0
  47. mima/objects/attributes.py +126 -0
  48. mima/objects/creature.py +384 -0
  49. mima/objects/dynamic.py +206 -0
  50. mima/objects/effects/__init__.py +0 -0
  51. mima/objects/effects/colorize_screen.py +60 -0
  52. mima/objects/effects/debug_box.py +133 -0
  53. mima/objects/effects/light.py +103 -0
  54. mima/objects/effects/show_sprite.py +50 -0
  55. mima/objects/effects/walking_on_grass.py +70 -0
  56. mima/objects/effects/walking_on_water.py +57 -0
  57. mima/objects/loader.py +111 -0
  58. mima/objects/projectile.py +111 -0
  59. mima/objects/sprite.py +116 -0
  60. mima/objects/world/__init__.py +0 -0
  61. mima/objects/world/color_gate.py +67 -0
  62. mima/objects/world/color_switch.py +101 -0
  63. mima/objects/world/container.py +175 -0
  64. mima/objects/world/floor_switch.py +109 -0
  65. mima/objects/world/gate.py +178 -0
  66. mima/objects/world/light_source.py +121 -0
  67. mima/objects/world/logic_gate.py +157 -0
  68. mima/objects/world/movable.py +399 -0
  69. mima/objects/world/oneway.py +195 -0
  70. mima/objects/world/pickup.py +157 -0
  71. mima/objects/world/switch.py +179 -0
  72. mima/objects/world/teleport.py +308 -0
  73. mima/py.typed +0 -0
  74. mima/scripts/__init__.py +2 -0
  75. mima/scripts/command.py +38 -0
  76. mima/scripts/commands/__init__.py +0 -0
  77. mima/scripts/commands/add_quest.py +19 -0
  78. mima/scripts/commands/change_map.py +34 -0
  79. mima/scripts/commands/close_dialog.py +9 -0
  80. mima/scripts/commands/equip_weapon.py +23 -0
  81. mima/scripts/commands/give_item.py +26 -0
  82. mima/scripts/commands/give_resource.py +51 -0
  83. mima/scripts/commands/move_map.py +152 -0
  84. mima/scripts/commands/move_to.py +49 -0
  85. mima/scripts/commands/oneway_move.py +58 -0
  86. mima/scripts/commands/parallel.py +66 -0
  87. mima/scripts/commands/play_sound.py +13 -0
  88. mima/scripts/commands/present_item.py +53 -0
  89. mima/scripts/commands/progress_quest.py +12 -0
  90. mima/scripts/commands/quit_game.py +8 -0
  91. mima/scripts/commands/save_game.py +14 -0
  92. mima/scripts/commands/screen_fade.py +83 -0
  93. mima/scripts/commands/serial.py +69 -0
  94. mima/scripts/commands/set_facing_direction.py +21 -0
  95. mima/scripts/commands/set_spawn_map.py +17 -0
  96. mima/scripts/commands/show_choices.py +52 -0
  97. mima/scripts/commands/show_dialog.py +118 -0
  98. mima/scripts/commands/take_coins.py +23 -0
  99. mima/scripts/script_processor.py +61 -0
  100. mima/standalone/__init__.py +0 -0
  101. mima/standalone/camera.py +153 -0
  102. mima/standalone/geometry.py +1318 -0
  103. mima/standalone/multicolumn_list.py +54 -0
  104. mima/standalone/pixel_font.py +84 -0
  105. mima/standalone/scripting.py +145 -0
  106. mima/standalone/spatial.py +186 -0
  107. mima/standalone/sprite.py +158 -0
  108. mima/standalone/tiled_map.py +1247 -0
  109. mima/standalone/transformed_view.py +433 -0
  110. mima/standalone/user_input.py +563 -0
  111. mima/states/__init__.py +0 -0
  112. mima/states/game_state.py +189 -0
  113. mima/states/memory.py +28 -0
  114. mima/states/quest.py +71 -0
  115. mima/types/__init__.py +0 -0
  116. mima/types/alignment.py +7 -0
  117. mima/types/blend.py +8 -0
  118. mima/types/damage.py +42 -0
  119. mima/types/direction.py +44 -0
  120. mima/types/gate_color.py +7 -0
  121. mima/types/graphic_state.py +23 -0
  122. mima/types/keys.py +64 -0
  123. mima/types/mode.py +9 -0
  124. mima/types/nature.py +12 -0
  125. mima/types/object.py +22 -0
  126. mima/types/player.py +9 -0
  127. mima/types/position.py +13 -0
  128. mima/types/start.py +7 -0
  129. mima/types/terrain.py +9 -0
  130. mima/types/tile_collision.py +11 -0
  131. mima/types/weapon_slot.py +6 -0
  132. mima/types/window.py +44 -0
  133. mima/usables/__init__.py +0 -0
  134. mima/usables/item.py +51 -0
  135. mima/usables/weapon.py +68 -0
  136. mima/util/__init__.py +1 -0
  137. mima/util/colors.py +50 -0
  138. mima/util/constants.py +55 -0
  139. mima/util/functions.py +38 -0
  140. mima/util/input_defaults.py +170 -0
  141. mima/util/logging.py +51 -0
  142. mima/util/property.py +8 -0
  143. mima/util/runtime_config.py +327 -0
  144. mima/util/trading_item.py +23 -0
  145. mima/view/__init__.py +0 -0
  146. mima/view/camera.py +192 -0
  147. mima/view/mima_mode.py +618 -0
  148. mima/view/mima_scene.py +231 -0
  149. mima/view/mima_view.py +12 -0
  150. mima/view/mima_window.py +244 -0
  151. mima_engine-0.4.0.dist-info/METADATA +47 -0
  152. mima_engine-0.4.0.dist-info/RECORD +153 -0
  153. 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