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,302 @@
1
+ import logging
2
+ import time
3
+ from dataclasses import dataclass
4
+
5
+ import pygame
6
+ from typing_extensions import Iterable, Protocol, TypeAlias
7
+
8
+ from mima.standalone.user_input import Input, InputManager, Player
9
+
10
+ LOG = logging.getLogger(__name__)
11
+
12
+
13
+ class InputResolver(Protocol):
14
+ def resolve(self, im: InputManager, player: Player): ...
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AnalogDirectionSource:
19
+ x: Input | None
20
+ y: Input | None
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class DigitalDirectionSource:
25
+ left: Input
26
+ right: Input
27
+ up: Input
28
+ down: Input
29
+
30
+
31
+ DirectionSource: TypeAlias = AnalogDirectionSource | DigitalDirectionSource
32
+
33
+
34
+ class DirectionResolver:
35
+ """Resolves directional intent as a Vector2.
36
+
37
+ Resolution order is significant, first non-zero source wins.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ sources: Iterable[DirectionSource],
43
+ deadzone: float = 0.5,
44
+ normalize: bool = True,
45
+ ) -> None:
46
+ self.sources = tuple(sources)
47
+ self.deadzone = deadzone
48
+ self.normalize = normalize
49
+
50
+ def resolve(self, im: InputManager, player: Player = Player.P1) -> pygame.Vector2:
51
+ for src in self.sources:
52
+ if isinstance(src, AnalogDirectionSource):
53
+ vec = self._resolve_analog(im, player, src)
54
+ else:
55
+ vec = self._resolve_digital(im, player, src)
56
+
57
+ if vec.length_squared() > 0:
58
+ if self.normalize and vec.length_squared() > 1:
59
+ vec = vec.normalize()
60
+ return vec
61
+
62
+ return pygame.Vector2()
63
+
64
+ def _resolve_analog(
65
+ self, im: InputManager, player: Player, src: AnalogDirectionSource
66
+ ) -> pygame.Vector2:
67
+ x = im.value(src.x, player) if src.x else 0.0
68
+ y = im.value(src.y, player) if src.y else 0.0
69
+
70
+ if abs(x) < self.deadzone:
71
+ x = 0.0
72
+ if abs(y) < self.deadzone:
73
+ y = 0.0
74
+
75
+ return pygame.Vector2(x, y)
76
+
77
+ def _resolve_digital(
78
+ self, im: InputManager, player: Player, src: DigitalDirectionSource
79
+ ) -> pygame.Vector2:
80
+ x = (1 if im.held(src.right, player) else 0) - (
81
+ 1 if im.held(src.left, player) else 0
82
+ )
83
+ y = (1 if im.held(src.down, player) else 0) - (
84
+ 1 if im.held(src.up, player) else 0
85
+ )
86
+ return pygame.Vector2(x, y)
87
+
88
+
89
+ class ButtonGroupResolver:
90
+ """Resolves a logical button as active if ANY source is active."""
91
+
92
+ def __init__(self, *inputs: Input) -> None:
93
+ self.inputs = inputs
94
+
95
+ def pressed(self, im: InputManager, player: Player = Player.P1) -> bool:
96
+ return any(im.pressed(i, player) for i in self.inputs)
97
+
98
+ def held(self, im: InputManager, player: Player = Player.P1) -> bool:
99
+ return any(im.held(i, player) for i in self.inputs)
100
+
101
+ def released(self, im: InputManager, player: Player = Player.P1) -> bool:
102
+ return any(im.released(i, player) for i in self.inputs)
103
+
104
+
105
+ class AxisSnapResolver:
106
+ """Converts an axis into digital directions.
107
+
108
+ The AxisSnapResolver allows analog sticks to behave like digital buttons, making
109
+ them suitable for menus, grids, and UI navigation while preserving unified input
110
+ semantics across keyboard, D-Pad, and controller.
111
+
112
+ """
113
+
114
+ def __init__(
115
+ self, axis: Input, negative: Input, positive: Input, deadzone: float = 0.5
116
+ ) -> None:
117
+ self.axis = axis
118
+ self.negative = negative
119
+ self.positive = positive
120
+ self.deadzone = deadzone
121
+ self._last_value: float = 0.0
122
+
123
+ def resolve(self, im: InputManager, player: Player = Player.P1) -> set[Input]:
124
+ value = im.value(self.axis, player)
125
+ result: set[Input] = set()
126
+
127
+ if value <= -self.deadzone and self._last_value > -self.deadzone:
128
+ result.add(self.negative)
129
+ elif value >= self.deadzone and self._last_value < self.deadzone:
130
+ result.add(self.positive)
131
+
132
+ self._last_value = value
133
+
134
+ return result
135
+
136
+
137
+ # def load_direction_resolver(data: dict) -> DirectionResolver:
138
+ # """TODO:"""
139
+ # sources = [
140
+ # AnalogDirectionSource(
141
+ # Input[src["x"]] if src.get("x") else None,
142
+ # Input[src["y"]] if src.get("y") else None,
143
+ # )
144
+ # for src in data["sources"]
145
+ # ]
146
+
147
+ # return DirectionResolver(
148
+ # sources=sources,
149
+ # deadzone=data.get("deadzone", 0.5),
150
+ # normalize=data.get("normalize", True),
151
+ # )
152
+
153
+
154
+ MOVE_DIRECTION = DirectionResolver(
155
+ sources=[
156
+ AnalogDirectionSource(Input.LEFT_STICK_X, Input.LEFT_STICK_Y),
157
+ DigitalDirectionSource(
158
+ left=Input.DPAD_LEFT,
159
+ right=Input.DPAD_RIGHT,
160
+ up=Input.DPAD_UP,
161
+ down=Input.DPAD_DOWN,
162
+ ),
163
+ DigitalDirectionSource(
164
+ left=Input.LEFT, right=Input.RIGHT, up=Input.UP, down=Input.DOWN
165
+ ),
166
+ ]
167
+ )
168
+
169
+ KEYBOARD_MOVE = DirectionResolver(
170
+ sources=[
171
+ DigitalDirectionSource(
172
+ left=Input.LEFT, right=Input.RIGHT, up=Input.UP, down=Input.DOWN
173
+ )
174
+ ]
175
+ )
176
+
177
+ CONTROLLER_MOVE = DirectionResolver(
178
+ sources=[
179
+ AnalogDirectionSource(Input.LEFT_STICK_X, Input.LEFT_STICK_Y),
180
+ DigitalDirectionSource(
181
+ left=Input.DPAD_LEFT,
182
+ right=Input.DPAD_RIGHT,
183
+ up=Input.DPAD_UP,
184
+ down=Input.DPAD_DOWN,
185
+ ),
186
+ ]
187
+ )
188
+
189
+ MENU_MOVE_X = AxisSnapResolver(
190
+ axis=Input.LEFT_STICK_X, negative=Input.LEFT, positive=Input.RIGHT, deadzone=0.25
191
+ )
192
+
193
+ MENU_MOVE_Y = AxisSnapResolver(
194
+ axis=Input.LEFT_STICK_Y, negative=Input.UP, positive=Input.DOWN, deadzone=0.25
195
+ )
196
+
197
+
198
+ def resolve_menu_directions(im: InputManager, player: Player = Player.P1) -> set[Input]:
199
+ directions: set[Input] = set()
200
+
201
+ # Analog stick -> digital
202
+ directions |= MENU_MOVE_X.resolve(im, player)
203
+ directions |= MENU_MOVE_Y.resolve(im, player)
204
+
205
+ # D-Pad and keyboard
206
+ if im.pressed(Input.DPAD_UP, player) or im.pressed(Input.UP):
207
+ directions.add(Input.UP)
208
+ if im.pressed(Input.DPAD_DOWN, player) or im.pressed(Input.DOWN):
209
+ directions.add(Input.DOWN)
210
+ if im.pressed(Input.DPAD_LEFT, player) or im.pressed(Input.LEFT):
211
+ directions.add(Input.LEFT)
212
+ if im.pressed(Input.DPAD_RIGHT, player) or im.pressed(Input.RIGHT):
213
+ directions.add(Input.RIGHT)
214
+
215
+ return directions
216
+
217
+
218
+ @dataclass
219
+ class MenuNavConfig:
220
+ initial_delay: float = 0.4
221
+ repeat_interval: float = 0.1
222
+ axis_threshold: float = 0.6
223
+
224
+
225
+ class MenuNavigationResolver:
226
+ """Resolves menu navigation intent from keyboard, d-pad, and stick."""
227
+
228
+ def __init__(self, config: MenuNavConfig | None = None) -> None:
229
+ self.config = config or MenuNavConfig()
230
+
231
+ # Per-player, per-direction state
232
+ self._held_since: dict[tuple[Player, Input], float] = {}
233
+ self._last_emit: dict[tuple[Player, Input], float] = {}
234
+
235
+ def resolve(self, im: InputManager, player: Player = Player.P1) -> set[Input]:
236
+ now = time.monotonic()
237
+ result: set[Input] = set()
238
+
239
+ # Collect raw directional state
240
+ active = self._collect_active_directions(im, player)
241
+
242
+ # Resolve edge and repeat
243
+ for direction in active:
244
+ key = (player, direction)
245
+
246
+ if key not in self._held_since:
247
+ # Fresh press
248
+ self._held_since[key] = now
249
+ self._last_emit[key] = now
250
+ result.add(direction)
251
+ continue
252
+
253
+ held_time = now - self._held_since[key]
254
+ since_last = now - self._last_emit[key]
255
+
256
+ if held_time >= self.config.initial_delay:
257
+ if since_last >= self.config.repeat_interval:
258
+ self._last_emit[key] = now
259
+ result.add(direction)
260
+
261
+ # Clear released directions
262
+ released = set(self._held_since.keys()) - {(player, d) for d in active}
263
+ for key in released:
264
+ self._held_since.pop(key, None)
265
+ self._last_emit.pop(key, None)
266
+
267
+ return result
268
+
269
+ def _collect_active_directions(
270
+ self, im: InputManager, player: Player
271
+ ) -> set[Input]:
272
+ result: set[Input] = set()
273
+
274
+ # D-Pad and keyboard
275
+ if im.held(Input.DPAD_UP, player) or im.held(Input.UP):
276
+ result.add(Input.UP)
277
+ if im.held(Input.DPAD_DOWN, player) or im.held(Input.DOWN):
278
+ result.add(Input.DOWN)
279
+ if im.held(Input.DPAD_LEFT, player) or im.held(Input.LEFT):
280
+ result.add(Input.LEFT)
281
+ if im.held(Input.DPAD_RIGHT, player) or im.held(Input.RIGHT):
282
+ result.add(Input.RIGHT)
283
+
284
+ # Stick (snapped)
285
+ x = im.value(Input.LEFT_STICK_X, player)
286
+ y = im.value(Input.LEFT_STICK_Y, player)
287
+
288
+ if x <= -self.config.axis_threshold:
289
+ result.add(Input.LEFT)
290
+ elif x >= self.config.axis_threshold:
291
+ result.add(Input.RIGHT)
292
+ if y <= -self.config.axis_threshold:
293
+ result.add(Input.UP)
294
+ elif y >= self.config.axis_threshold:
295
+ result.add(Input.DOWN)
296
+
297
+ return result
298
+
299
+
300
+ MENU_NAV = MenuNavigationResolver(
301
+ MenuNavConfig(initial_delay=0.35, repeat_interval=0.08, axis_threshold=0.65)
302
+ )
mima/maps/__init__.py ADDED
File without changes
mima/maps/template.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Dict, Optional, Union
4
+
5
+ from ..util.functions import strtobool
6
+ from ..util.property import Property
7
+
8
+ if TYPE_CHECKING:
9
+ from ..engine import MimaEngine
10
+
11
+
12
+ class Template:
13
+ engine: MimaEngine
14
+
15
+ def __init__(self, name: str):
16
+ self.name: str = name
17
+ self.properties: Dict[str, Property] = {}
18
+
19
+ def get_string(self, key: str, default_val: str = "") -> str:
20
+ if key in self.properties:
21
+ return self.properties[key].value
22
+ else:
23
+ return default_val
24
+
25
+ def get_int(self, key: str, default_val: int = 0) -> int:
26
+ if key in self.properties:
27
+ return int(self.properties[key].value)
28
+ else:
29
+ return default_val
30
+
31
+ def get_float(self, key: str, default_val: float = 0.0) -> float:
32
+ if key in self.properties:
33
+ return float(self.properties[key].value)
34
+ else:
35
+ return default_val
36
+
37
+ def get_bool(self, key: str, default_val: bool = False) -> bool:
38
+ if key in self.properties:
39
+ return bool(strtobool(self.properties[key].value))
40
+ else:
41
+ return default_val
42
+
43
+ def get(
44
+ self, key: str, default_val: Optional[str, int, float, bool] = None
45
+ ) -> Union[str, int, float, bool]:
46
+
47
+ if key in self.properties:
48
+ if self.properties[key].dtype == "str":
49
+ if default_val is None:
50
+ return self.get_string(key)
51
+ return self.get_string(key, default_val)
52
+ if self.properties[key].dtype == "int":
53
+ if default_val is None:
54
+ return self.get_int(key)
55
+ return self.get_int(key, int(default_val))
56
+ if self.properties[key].dtype == "float":
57
+ if default_val is None:
58
+ return self.get_float(key)
59
+ return self.get_float(key, float(default_val))
60
+ if self.properties[key].dtype == "bool":
61
+ if default_val is None:
62
+ return self.get_bool(key)
63
+ elif not isinstance(default_val, bool):
64
+ msg = (
65
+ "Trying to access a bool value but default value "
66
+ f"{default_val} is of type {type(default_val)}"
67
+ )
68
+ raise TypeError(msg)
69
+ return self.get_bool(key, default_val)
70
+
71
+ return default_val
mima/maps/tile.py ADDED
@@ -0,0 +1,20 @@
1
+ from ..types.direction import Direction
2
+ from ..types.graphic_state import GraphicState
3
+ from ..types.terrain import Terrain
4
+
5
+
6
+ class Tile:
7
+ def __init__(self):
8
+ self.basic_tile_id: int = 0
9
+ self.tile_id: int = 0
10
+ self.solid: bool = False
11
+ self.animated: bool = False
12
+ self.pz: float = 0.0
13
+ self.z_height: float = 0.0
14
+ self.terrain: Terrain = Terrain.DEFAULT
15
+ self.facing_direction: Direction = Direction.SOUTH
16
+ self.graphic_state: GraphicState = GraphicState.STANDING
17
+ self.sprite_name: str = ""
18
+
19
+ def update(self, elapsed_time: float) -> bool:
20
+ return True
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class TileAnimation:
6
+ frame_id: int
7
+ duration: float
mima/maps/tile_info.py ADDED
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .tile import Tile
4
+ from .tileset import Tileset
5
+
6
+
7
+ @dataclass
8
+ class TileInfo:
9
+ tileset: Tileset
10
+ tile: Tile
@@ -0,0 +1,52 @@
1
+ import math
2
+ from typing import List
3
+
4
+
5
+ class TileLayer:
6
+ def __init__(self):
7
+ self.name: str = "Unnamed Layer"
8
+ self.layer_id: int = 0
9
+ self.width: int = 0
10
+ self.height: int = 0
11
+ self.layer_pos: int = 0
12
+ self.speed_x: float = 0.0
13
+ self.speed_y: float = 0.0
14
+ self.layer_ox: float = 0.0
15
+ self.layer_oy: float = 0.0
16
+ self.parallax_x: float = 0.0 # Not used yet
17
+ self.parallax_y: float = 0.0 # Not used yet
18
+
19
+ self.indices: List[int] = []
20
+
21
+ def update(self, elapsed_time: float):
22
+ self.layer_ox += self.speed_x * elapsed_time
23
+ self.layer_oy += self.speed_y * elapsed_time
24
+
25
+ if self.layer_ox > self.width:
26
+ self.layer_ox -= self.width
27
+ if self.layer_ox < 0:
28
+ self.layer_ox += self.width
29
+ if self.layer_oy > self.height:
30
+ self.layer_oy -= self.height
31
+ if self.layer_oy < 0:
32
+ self.layer_oy += self.height
33
+
34
+ def get_index(self, px: float, py: float) -> int:
35
+ if self.layer_ox != 0.0:
36
+ px = math.floor(px - self.layer_ox)
37
+ if px > self.width:
38
+ px -= self.width
39
+ while px < 0:
40
+ px += self.width
41
+
42
+ if self.layer_oy != 0.0:
43
+ py = math.floor(py - self.layer_oy)
44
+ if py > self.height:
45
+ py -= self.height
46
+ while py < 0:
47
+ py += self.height
48
+
49
+ if 0 <= px < self.width and 0 <= py < self.height:
50
+ return self.indices[py * self.width + px]
51
+ else:
52
+ return 0
File without changes
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ from io import StringIO
5
+ from typing import TYPE_CHECKING, List
6
+
7
+ from ..tile_layer import TileLayer
8
+
9
+ if TYPE_CHECKING:
10
+ from xml.etree.ElementTree import Element
11
+
12
+
13
+ class TiledLayer(TileLayer):
14
+ def __init__(self, l_xtree: Element):
15
+ super().__init__()
16
+
17
+ self.name: str = l_xtree.attrib["name"]
18
+ self.layer_id: int = int(l_xtree.attrib["id"])
19
+ self.type: str = l_xtree.attrib.get("class", "NormalLayer")
20
+ if "Foreground" in self.type:
21
+ # FIXME: Hack, propertytypes.json should be read and applied
22
+ self.layer_pos = 1
23
+ self.width: int = int(l_xtree.attrib["width"])
24
+ self.height: int = int(l_xtree.attrib["height"])
25
+ self.indices: List[int] = []
26
+
27
+ layer_data = l_xtree.findall("data")[0]
28
+ reader = csv.reader(StringIO(layer_data.text), delimiter=",")
29
+
30
+ for row in reader:
31
+ if len(row) <= 0:
32
+ continue
33
+
34
+ for entry in row:
35
+ try:
36
+ self.indices.append(int(entry))
37
+ except ValueError:
38
+ pass # Empty string
39
+
40
+ property_data = l_xtree.findall("properties")
41
+ for pdata in property_data:
42
+ properties = pdata.findall("property")
43
+
44
+ for prop in properties:
45
+ if prop.attrib["name"] == "layer":
46
+ self.layer_pos = int(prop.attrib["value"])
47
+ if prop.attrib["name"] == "speed":
48
+ self.speed_x = float(prop.attrib.get("value", 0.0))
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from typing import List
6
+ from xml.etree import ElementTree
7
+
8
+ from ...util.property import Property
9
+ from ..tilemap import Tilemap
10
+ from ..tileset_info import TilesetInfo
11
+ from .tiled_layer import TiledLayer
12
+ from .tiled_objectgroup import TiledObjectgroup
13
+
14
+ # if TYPE_CHECKING:
15
+ # from ..engine import Avare
16
+
17
+
18
+ LOG = logging.getLogger(__name__)
19
+
20
+
21
+ class TiledMap(Tilemap):
22
+ def __init__(self, name: str, filename: str = ""):
23
+ super().__init__(name)
24
+
25
+ if filename == "":
26
+ filename = f"{name}.tmx"
27
+
28
+ if not os.path.isfile(filename):
29
+ filename = os.path.join(
30
+ self.engine.backend.data_path,
31
+ "maps",
32
+ os.path.split(filename)[-1],
33
+ )
34
+
35
+ self._objects: List[TiledObjectgroup] = []
36
+
37
+ if not os.path.isfile(filename):
38
+ filename = os.path.join(
39
+ self.engine.backend.data_path, "maps", filename
40
+ )
41
+
42
+ LOG.info("Loading map %s from TMX file '%s' ...", name, filename)
43
+ tree = ElementTree.parse(filename)
44
+ LOG.debug("Loaded file %s successfully.", filename)
45
+ root = tree.getroot()
46
+
47
+ LOG.debug("Loading map properties ...")
48
+ self.width = int(root.attrib["width"])
49
+ self.height = int(root.attrib["height"])
50
+ self.tile_width = int(root.attrib["tilewidth"])
51
+ self.tile_height = int(root.attrib["tileheight"])
52
+
53
+ LOG.debug("Loading properties ...")
54
+ properties = root.findall("properties")
55
+ if properties:
56
+ properties = properties[0].findall("property")
57
+ for p in properties:
58
+ pname = p.attrib["name"]
59
+ self.properties[pname] = Property(
60
+ name=pname,
61
+ dtype=p.attrib.get("type", "str"),
62
+ value=p.attrib["value"],
63
+ )
64
+
65
+ LOG.debug("Loading tilesets ...")
66
+ tilesets = root.findall("tileset") # Only one tileset
67
+ for tileset in tilesets:
68
+ tname = os.path.split(tileset.attrib["source"])[-1][:-4]
69
+ first_gid = int(tileset.attrib["firstgid"])
70
+ self._tilesets.append(
71
+ TilesetInfo(
72
+ tileset=self.engine.assets.get_tileset(tname),
73
+ first_gid=first_gid,
74
+ )
75
+ )
76
+
77
+ LOG.debug("Loading layers ...")
78
+ layers = root.findall("layer")
79
+ for layer in layers:
80
+ self._layers.append(TiledLayer(layer))
81
+
82
+ LOG.debug("Loading objects ...")
83
+ objectgroups = root.findall("objectgroup")
84
+ for objectgroup in objectgroups:
85
+ self._objects.append(TiledObjectgroup(objectgroup))
86
+
87
+ LOG.info("Map %s successfully loaded.", self.name)
88
+
89
+ @property
90
+ def objects(self):
91
+ all_objects = []
92
+ for group in self._objects:
93
+ all_objects += group.objects
94
+
95
+ return all_objects
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ...util.property import Property
7
+ from ..template import Template
8
+
9
+ if TYPE_CHECKING:
10
+ from xml.etree.ElementTree import Element
11
+
12
+ from .tiled_template import TiledTemplate
13
+ from .tiled_tileset import TiledTileset
14
+
15
+
16
+ class TiledObject(Template):
17
+ def __init__(self, o_xtree: Element):
18
+ super().__init__("TiledObject")
19
+ self.object_id: int = int(o_xtree.attrib["id"])
20
+
21
+ self.name: str = o_xtree.attrib.get("name", "Unnamed")
22
+ self.type: str = o_xtree.attrib.get(
23
+ "type", o_xtree.attrib.get("class", "Untyped")
24
+ )
25
+ self.px: float = float(o_xtree.attrib["x"])
26
+ self.py: float = float(o_xtree.attrib["y"])
27
+
28
+ tsource = o_xtree.attrib.get("template", None)
29
+ if tsource is not None:
30
+ tname = os.path.split(tsource)[-1].split(".")[0]
31
+ tpl: TiledTemplate = self.engine.assets.get_template(tname)
32
+
33
+ self.name = tpl.oname
34
+ self.type = tpl.otype
35
+ self.width = tpl.width
36
+ self.height = tpl.height
37
+ # Templates' y positions are bottom instead of top
38
+ self.py -= self.height
39
+
40
+ for key, prop in tpl.properties.items():
41
+ self.properties[key] = Property(
42
+ name=prop.name, dtype=prop.dtype, value=prop.value
43
+ )
44
+
45
+ ts: TiledTileset = self.engine.assets.get_tileset(tpl.tileset_name)
46
+ self.properties["tileset_name"] = Property(
47
+ name="tileset_name", dtype="str", value=tpl.tileset_name
48
+ )
49
+ self.properties["image_name"] = Property(
50
+ name="image_name", dtype="str", value=""
51
+ )
52
+ self.properties["sprite_offset_x"] = Property(
53
+ name="sprite_offset_x",
54
+ dtype="int",
55
+ value=f"{(tpl.gid - tpl.first_gid) % ts.columns}",
56
+ )
57
+
58
+ self.properties["sprite_offset_y"] = Property(
59
+ name="sprite_offset_y",
60
+ dtype="int",
61
+ value=f"{(tpl.gid - tpl.first_gid) // ts.columns}",
62
+ )
63
+ else:
64
+ self.width = float(o_xtree.attrib["width"])
65
+ self.height = float(o_xtree.attrib["height"])
66
+
67
+ if self.type == "container":
68
+ self.engine.total_chests += 1
69
+ props = o_xtree.findall("properties")
70
+ if props:
71
+ props = props[0].findall("property")
72
+
73
+ for p in props:
74
+ pname = p.attrib["name"]
75
+ self.properties[pname] = Property(
76
+ name=pname,
77
+ dtype=p.attrib.get("type", "str"),
78
+ value=p.attrib["value"],
79
+ )