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
|
@@ -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
|
mima/maps/tile_info.py
ADDED
mima/maps/tile_layer.py
ADDED
|
@@ -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
|
+
)
|