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
@@ -0,0 +1,351 @@
1
+ import math
2
+
3
+ from pygame import Vector2
4
+ from typing_extensions import Generic, Protocol
5
+
6
+ from mima.integrated.entity import Entity, Nature, TEntity
7
+ from mima.layered.shape import ShapeCollection
8
+ from mima.standalone.geometry import Rect, shape_from_str
9
+ from mima.standalone.spatial import SpatialGrid
10
+ from mima.standalone.tiled_map import TiledObject, TiledTile, TiledTilemap, vfloor
11
+ from mima.standalone.transformed_view import TileTransformedView
12
+
13
+ VOFF = Vector2(0.1, 0.1)
14
+
15
+
16
+ class Drawable(Protocol):
17
+ def draw(self, ttv: TileTransformedView, cache: bool = False) -> None: ...
18
+
19
+ def get_pos(self) -> Vector2: ...
20
+
21
+
22
+ class Renderable:
23
+ def __init__(self, drawable: Drawable, layer: int, elevation: int) -> None:
24
+ self.drawable = drawable
25
+ self.layer = layer
26
+ self.elevation = elevation
27
+
28
+ def draw(self, ttv: TileTransformedView, cache: bool = False) -> None:
29
+ self.drawable.draw(ttv, cache)
30
+
31
+ def get_bottom(self) -> float:
32
+ return self.drawable.get_pos().y
33
+
34
+
35
+ class ObjectLoader(Generic[TEntity]):
36
+ def load_objects(self, obj: TiledObject) -> list[tuple[TEntity, int]]: ...
37
+
38
+
39
+ class LayeredMap(Generic[TEntity]):
40
+ def __init__(
41
+ self, tilemap: TiledTilemap, loader: ObjectLoader[TEntity] | None = None
42
+ ) -> None:
43
+ self._tilemap: TiledTilemap = tilemap
44
+ self._world_size: Vector2 = self._tilemap.world_size
45
+ self._cell_size: int = 8
46
+ self._map_objects: list[tuple[TEntity, int]] = []
47
+
48
+ self._background_layer: SpatialGrid[TEntity] = SpatialGrid[TEntity](
49
+ self._world_size, self._cell_size
50
+ )
51
+ self._collision_layer: SpatialGrid[TEntity] = SpatialGrid[TEntity](
52
+ self._world_size, self._cell_size
53
+ )
54
+ self._foreground_layer: SpatialGrid[TEntity] = SpatialGrid[TEntity](
55
+ self._world_size, self._cell_size
56
+ )
57
+ self._ui_layer: list[TEntity] = []
58
+ self._large_objects: list[TEntity] = []
59
+
60
+ self._loader: ObjectLoader | None = loader
61
+
62
+ self._visible_objects: set[TEntity] = set()
63
+ self._collisions: set[frozenset[TEntity]] = set()
64
+ self._prev_interactions: set[tuple[TEntity, TEntity]] = set()
65
+ self._curr_interactions: set[tuple[TEntity, TEntity]] = set()
66
+
67
+ self._tilemap.prerender_layers()
68
+ self.draw_grid: bool = False
69
+
70
+ def start_new_frame(self) -> None:
71
+ for grid in [
72
+ self._background_layer,
73
+ self._collision_layer,
74
+ self._foreground_layer,
75
+ ]:
76
+ objects = grid.get_all_objects()
77
+ redundants = [obj for obj in objects if obj.is_redundant]
78
+
79
+ for obj in redundants:
80
+ # TODO: query quests
81
+ obj.on_death()
82
+ grid.remove(obj)
83
+
84
+ self._tilemap.start_new_frame()
85
+
86
+ def update_global(self, elapsed_time: float) -> bool:
87
+ self._tilemap.update(elapsed_time)
88
+ it = [
89
+ (self._background_layer, self._background_layer.get_all_objects()),
90
+ (self._collision_layer, self._collision_layer.get_all_objects()),
91
+ (self._foreground_layer, self._foreground_layer.get_all_objects()),
92
+ ]
93
+ for grid, objects in it:
94
+ for obj in objects:
95
+ _update_object_global(elapsed_time, obj, grid)
96
+
97
+ for obj in self._ui_layer:
98
+ obj.update_global(elapsed_time)
99
+ return False
100
+
101
+ def update_visible(self, elapsed_time: float, ttv: TileTransformedView) -> bool:
102
+ self.view = Rect(
103
+ ttv.get_tl_tile() - Vector2(0.5, 0.5),
104
+ ttv.get_br_tile() - ttv.get_tl_tile() + Vector2(1.5, 1.5),
105
+ )
106
+
107
+ self._curr_interactions.clear()
108
+ self._collisions.clear()
109
+
110
+ visible_objects = self._collision_layer.get_objects_in_region(
111
+ self.view.pos, self.view.size
112
+ )
113
+
114
+ for obj in visible_objects:
115
+ obj.update_visible(elapsed_time, ttv)
116
+
117
+ if not obj.is_collider:
118
+ continue
119
+
120
+ elevation = math.floor(obj.elevation)
121
+ hitbox = obj.get_hitbox()
122
+ tl = vfloor(hitbox.get_tl_pos() - VOFF)
123
+ br = vfloor(hitbox.get_br_pos() + VOFF)
124
+ if obj.collides_with_map:
125
+ # Collision with map
126
+ for y in range(int(tl.y), int(br.y) + 1):
127
+ for x in range(int(tl.x), int(br.x) + 1):
128
+ tiles = self._tilemap.get_tiles(x, y, elevation)
129
+ new_pos = collide_with_tiles(hitbox, tiles, Vector2(x, y))
130
+ self.relocate_object(self._collision_layer, obj, new_pos)
131
+
132
+ tl = hitbox.get_tl_pos() - (VOFF * 2)
133
+ br = hitbox.get_br_pos() + (VOFF * 2)
134
+ obj_view = Rect(tl, br - tl)
135
+ close_objects = self._collision_layer.get_objects_in_region(
136
+ obj_view.pos, obj_view.size
137
+ )
138
+
139
+ # Collision with other objects
140
+ for other in close_objects:
141
+ if obj == other:
142
+ continue
143
+ # TODO: implement overlaps/resolve_collision for hitboxes
144
+ if obj.get_hitbox().overlaps(other.get_hitbox().bounding_box):
145
+ self._collisions.add(frozenset({obj, other}))
146
+
147
+ for obj1, obj2 in self._collisions:
148
+ collider = obj1
149
+ target = obj2
150
+
151
+ move_both = obj1.is_collider and obj2.is_collider
152
+ if not move_both and obj2.is_collider:
153
+ collider, target = obj2, obj1
154
+ elif move_both and id(obj1) > id(obj2):
155
+ collider, target = obj2, obj1
156
+
157
+ if obj1.collides_with_dyn and obj2.collides_with_dyn:
158
+ new_pos1, new_pos2 = collider.get_hitbox().resolve_collision(
159
+ target.get_hitbox().bounding_box, move_both
160
+ )
161
+ self.relocate_object(self._collision_layer, collider, new_pos1)
162
+ self.relocate_object(self._collision_layer, target, new_pos2)
163
+
164
+ self._curr_interactions.add((obj1, obj2))
165
+ self._curr_interactions.add((obj2, obj1))
166
+
167
+ entered = self._curr_interactions - self._prev_interactions
168
+ stayed = self._curr_interactions & self._prev_interactions
169
+ exited = self._prev_interactions - self._curr_interactions
170
+ for collider, target in entered:
171
+ target.on_interaction(collider, Nature.ENTER)
172
+ # elif obj.is_player: # implicitly: obj.collides_with_dyn==True
173
+ # # Check quests (somehow; they are not available here)
174
+ # # self.on_interaction(other, Nature.WALK)
175
+ for collider, target in stayed:
176
+ target.on_interaction(collider, Nature.WALK)
177
+ for collider, target in exited:
178
+ target.on_interaction(collider, Nature.EXIT)
179
+ self._prev_interactions = self._curr_interactions.copy()
180
+ return False
181
+
182
+ def draw(self, ttv: TileTransformedView) -> None:
183
+ renderables: list[Renderable] = []
184
+
185
+ for layer in self._tilemap.get_rendered_layers():
186
+ renderables.append(Renderable(layer, layer.layer, layer.elevation))
187
+
188
+ for layer, objects in enumerate(
189
+ [
190
+ self._background_layer.get_objects_in_region(
191
+ self.view.pos, self.view.size
192
+ ),
193
+ self._collision_layer.get_objects_in_region(
194
+ self.view.pos, self.view.size
195
+ ),
196
+ self._foreground_layer.get_objects_in_region(
197
+ self.view.pos, self.view.size
198
+ ),
199
+ ]
200
+ ):
201
+ for obj in objects:
202
+ renderables.append(Renderable(obj, layer, math.floor(obj.elevation)))
203
+
204
+ draw_sorted(renderables, ttv)
205
+
206
+ for obj in self._ui_layer:
207
+ obj.draw(ttv)
208
+
209
+ if self.draw_grid:
210
+ # cells = self._collision_layer.get_cells_in_region(
211
+ # self.view.pos, self.view.size
212
+ # )
213
+ # visible_cells = len(cells)
214
+ world_size = self._collision_layer._world_size # type: ignore[reportPrivateUsage]
215
+ cell_size = self._collision_layer._cell_size # type: ignore[reportPrivateUsage]
216
+ for i in range(0, int(world_size.x), cell_size):
217
+ ttv.draw_line(Vector2(i, 0), Vector2(i, world_size.y), (0, 255, 255))
218
+ for i in range(0, int(world_size.y), cell_size):
219
+ ttv.draw_line(Vector2(0, i), Vector2(world_size.x, i))
220
+
221
+ return
222
+
223
+ def relocate_object(
224
+ self, grid: SpatialGrid[TEntity], obj: TEntity, new_pos: Vector2
225
+ ) -> None:
226
+ obj.old_pos = obj.get_pos()
227
+ obj.set_pos(new_pos)
228
+ grid.relocate(obj, obj.old_pos, new_pos)
229
+
230
+ def add_to_layer(self, obj: TEntity, layer: int = 1) -> None:
231
+ if layer == 0:
232
+ self._background_layer.insert(
233
+ obj, obj.get_pos(), obj.get_hitbox().get_bounding_size()
234
+ )
235
+ elif layer == 2:
236
+ self._foreground_layer.insert(
237
+ obj, obj.get_pos(), obj.get_hitbox().get_bounding_size()
238
+ )
239
+ elif layer == 3:
240
+ self._ui_layer.append(obj)
241
+ else:
242
+ self._collision_layer.insert(
243
+ obj, obj.get_pos(), obj.get_hitbox().get_bounding_size()
244
+ )
245
+
246
+ def remove_from_layer(self, obj: TEntity, layer: int = 1) -> None:
247
+ if layer == 0:
248
+ self._background_layer.remove(obj)
249
+ elif layer == 2:
250
+ self._foreground_layer.remove(obj)
251
+ elif layer == 3 and obj in self._ui_layer:
252
+ self._ui_layer.remove(obj)
253
+ else:
254
+ self._collision_layer.remove(obj)
255
+
256
+ def populate_dynamics(self) -> None:
257
+ self._background_layer.clear()
258
+ self._collision_layer.clear()
259
+ self._foreground_layer.clear()
260
+
261
+ if self._map_objects:
262
+ for dyn, layer in self._map_objects:
263
+ dyn.on_interaction(dyn, Nature.RESUME)
264
+ self.add_to_layer(dyn, layer)
265
+ elif self._loader is not None:
266
+ for obj in self._tilemap.get_objects():
267
+ dyns = self._loader.load_objects(obj)
268
+ for dyn, layer in dyns:
269
+ self.add_to_layer(dyn, layer)
270
+ self._map_objects.append((dyn, layer))
271
+
272
+ def is_tile_free(
273
+ self, pos: Vector2, size: Vector2 | None = None, elevation: int = 0
274
+ ) -> bool:
275
+ size = Vector2(1, 1) if size is None else size
276
+
277
+ tiles = self._tilemap.get_tiles_in_area(pos, size, elevation)
278
+
279
+ free = True
280
+ for tile in tiles:
281
+ if tile.get_collision_boxes():
282
+ return False
283
+
284
+ # TODO: Check existing objects
285
+ return free
286
+
287
+ @property
288
+ def name(self) -> str:
289
+ return self._tilemap.name
290
+
291
+ @property
292
+ def tl(self) -> Vector2:
293
+ return self._tilemap.tl
294
+
295
+ @property
296
+ def br(self) -> Vector2:
297
+ return self._tilemap.br
298
+
299
+ @property
300
+ def n_objects(self) -> int:
301
+ return (
302
+ self._background_layer.n_objects
303
+ + self._collision_layer.n_objects
304
+ + self._foreground_layer.n_objects
305
+ + len(self._ui_layer)
306
+ )
307
+
308
+ def set_loader(self, loader: ObjectLoader[TEntity]) -> None:
309
+ self._loader = loader
310
+
311
+
312
+ def _update_object_global(
313
+ elapsed_time: float, obj: TEntity, grid: SpatialGrid[TEntity]
314
+ ) -> None:
315
+ obj.update_global(elapsed_time)
316
+ pos = obj.get_pos()
317
+ obj.old_pos = pos
318
+ new_pos = pos + obj.vel * obj.speed * elapsed_time
319
+ obj.set_pos(new_pos)
320
+ grid.relocate(obj, pos, new_pos)
321
+
322
+
323
+ def collide_with_tiles(
324
+ obj: ShapeCollection, tiles: list[TiledTile], tile_offset: Vector2
325
+ ) -> Vector2:
326
+ obj_origin = obj.pos
327
+
328
+ for tile in tiles:
329
+ hitboxes = tile.get_collision_boxes()
330
+
331
+ if not hitboxes:
332
+ continue
333
+
334
+ for hitbox in hitboxes:
335
+ shape = shape_from_str(
336
+ hitbox.shape, hitbox.pos + tile_offset, hitbox.size, hitbox.radius
337
+ )
338
+
339
+ if obj.overlaps(shape):
340
+ res = obj.resolve_collision(shape)
341
+ obj.pos = res[0]
342
+ new_pos = obj.pos
343
+ obj.pos = obj_origin
344
+ return new_pos
345
+
346
+
347
+ def draw_sorted(renderables: list[Renderable], ttv: TileTransformedView) -> None:
348
+ renderables.sort(key=lambda r: (r.layer, r.elevation, r.get_bottom()))
349
+
350
+ for r in renderables:
351
+ r.draw(ttv, cache=True)
@@ -0,0 +1,156 @@
1
+ from enum import Enum
2
+
3
+ from pygame import Vector2
4
+ from typing_extensions import Tuple
5
+
6
+ from mima.layered.shaped_sprite import SpriteWithShape
7
+ from mima.standalone.tiled_map import TiledTileset
8
+
9
+
10
+ class GraphicState(Enum):
11
+ STANDING = 0
12
+ WALKING = 1
13
+ ATTACKING = 2
14
+ DAMAGED = 3
15
+ CELEBRATING = 4
16
+ DEAD = 5
17
+ DEFEATED = 6
18
+ PUSHING = 7
19
+ OPEN = 8
20
+ CLOSED = 9
21
+ LOCKED = 10
22
+ OFF = 11
23
+ ON = 12
24
+ ICON = 13
25
+
26
+
27
+ class Direction4(Enum):
28
+ SOUTH = 0
29
+ WEST = 1
30
+ NORTH = 2
31
+ EAST = 3
32
+
33
+
34
+ Direction = Direction4
35
+
36
+
37
+ class Direction8(Enum):
38
+ SOUTH = 0
39
+ SOUTHWEST = 1
40
+ WEST = 2
41
+ NORTH_WEST = 3
42
+ NORTH = 4
43
+ NORTH_EAST = 5
44
+ EAST = 6
45
+ SOUTH_EAST = 7
46
+
47
+
48
+ def sprite_from_tileset(
49
+ tileset: TiledTileset, sprite_name: str
50
+ ) -> SpriteWithShape[GraphicState, Direction]:
51
+ sprite = SpriteWithShape[GraphicState, Direction]()
52
+
53
+ for tile in tileset.tiles.values():
54
+ if tile.get("sprite_name", "") != sprite_name:
55
+ continue
56
+
57
+ gs = GraphicState[tile.get("graphic_state", "standing").upper()]
58
+ d = Direction[tile.get("facing_direction", "south").upper()]
59
+ if tile.animated:
60
+ for frame in tile.frames:
61
+ sprite.add_frame(gs, d, frame.as_dict())
62
+ else:
63
+ sprite.add_frame(gs, d, tile.get_frame().as_dict())
64
+
65
+ return sprite
66
+
67
+
68
+ def sprite_from_tileset_gid(
69
+ tileset: TiledTileset, gid: int
70
+ ) -> SpriteWithShape[GraphicState, Direction]:
71
+ sprite = SpriteWithShape[GraphicState, Direction]()
72
+
73
+ tile = tileset.tiles[gid]
74
+ gs = GraphicState[tile.get("graphic_state", "standing").upper()]
75
+ d = Direction[tile.get("facing_direction", "south").upper()]
76
+
77
+ if tile.animated:
78
+ for frame in tile.frames:
79
+ sprite.add_frame(gs, d, frame.as_dict())
80
+ else:
81
+ sprite.add_frame(gs, d, tile.get_frame().as_dict())
82
+
83
+ return sprite
84
+
85
+
86
+ def vel_to_dir(v: Vector2):
87
+ direction = None
88
+ if abs(v.x) >= abs(v.y):
89
+ if v.x > 0:
90
+ direction = Direction.EAST
91
+ elif v.x < 0:
92
+ direction = Direction.WEST
93
+ else:
94
+ # TODO: Check for up and down
95
+ pass
96
+ elif abs(v.x) < abs(v.y):
97
+ if v.y > 0:
98
+ direction = Direction.SOUTH
99
+ elif v.y < 0:
100
+ direction = Direction.NORTH
101
+ else:
102
+ # TODO: Check for left and right
103
+ pass
104
+ return direction
105
+
106
+
107
+ def dir_to_vel(direction: Direction) -> Tuple[int, int]:
108
+ vx = vy = 0
109
+
110
+ if direction == Direction.SOUTH:
111
+ vy = 1
112
+ elif direction == Direction.EAST:
113
+ vx = 1
114
+ elif direction == Direction.NORTH:
115
+ vy = -1
116
+ elif direction == Direction.WEST:
117
+ vx = -1
118
+ return vx, vy
119
+
120
+
121
+ class Anchor(Enum):
122
+ TOP_LEFT = 0
123
+ TOP_RIGHT = 1
124
+ BOTTOM_LEFT = 2
125
+ BOTTOM_RIGHT = 3
126
+ CENTER = 4
127
+ TOP_CENTER = 5
128
+ RIGHT_CENTER = 6
129
+ BOTTOM_CENTER = 7
130
+ LEFT_CENTER = 8
131
+
132
+
133
+ def anchor_offset(anchor: Anchor) -> Vector2:
134
+ if anchor == Anchor.TOP_RIGHT:
135
+ return Vector2(-1, 0)
136
+ elif anchor == Anchor.BOTTOM_LEFT:
137
+ return Vector2(0, -1)
138
+ elif anchor == Anchor.BOTTOM_RIGHT:
139
+ return Vector2(-1, -1)
140
+ elif anchor == Anchor.CENTER:
141
+ return Vector2(-0.5, -0.5)
142
+ elif anchor == Anchor.TOP_CENTER:
143
+ return Vector2(-0.5, 0)
144
+ elif anchor == Anchor.RIGHT_CENTER:
145
+ return Vector2(-1, -0.5)
146
+ elif anchor == Anchor.BOTTOM_CENTER:
147
+ return Vector2(-0.5, -1.0)
148
+ elif anchor == Anchor.LEFT_CENTER:
149
+ return Vector2(0, -0.5)
150
+ else:
151
+ return Vector2()
152
+
153
+
154
+ class Until(Enum):
155
+ UNLOCK = 0
156
+ NEXT_UPDATE = 1
File without changes
mima/layered/assets.py ADDED
@@ -0,0 +1,56 @@
1
+ import csv
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ from typing_extensions import TypeAlias
6
+
7
+ from mima.standalone.tiled_map import MapManager
8
+
9
+ CSVContent: TypeAlias = list[dict[str, str]]
10
+ LOG = logging.getLogger(__name__)
11
+
12
+
13
+ class AssetManager(MapManager):
14
+ def __init__(self) -> None:
15
+ super().__init__()
16
+
17
+ self._csvs: dict[str, CSVContent] = {}
18
+
19
+ def _load_file(self, fp: Path, skip_invalid: bool = False) -> None:
20
+ fp = fp.resolve()
21
+ if str(fp) in self._loaded_paths:
22
+ return
23
+
24
+ _, suf = fp.parts[-1].rsplit(".")
25
+
26
+ if suf.lower() == "png":
27
+ self._load_image(fp)
28
+ elif suf.lower() == "csv":
29
+ self._load_csv(fp)
30
+ else:
31
+ super()._load_file(fp, skip_invalid)
32
+
33
+ def _load_csv(self, path: Path) -> str:
34
+ name = Path(path).parts[-1].rsplit(".", 1)[0]
35
+ if name in self._csvs:
36
+ return name
37
+
38
+ with path.open("r") as csv_file:
39
+ reader = csv.DictReader(csv_file, delimiter=",")
40
+ self._csvs[name] = [r for r in reader]
41
+
42
+ self._loaded_paths[str(path)] = name
43
+
44
+ return name
45
+
46
+ def get_csv(self, name: str | Path) -> CSVContent:
47
+ name = str(name)
48
+ if name in self._csvs:
49
+ return self._csvs[name]
50
+ if name in self._loaded_paths:
51
+ return self._csvs[self._loaded_paths[name]]
52
+
53
+ LOG.info("CSV '%s' not found. Attempting to read from disk ...", name)
54
+ self._load_csv(Path(name))
55
+
56
+ return self._csvs[self._loaded_paths[name]]