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,1247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from xml.etree import ElementTree
|
|
10
|
+
from xml.etree.ElementTree import Element
|
|
11
|
+
|
|
12
|
+
import pygame
|
|
13
|
+
from pygame import Vector2
|
|
14
|
+
from typing_extensions import (
|
|
15
|
+
TYPE_CHECKING,
|
|
16
|
+
Any,
|
|
17
|
+
Protocol,
|
|
18
|
+
Self,
|
|
19
|
+
TypeAlias,
|
|
20
|
+
Union,
|
|
21
|
+
overload,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from pygame import Surface
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
LOG = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
Property: TypeAlias = str | int | float | bool
|
|
31
|
+
TileCache: TypeAlias = dict[tuple[int, tuple[int, int]], tuple[int, int]]
|
|
32
|
+
CollisionBoxType: TypeAlias = dict[str, str | float | Vector2]
|
|
33
|
+
TileAnimationType: TypeAlias = dict[
|
|
34
|
+
str, Union[int, float, tuple[int, int], "Surface", list[CollisionBoxType]]
|
|
35
|
+
]
|
|
36
|
+
VZERO = Vector2()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _CollisionBox:
|
|
41
|
+
shape: str
|
|
42
|
+
pos: Vector2
|
|
43
|
+
radius: float
|
|
44
|
+
size: Vector2
|
|
45
|
+
|
|
46
|
+
def as_dict(self) -> CollisionBoxType:
|
|
47
|
+
return {
|
|
48
|
+
"shape": self.shape,
|
|
49
|
+
"pos": self.pos,
|
|
50
|
+
"size": self.size,
|
|
51
|
+
"radius": self.radius,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class _TileAnimation:
|
|
57
|
+
frame_id: int
|
|
58
|
+
duration: float
|
|
59
|
+
offset: tuple[int, int]
|
|
60
|
+
size: tuple[int, int]
|
|
61
|
+
image: pygame.Surface
|
|
62
|
+
collision_boxes: list[_CollisionBox]
|
|
63
|
+
|
|
64
|
+
def as_dict(self) -> TileAnimationType:
|
|
65
|
+
return {
|
|
66
|
+
"frame_id": self.frame_id,
|
|
67
|
+
"duration": self.duration,
|
|
68
|
+
"offset": self.offset,
|
|
69
|
+
"size": self.size,
|
|
70
|
+
"image": self.image,
|
|
71
|
+
"collision": [cb.as_dict() for cb in self.collision_boxes],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class _TilesetInfo:
|
|
77
|
+
tileset: TiledTileset
|
|
78
|
+
first_gid: int
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class _TileInfo:
|
|
83
|
+
tileset: TiledTileset
|
|
84
|
+
tile: TiledTile
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Renderer(Protocol):
|
|
88
|
+
def draw_surface(
|
|
89
|
+
self,
|
|
90
|
+
pos: Vector2,
|
|
91
|
+
surf: pygame.Surface,
|
|
92
|
+
*,
|
|
93
|
+
src_pos: Vector2 | None = None,
|
|
94
|
+
src_size: Vector2 | None = None,
|
|
95
|
+
scale: float = 1.0,
|
|
96
|
+
angle: float = 0,
|
|
97
|
+
cache: bool = False,
|
|
98
|
+
special_flags: int = 0,
|
|
99
|
+
) -> None: ...
|
|
100
|
+
|
|
101
|
+
def get_tl_tile(self) -> Vector2: ...
|
|
102
|
+
|
|
103
|
+
def get_br_tile(self) -> Vector2: ...
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _TiledBase:
|
|
107
|
+
def __init__(self, mapman: MapManager, name: str = "Unnamed Object") -> None:
|
|
108
|
+
self.mapman = mapman
|
|
109
|
+
self.name = name
|
|
110
|
+
self.props: dict[str, Property] = {}
|
|
111
|
+
|
|
112
|
+
@overload
|
|
113
|
+
def get(self, key: str, default: bool) -> bool: ...
|
|
114
|
+
@overload
|
|
115
|
+
def get(self, key: str, default: str) -> str: ...
|
|
116
|
+
@overload
|
|
117
|
+
def get(self, key: str, default: int) -> int: ...
|
|
118
|
+
@overload
|
|
119
|
+
def get(self, key: str, default: float) -> float: ...
|
|
120
|
+
@overload
|
|
121
|
+
def get(self, key: str, default: None) -> Property: ...
|
|
122
|
+
|
|
123
|
+
def get(self, key: str, default: Property | None = None) -> Property | None:
|
|
124
|
+
return self.props.get(key, default)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TiledTile(_TiledBase):
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
mapman: MapManager,
|
|
131
|
+
image: pygame.Surface,
|
|
132
|
+
width: int = 0,
|
|
133
|
+
height: int = 0,
|
|
134
|
+
ts_cols: int = 0,
|
|
135
|
+
) -> None:
|
|
136
|
+
super().__init__(mapman, "TiledTile")
|
|
137
|
+
|
|
138
|
+
self.image = image
|
|
139
|
+
self.size: tuple[int, int] = (width, height)
|
|
140
|
+
self.ts_cols: int = ts_cols
|
|
141
|
+
self.basic_tile_id: int = 0
|
|
142
|
+
self.current_tile_id: int = 0
|
|
143
|
+
self.animated: bool = False
|
|
144
|
+
self._tile_type: str = "tile"
|
|
145
|
+
self.frames: list[_TileAnimation]
|
|
146
|
+
self._frame: int = 0
|
|
147
|
+
self._n_frames: int = 0
|
|
148
|
+
self._frame_timer: float = 0.0
|
|
149
|
+
self._collision_objects: list[_CollisionBox] = []
|
|
150
|
+
self._groups: list[_TiledObjectGroup] = []
|
|
151
|
+
self.z_height: float = 0.01
|
|
152
|
+
|
|
153
|
+
def load_from_xml(self, t_xtree: Element, path: Path) -> Self:
|
|
154
|
+
self._tile_type = t_xtree.attrib.get(
|
|
155
|
+
"class", t_xtree.attrib.get("type", "tile")
|
|
156
|
+
)
|
|
157
|
+
self.basic_tile_id = int(t_xtree.attrib["id"])
|
|
158
|
+
|
|
159
|
+
self._groups = [
|
|
160
|
+
_TiledObjectGroup(self.mapman).load_from_xml(og, path)
|
|
161
|
+
for og in t_xtree.findall("objectgroup")
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
tile_size = Vector2(self.size)
|
|
165
|
+
for group in self._groups:
|
|
166
|
+
for obj in group._objects:
|
|
167
|
+
pos = obj.pos.elementwise() / tile_size.elementwise()
|
|
168
|
+
size = obj.size.elementwise() / tile_size.elementwise()
|
|
169
|
+
if obj.type == "circle":
|
|
170
|
+
shape = "circle"
|
|
171
|
+
radius = obj.size.x / tile_size.x / 2.0
|
|
172
|
+
pos = pos.elementwise() + radius
|
|
173
|
+
else:
|
|
174
|
+
shape = "rect"
|
|
175
|
+
radius = 0
|
|
176
|
+
self._collision_objects.append(
|
|
177
|
+
_CollisionBox(shape=shape, pos=pos, radius=radius, size=size)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def pos_from_id(tile_id: int) -> tuple[int, int]:
|
|
181
|
+
sx = (tile_id % self.ts_cols) * self.size[0]
|
|
182
|
+
sy = (tile_id // self.ts_cols) * self.size[1]
|
|
183
|
+
return int(sx), int(sy)
|
|
184
|
+
|
|
185
|
+
animation = t_xtree.findall("animation")
|
|
186
|
+
|
|
187
|
+
if animation:
|
|
188
|
+
frames = animation[0].findall("frame")
|
|
189
|
+
self.frames = [
|
|
190
|
+
_TileAnimation(
|
|
191
|
+
frame_id=int(f.attrib["tileid"]),
|
|
192
|
+
duration=int(f.attrib["duration"]) / 1000.0,
|
|
193
|
+
offset=pos_from_id(int(f.attrib["tileid"])),
|
|
194
|
+
size=self.size,
|
|
195
|
+
image=self.image,
|
|
196
|
+
collision_boxes=(
|
|
197
|
+
self._collision_objects
|
|
198
|
+
if int(f.attrib["tileid"]) == self.basic_tile_id
|
|
199
|
+
else []
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
for f in frames
|
|
203
|
+
]
|
|
204
|
+
self.animated = True
|
|
205
|
+
else:
|
|
206
|
+
self.frames = [
|
|
207
|
+
_TileAnimation(
|
|
208
|
+
frame_id=self.basic_tile_id,
|
|
209
|
+
duration=0.0,
|
|
210
|
+
offset=pos_from_id(self.basic_tile_id),
|
|
211
|
+
size=self.size,
|
|
212
|
+
image=self.image,
|
|
213
|
+
collision_boxes=self._collision_objects,
|
|
214
|
+
)
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
self._n_frames = len(self.frames)
|
|
218
|
+
self.current_tile_id = self.frames[0].frame_id
|
|
219
|
+
self._frame_timer = self.frames[0].duration
|
|
220
|
+
|
|
221
|
+
self.props = read_properties_to_dict(t_xtree)
|
|
222
|
+
self.z_height = self.get("z_height", 0.01)
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def update(self, elapsed_time: float) -> bool:
|
|
226
|
+
"""Update tile and return True on new frame."""
|
|
227
|
+
if self._n_frames <= 1:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
self._frame_timer -= elapsed_time
|
|
231
|
+
if self._frame_timer <= 0:
|
|
232
|
+
self._frame = (self._frame + 1) % self._n_frames
|
|
233
|
+
self.current_tile_id = self.frames[self._frame].frame_id
|
|
234
|
+
self._frame_timer += self.frames[self._frame].duration
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
def current_offset(self):
|
|
240
|
+
return self.frames[self._frame].offset
|
|
241
|
+
|
|
242
|
+
def get_frame(self) -> _TileAnimation:
|
|
243
|
+
return self.frames[self._frame]
|
|
244
|
+
|
|
245
|
+
def get_collision_boxes(self) -> list[_CollisionBox]:
|
|
246
|
+
frame_cb = self.frames[self._frame].collision_boxes
|
|
247
|
+
if frame_cb:
|
|
248
|
+
return frame_cb
|
|
249
|
+
return self._collision_objects
|
|
250
|
+
|
|
251
|
+
def reset(self) -> None:
|
|
252
|
+
self._frame = 0
|
|
253
|
+
self._frame_timer = self.frames[self._frame].duration
|
|
254
|
+
self.current_tile_id = self.frames[self._frame].frame_id
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TiledTemplate(_TiledBase):
|
|
258
|
+
def __init__(self, mapman: MapManager, name: str) -> None:
|
|
259
|
+
super().__init__(mapman, "TiledTemplate")
|
|
260
|
+
self.name = name
|
|
261
|
+
self.tileset: TiledTileset
|
|
262
|
+
self.first_gid: int = 0
|
|
263
|
+
self.template_name: str = "Unnamed"
|
|
264
|
+
self.template_type: str = "Untyped"
|
|
265
|
+
self.gid: int = 0
|
|
266
|
+
self.size: Vector2 = Vector2()
|
|
267
|
+
|
|
268
|
+
def load_from_path(self, path: Path) -> Self:
|
|
269
|
+
path = Path(path)
|
|
270
|
+
t_xtree = ElementTree.parse(path).getroot()
|
|
271
|
+
tileset = t_xtree.findall("tileset")[0]
|
|
272
|
+
self.tileset = self.mapman.get_tileset(
|
|
273
|
+
(path.parent / tileset.attrib["source"]).resolve()
|
|
274
|
+
)
|
|
275
|
+
self.first_gid = int(tileset.attrib["firstgid"])
|
|
276
|
+
|
|
277
|
+
obj = t_xtree.findall("object")[0]
|
|
278
|
+
self.template_name = obj.attrib.get("name", "Unnamed")
|
|
279
|
+
self.template_type = obj.attrib.get("type", obj.attrib.get("class", "Untyped"))
|
|
280
|
+
self.gid = int(obj.attrib["gid"])
|
|
281
|
+
self.size = Vector2(int(obj.attrib["width"]), int(obj.attrib["height"]))
|
|
282
|
+
|
|
283
|
+
self.props = read_properties_to_dict(obj)
|
|
284
|
+
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class TiledObject(_TiledBase):
|
|
289
|
+
def __init__(self, mapman: MapManager) -> None:
|
|
290
|
+
super().__init__(mapman, "TiledObject")
|
|
291
|
+
|
|
292
|
+
self.object_id: int = 0
|
|
293
|
+
self.type: str = ""
|
|
294
|
+
self.pos: Vector2 = Vector2()
|
|
295
|
+
self.size: Vector2 = Vector2()
|
|
296
|
+
self.tileset: TiledTileset | None = None
|
|
297
|
+
self.gid: int = 0
|
|
298
|
+
|
|
299
|
+
def load_from_xml(self, o_xtree: Element, path: Path) -> Self:
|
|
300
|
+
self.object_id = int(o_xtree.attrib["id"])
|
|
301
|
+
self.pos = Vector2(float(o_xtree.attrib["x"]), float(o_xtree.attrib["y"]))
|
|
302
|
+
|
|
303
|
+
tsource = o_xtree.attrib.get("template", "")
|
|
304
|
+
if tsource:
|
|
305
|
+
tpl = self.mapman.get_template((path.parent / tsource).resolve())
|
|
306
|
+
self.name = tpl.template_name
|
|
307
|
+
self.type = tpl.template_type
|
|
308
|
+
self.size = tpl.size
|
|
309
|
+
self.tileset = tpl.tileset
|
|
310
|
+
self.gid = tpl.gid - tpl.first_gid
|
|
311
|
+
self.props = tpl.props.copy()
|
|
312
|
+
# Templates' y positions are bottom instead of top
|
|
313
|
+
self.pos.y -= self.size.y
|
|
314
|
+
else:
|
|
315
|
+
self.size = Vector2(
|
|
316
|
+
float(o_xtree.attrib.get("width", 0.0)),
|
|
317
|
+
float(o_xtree.attrib.get("height", 0.0)),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
self.name = o_xtree.attrib.get("name", self.name)
|
|
321
|
+
self.type = o_xtree.attrib.get("type", o_xtree.attrib.get("class", self.type))
|
|
322
|
+
self.props.update(read_properties_to_dict(o_xtree))
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class _TiledObjectGroup(_TiledBase):
|
|
327
|
+
def __init__(self, mapman: MapManager):
|
|
328
|
+
super().__init__(mapman, "TiledObjectGroup")
|
|
329
|
+
|
|
330
|
+
self._layer_id: int = 0
|
|
331
|
+
self._objects: list[TiledObject] = []
|
|
332
|
+
|
|
333
|
+
def load_from_xml(self, o_xtree: Element, path: Path) -> Self:
|
|
334
|
+
self._layer_id = int(o_xtree.attrib["id"])
|
|
335
|
+
objects = o_xtree.findall("object")
|
|
336
|
+
for obj in objects:
|
|
337
|
+
self._objects.append(TiledObject(self.mapman).load_from_xml(obj, path))
|
|
338
|
+
|
|
339
|
+
return self
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class TiledTileset:
|
|
343
|
+
"""Represents a Tiled tileset normally stored in a .tsx file."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, mapman: MapManager, name: str = "") -> None:
|
|
346
|
+
self._mapman = mapman
|
|
347
|
+
self.name: str = name
|
|
348
|
+
self.image: pygame.Surface = pygame.Surface((0, 0))
|
|
349
|
+
self._image_width: int = 0
|
|
350
|
+
self._image_height: int = 0
|
|
351
|
+
self._tile_width: int = 0
|
|
352
|
+
self._tile_height: int = 0
|
|
353
|
+
self._tile_count: int = 0
|
|
354
|
+
self.n_cols: int = 0
|
|
355
|
+
|
|
356
|
+
self.tiles: dict[int, TiledTile] = {}
|
|
357
|
+
self._anim_tiles: list[TiledTile] = []
|
|
358
|
+
self._is_new_frame: bool = False
|
|
359
|
+
|
|
360
|
+
def load_from_path(self, path: str | Path) -> Self:
|
|
361
|
+
"""Load tileset information from tsx file.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
path: File path to the tsx file to load.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
A reference to this object.
|
|
368
|
+
|
|
369
|
+
"""
|
|
370
|
+
path = Path(path)
|
|
371
|
+
t_xtree = ElementTree.parse(path).getroot()
|
|
372
|
+
|
|
373
|
+
self._tile_width = int(t_xtree.attrib["tilewidth"])
|
|
374
|
+
self._tile_height = int(t_xtree.attrib["tileheight"])
|
|
375
|
+
self._tile_count = int(t_xtree.attrib["tilecount"])
|
|
376
|
+
self.n_cols = int(t_xtree.attrib["columns"])
|
|
377
|
+
|
|
378
|
+
image = t_xtree.findall("image")[0]
|
|
379
|
+
image_path = path.parent / image.attrib["source"]
|
|
380
|
+
self._image_width = int(image.attrib["width"])
|
|
381
|
+
self._image_height = int(image.attrib["height"])
|
|
382
|
+
self.image = self._mapman.get_image(image_path)
|
|
383
|
+
|
|
384
|
+
tiles = t_xtree.findall("tile")
|
|
385
|
+
|
|
386
|
+
for tile in tiles:
|
|
387
|
+
t_obj = TiledTile(
|
|
388
|
+
self._mapman,
|
|
389
|
+
self.image,
|
|
390
|
+
self._tile_width,
|
|
391
|
+
self._tile_height,
|
|
392
|
+
self.n_cols,
|
|
393
|
+
).load_from_xml(tile, path)
|
|
394
|
+
self.tiles[t_obj.basic_tile_id] = t_obj
|
|
395
|
+
if t_obj.animated:
|
|
396
|
+
self._anim_tiles.append(t_obj)
|
|
397
|
+
|
|
398
|
+
return self
|
|
399
|
+
|
|
400
|
+
def update(self, elapsed_time: float) -> bool:
|
|
401
|
+
"""Update all animated tiles in this tileset.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
elapsed_time: How many seconds to progress the simulation.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
True if any of the tiles reached a new state.
|
|
408
|
+
"""
|
|
409
|
+
if self._is_new_frame:
|
|
410
|
+
changed = False
|
|
411
|
+
for tile in self._anim_tiles:
|
|
412
|
+
changed = tile.update(elapsed_time) or changed
|
|
413
|
+
self._is_new_frame = False
|
|
414
|
+
return changed
|
|
415
|
+
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
def get_tile(self, tile_id: int) -> TiledTile:
|
|
419
|
+
"""Return tile at index ``tile_id``."""
|
|
420
|
+
return self.tiles[tile_id]
|
|
421
|
+
|
|
422
|
+
def start_new_frame(self):
|
|
423
|
+
"""Notify tileset about a new frame starting.
|
|
424
|
+
|
|
425
|
+
Tilesets should not update more than once per frame, which can happen if
|
|
426
|
+
multiple maps with the same tileset are active at the same time.
|
|
427
|
+
Tilesets will only update if a new frame was triggered manually.
|
|
428
|
+
"""
|
|
429
|
+
self._is_new_frame = True
|
|
430
|
+
|
|
431
|
+
def reset(self) -> None:
|
|
432
|
+
"""Reset the animation state of all animated tiles."""
|
|
433
|
+
for tile in self._anim_tiles:
|
|
434
|
+
tile.reset()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class TiledLayerRenderable:
|
|
438
|
+
def __init__(
|
|
439
|
+
self,
|
|
440
|
+
image: pygame.Surface,
|
|
441
|
+
tile_size: Vector2,
|
|
442
|
+
layer: int = 0,
|
|
443
|
+
elevation: int = 0,
|
|
444
|
+
) -> None:
|
|
445
|
+
self.image = image
|
|
446
|
+
self.layer = layer
|
|
447
|
+
self.tile_size = tile_size
|
|
448
|
+
self.elevation = elevation
|
|
449
|
+
self.duration: float = 1.0
|
|
450
|
+
self.pos: Vector2 = Vector2()
|
|
451
|
+
self.parallax: Vector2 = Vector2(1.0, 1.0)
|
|
452
|
+
self.wrap_x: bool = False
|
|
453
|
+
self.wrap_y: bool = False
|
|
454
|
+
self.wrap_margin: float = 0 # Does not yet work as intended
|
|
455
|
+
|
|
456
|
+
def draw(self, ttv: Renderer, cache: bool = False) -> None:
|
|
457
|
+
# No wrapping; render directly
|
|
458
|
+
view_pos = ttv.get_tl_tile()
|
|
459
|
+
base_pos = self.pos + view_pos.elementwise() * (
|
|
460
|
+
1.0 - self.parallax.elementwise()
|
|
461
|
+
)
|
|
462
|
+
if not self.wrap_x and not self.wrap_y:
|
|
463
|
+
ttv.draw_surface(base_pos, self.image, cache=cache)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
img_size = Vector2(self.image.get_size()).elementwise() / self.tile_size
|
|
467
|
+
|
|
468
|
+
view_size = ttv.get_br_tile() - view_pos
|
|
469
|
+
|
|
470
|
+
# Normalize start so we begin drawing off-screen
|
|
471
|
+
start_x = base_pos.x
|
|
472
|
+
start_y = base_pos.y
|
|
473
|
+
end_x = view_pos.x + view_size.x + self.wrap_margin
|
|
474
|
+
end_y = view_pos.y + view_size.y + self.wrap_margin
|
|
475
|
+
|
|
476
|
+
if self.wrap_x:
|
|
477
|
+
start_x -= (
|
|
478
|
+
(start_x - view_pos.x) // img_size.x + 1
|
|
479
|
+
) * img_size.x - self.wrap_margin
|
|
480
|
+
end_x += self.wrap_margin
|
|
481
|
+
if self.wrap_y:
|
|
482
|
+
start_y -= (
|
|
483
|
+
(start_y - view_pos.y) // img_size.y + 1
|
|
484
|
+
) * img_size.y - self.wrap_margin
|
|
485
|
+
end_y += self.wrap_margin
|
|
486
|
+
|
|
487
|
+
y = start_y
|
|
488
|
+
while y < end_y:
|
|
489
|
+
x = start_x
|
|
490
|
+
while x < end_x:
|
|
491
|
+
ttv.draw_surface(Vector2(x, y), self.image, cache=cache)
|
|
492
|
+
if not self.wrap_x:
|
|
493
|
+
break
|
|
494
|
+
x += img_size.x
|
|
495
|
+
if not self.wrap_y:
|
|
496
|
+
break
|
|
497
|
+
y += img_size.y
|
|
498
|
+
|
|
499
|
+
def get_pos(self) -> Vector2:
|
|
500
|
+
return self.pos
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class _TiledLayer(_TiledBase):
|
|
504
|
+
def __init__(self, mapman: MapManager) -> None:
|
|
505
|
+
super().__init__(mapman, "Unnamed Layer")
|
|
506
|
+
self._layer_id: int = 0
|
|
507
|
+
self._width: int = 0
|
|
508
|
+
self._height: int = 0
|
|
509
|
+
self._layer_type: str = "layer"
|
|
510
|
+
self.offset: Vector2 = Vector2(0, 0)
|
|
511
|
+
self.speed: Vector2 = Vector2(0, 0)
|
|
512
|
+
self.elevation: int = 0
|
|
513
|
+
self.is_wall_layer: bool = False
|
|
514
|
+
self._is_prerendered: bool = False
|
|
515
|
+
self._prerendered_frames: list[TiledLayerRenderable] = []
|
|
516
|
+
self._durations: list[float] = []
|
|
517
|
+
self._timer: float = 0.25
|
|
518
|
+
self._frame: int = 0
|
|
519
|
+
|
|
520
|
+
self._indices: list[int] = []
|
|
521
|
+
|
|
522
|
+
def load_from_xml(self, l_xtree: Element) -> Self:
|
|
523
|
+
self._name = l_xtree.attrib["name"]
|
|
524
|
+
self._layer_id = int(l_xtree.attrib["id"])
|
|
525
|
+
self._layer_type = l_xtree.attrib.get(
|
|
526
|
+
"class", l_xtree.attrib.get("type", "Layer")
|
|
527
|
+
)
|
|
528
|
+
self._width = int(l_xtree.attrib["width"])
|
|
529
|
+
self._height = int(l_xtree.attrib["height"])
|
|
530
|
+
|
|
531
|
+
data = l_xtree.findall("data")[0]
|
|
532
|
+
reader = csv.reader(StringIO(data.text), delimiter=",")
|
|
533
|
+
for row in reader:
|
|
534
|
+
if len(row) <= 0:
|
|
535
|
+
continue
|
|
536
|
+
self._indices.extend([int(c) for c in row if c])
|
|
537
|
+
|
|
538
|
+
self.props = read_properties_to_dict(l_xtree)
|
|
539
|
+
self.elevation = self.get("elevation", 0)
|
|
540
|
+
self.is_wall_layer = self.get("is_wall_layer", False)
|
|
541
|
+
self.speed = Vector2(self.get("speed_x", 0.0), self.get("speed_y", 0.0))
|
|
542
|
+
return self
|
|
543
|
+
|
|
544
|
+
def update(self, elapsed_time: float) -> bool:
|
|
545
|
+
if self._is_prerendered:
|
|
546
|
+
self._timer -= elapsed_time
|
|
547
|
+
if self._timer <= 0.0:
|
|
548
|
+
self._frame = (self._frame + 1) % len(self._prerendered_frames)
|
|
549
|
+
self._timer += self._durations[self._frame]
|
|
550
|
+
|
|
551
|
+
if self.speed == 0.0:
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
self.offset += self.speed * elapsed_time
|
|
555
|
+
if self.offset.x > self._width:
|
|
556
|
+
self.offset.x -= self._width
|
|
557
|
+
if self.offset.x < 0:
|
|
558
|
+
self.offset.x += self._width
|
|
559
|
+
if self.offset.y > self._height:
|
|
560
|
+
self.offset.y -= self._height
|
|
561
|
+
if self.offset.y < 0:
|
|
562
|
+
self.offset.y += self._height
|
|
563
|
+
|
|
564
|
+
return True
|
|
565
|
+
|
|
566
|
+
def get_index(self, px: int, py: int) -> int:
|
|
567
|
+
if self.offset.x != 0.0:
|
|
568
|
+
px = math.floor(px - self.offset.x)
|
|
569
|
+
if px > self._width:
|
|
570
|
+
px -= self._width
|
|
571
|
+
while px < 0:
|
|
572
|
+
px += self._width
|
|
573
|
+
if self.offset.y != 0.0:
|
|
574
|
+
py = math.floor(py - self.offset.y)
|
|
575
|
+
if py > self._height:
|
|
576
|
+
py -= self._height
|
|
577
|
+
while py < 0:
|
|
578
|
+
py += self._height
|
|
579
|
+
|
|
580
|
+
if 0 <= px < self._width and 0 <= py < self._height:
|
|
581
|
+
return self._indices[py * self._width + px]
|
|
582
|
+
else:
|
|
583
|
+
return 0
|
|
584
|
+
|
|
585
|
+
def get_all_indices(self) -> list[int]:
|
|
586
|
+
return self._indices
|
|
587
|
+
|
|
588
|
+
def add_prerendered_frame(self, renderable: TiledLayerRenderable) -> None:
|
|
589
|
+
self._prerendered_frames.append(renderable)
|
|
590
|
+
self._durations.append(renderable.duration)
|
|
591
|
+
|
|
592
|
+
def get_pre_rendered_frame(self) -> TiledLayerRenderable | None:
|
|
593
|
+
if not self._is_prerendered:
|
|
594
|
+
return None
|
|
595
|
+
frame = self._prerendered_frames[self._frame]
|
|
596
|
+
frame.pos = Vector2() + self.offset
|
|
597
|
+
return frame
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class TiledTilemap:
|
|
601
|
+
"""Represents an orthogonal Tiled tilemap."""
|
|
602
|
+
|
|
603
|
+
def __init__(self, mapman: MapManager, name: str = "") -> None:
|
|
604
|
+
self._mapman: MapManager = mapman
|
|
605
|
+
self.name = name
|
|
606
|
+
|
|
607
|
+
self._width: int = 0
|
|
608
|
+
self._height: int = 0
|
|
609
|
+
self._tile_width = 1
|
|
610
|
+
self._tile_height = 1
|
|
611
|
+
self.world_size: Vector2 = Vector2(0, 0)
|
|
612
|
+
self.tile_size: Vector2 = Vector2(0, 0)
|
|
613
|
+
|
|
614
|
+
self._layers: list[_TiledLayer] = []
|
|
615
|
+
self._floor_layer_map: dict[int, list[_TiledLayer]] = {}
|
|
616
|
+
self._wall_layers: list[_TiledLayer] = []
|
|
617
|
+
self._decor_layers: list[_TiledLayer] = []
|
|
618
|
+
self._tilesets: list[_TilesetInfo] = []
|
|
619
|
+
self._groups: list[_TiledObjectGroup] = []
|
|
620
|
+
self._cache: dict[int, _TileInfo] = {}
|
|
621
|
+
self._tile_source_cache: TileCache = {}
|
|
622
|
+
self.debug_draws: list[dict[str, Any]] = []
|
|
623
|
+
self.br = Vector2()
|
|
624
|
+
self.tl = Vector2()
|
|
625
|
+
|
|
626
|
+
# Prerendering
|
|
627
|
+
self._is_pre_rendered: bool = False
|
|
628
|
+
self._rendered_layers: dict[int, list[tuple[pygame.Surface, float]]] = {}
|
|
629
|
+
|
|
630
|
+
def load_from_path(self, path: str | Path) -> None:
|
|
631
|
+
path = Path(path).resolve()
|
|
632
|
+
m_xtree = ElementTree.parse(path).getroot()
|
|
633
|
+
|
|
634
|
+
self._width = int(m_xtree.attrib["width"])
|
|
635
|
+
self._height = int(m_xtree.attrib["height"])
|
|
636
|
+
self.world_size = Vector2(self._width, self._height)
|
|
637
|
+
self._tile_width = int(m_xtree.attrib["tilewidth"])
|
|
638
|
+
self._tile_height = int(m_xtree.attrib["tileheight"])
|
|
639
|
+
self.tile_size = Vector2(self._tile_width, self._tile_height)
|
|
640
|
+
|
|
641
|
+
self._props = read_properties_to_dict(m_xtree)
|
|
642
|
+
|
|
643
|
+
# TODO allow to load embedded tilesets
|
|
644
|
+
self._tilesets = [
|
|
645
|
+
_TilesetInfo(
|
|
646
|
+
tileset=self._mapman.get_tileset(
|
|
647
|
+
(path.parent / ts.attrib["source"]).resolve()
|
|
648
|
+
),
|
|
649
|
+
first_gid=int(ts.attrib["firstgid"]),
|
|
650
|
+
)
|
|
651
|
+
for ts in m_xtree.findall("tileset")
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
self._layers = [
|
|
655
|
+
_TiledLayer(self._mapman).load_from_xml(layer)
|
|
656
|
+
for layer in m_xtree.findall("layer")
|
|
657
|
+
]
|
|
658
|
+
|
|
659
|
+
for layer in self._layers:
|
|
660
|
+
if layer.is_wall_layer:
|
|
661
|
+
self._wall_layers.append(layer)
|
|
662
|
+
else:
|
|
663
|
+
self._floor_layer_map.setdefault(layer.elevation, []).append(layer)
|
|
664
|
+
|
|
665
|
+
self._groups = [
|
|
666
|
+
_TiledObjectGroup(self._mapman).load_from_xml(og, path)
|
|
667
|
+
for og in m_xtree.findall("objectgroup")
|
|
668
|
+
]
|
|
669
|
+
|
|
670
|
+
def update(self, elapsed_time: float) -> bool:
|
|
671
|
+
changed = True
|
|
672
|
+
for info in self._tilesets:
|
|
673
|
+
changed = info.tileset.update(elapsed_time) and changed
|
|
674
|
+
|
|
675
|
+
for layer in self._layers:
|
|
676
|
+
changed = layer.update(elapsed_time) and changed
|
|
677
|
+
|
|
678
|
+
return changed
|
|
679
|
+
|
|
680
|
+
def draw(self, ttv: Renderer, layers: list[int] | None = None) -> None:
|
|
681
|
+
tl = vmax(ttv.get_tl_tile(), VZERO)
|
|
682
|
+
br = vmin(ttv.get_br_tile(), self.world_size)
|
|
683
|
+
self.tl = tl
|
|
684
|
+
self.br = br
|
|
685
|
+
if layers is None or not layers:
|
|
686
|
+
layers = [x for x in range(len(self._layers))]
|
|
687
|
+
|
|
688
|
+
self.debug_draws = []
|
|
689
|
+
|
|
690
|
+
for lid in layers:
|
|
691
|
+
if lid >= len(self._layers):
|
|
692
|
+
continue
|
|
693
|
+
layer = self._layers[lid]
|
|
694
|
+
|
|
695
|
+
for y in range(int(tl.y), int(br.y) + 1):
|
|
696
|
+
for x in range(int(tl.x), int(br.x) + 1):
|
|
697
|
+
tid = layer.get_index(x, y)
|
|
698
|
+
if tid <= 0:
|
|
699
|
+
continue
|
|
700
|
+
|
|
701
|
+
info = self._cache.get(tid)
|
|
702
|
+
if info is None:
|
|
703
|
+
if not self._load_to_cache(tid):
|
|
704
|
+
continue
|
|
705
|
+
info = self._cache[tid]
|
|
706
|
+
|
|
707
|
+
tile_position = Vector2(x, y)
|
|
708
|
+
src_pos = Vector2(info.tile.current_offset())
|
|
709
|
+
|
|
710
|
+
ttv.draw_surface(
|
|
711
|
+
tile_position,
|
|
712
|
+
info.tileset.image,
|
|
713
|
+
src_pos=src_pos,
|
|
714
|
+
src_size=self.tile_size,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
for obj in info.tile.get_collision_boxes():
|
|
718
|
+
self.debug_draws.append(
|
|
719
|
+
{
|
|
720
|
+
"is_circle": obj.shape == "circle",
|
|
721
|
+
"pos": obj.pos + tile_position,
|
|
722
|
+
"size": obj.size,
|
|
723
|
+
"radius": obj.radius,
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
def draw_to_surface(
|
|
730
|
+
self,
|
|
731
|
+
surf: pygame.Surface,
|
|
732
|
+
offset: Vector2,
|
|
733
|
+
visible_tiles_tl: Vector2 | None = None,
|
|
734
|
+
visible_tiles_br: Vector2 | None = None,
|
|
735
|
+
special_flags: int = 0,
|
|
736
|
+
) -> None:
|
|
737
|
+
visible_tiles_tl = (
|
|
738
|
+
Vector2(0, 0) if visible_tiles_tl is None else visible_tiles_tl
|
|
739
|
+
)
|
|
740
|
+
visible_tiles_br = (
|
|
741
|
+
Vector2(surf.get_size()) if visible_tiles_br is None else visible_tiles_br
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
# Get offsets for smooth movement
|
|
745
|
+
tile_offset = offset - vfloor(offset)
|
|
746
|
+
tile_offset.x *= self._tile_width
|
|
747
|
+
tile_offset.y *= self._tile_height
|
|
748
|
+
|
|
749
|
+
for layer in self._layers:
|
|
750
|
+
layer_offset = layer.offset - vfloor(layer.offset)
|
|
751
|
+
layer_offset.x *= self._tile_width
|
|
752
|
+
layer_offset.y *= self._tile_height
|
|
753
|
+
|
|
754
|
+
layer_vtiles_sx = int(visible_tiles_tl.x)
|
|
755
|
+
layer_vtiles_sy = int(visible_tiles_tl.y)
|
|
756
|
+
if layer.speed.x != 0.0:
|
|
757
|
+
layer_vtiles_sx -= 1
|
|
758
|
+
if layer.speed.y != 0.0:
|
|
759
|
+
layer_vtiles_sy -= 1
|
|
760
|
+
|
|
761
|
+
# Draw visible tiles of the map
|
|
762
|
+
for y in range(layer_vtiles_sy, int(visible_tiles_br.y) + 2):
|
|
763
|
+
for x in range(layer_vtiles_sx, int(visible_tiles_br.x) + 2):
|
|
764
|
+
tid = layer.get_index(
|
|
765
|
+
int(x + math.floor(offset.x)), int(y + math.floor(offset.y))
|
|
766
|
+
)
|
|
767
|
+
if tid <= 0:
|
|
768
|
+
# Zero means the tile was not set in Tiled
|
|
769
|
+
continue
|
|
770
|
+
if tid not in self._cache:
|
|
771
|
+
if not self._load_to_cache(tid):
|
|
772
|
+
continue
|
|
773
|
+
info = self._cache[tid]
|
|
774
|
+
sx = int(
|
|
775
|
+
(info.tile.current_tile_id % info.tileset.n_cols)
|
|
776
|
+
* self._tile_width
|
|
777
|
+
)
|
|
778
|
+
sy = int(
|
|
779
|
+
(info.tile.current_tile_id // info.tileset.n_cols)
|
|
780
|
+
* self._tile_height
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
px = int(x * self._tile_width - tile_offset.x + layer_offset.x)
|
|
784
|
+
py = int(y * self._tile_height - tile_offset.y + layer_offset.y)
|
|
785
|
+
|
|
786
|
+
surf.blit(
|
|
787
|
+
info.tileset.image,
|
|
788
|
+
(px, py),
|
|
789
|
+
((sx, sy), (self._tile_width, self._tile_height)),
|
|
790
|
+
special_flags,
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
def get_rendered_layers(self) -> list[TiledLayerRenderable]:
|
|
794
|
+
layers = [
|
|
795
|
+
layer_frame
|
|
796
|
+
for layer in self._layers
|
|
797
|
+
if (layer_frame := layer.get_pre_rendered_frame()) is not None
|
|
798
|
+
]
|
|
799
|
+
|
|
800
|
+
return layers
|
|
801
|
+
|
|
802
|
+
def _load_to_cache(self, tid: int) -> bool:
|
|
803
|
+
tileset: TiledTileset | None = None
|
|
804
|
+
first_gid: int = 0
|
|
805
|
+
for tsinfo in self._tilesets:
|
|
806
|
+
if tid < tsinfo.first_gid:
|
|
807
|
+
break
|
|
808
|
+
|
|
809
|
+
first_gid = tsinfo.first_gid
|
|
810
|
+
tileset = tsinfo.tileset
|
|
811
|
+
|
|
812
|
+
if tileset is None:
|
|
813
|
+
return False
|
|
814
|
+
|
|
815
|
+
tidx = tid - first_gid
|
|
816
|
+
tile = tileset.get_tile(tidx)
|
|
817
|
+
self._cache[tid] = _TileInfo(tileset=tileset, tile=tile)
|
|
818
|
+
return True
|
|
819
|
+
|
|
820
|
+
def start_new_frame(self) -> None:
|
|
821
|
+
for tsinfo in self._tilesets:
|
|
822
|
+
tsinfo.tileset.start_new_frame()
|
|
823
|
+
|
|
824
|
+
def get_tiles(self, x: int, y: int, z: int = 0) -> list[TiledTile]:
|
|
825
|
+
tiles = []
|
|
826
|
+
for layer in self._floor_layer_map.get(z, []):
|
|
827
|
+
tile = self._collect_tile(layer, x, y)
|
|
828
|
+
if tile is None:
|
|
829
|
+
continue
|
|
830
|
+
tiles.append(tile)
|
|
831
|
+
|
|
832
|
+
for layer in self._wall_layers:
|
|
833
|
+
if layer.elevation <= z:
|
|
834
|
+
tile = self._collect_tile(layer, x, y)
|
|
835
|
+
if tile is None:
|
|
836
|
+
continue
|
|
837
|
+
if z < layer.elevation + tile.z_height:
|
|
838
|
+
tiles.append(tile)
|
|
839
|
+
return tiles
|
|
840
|
+
|
|
841
|
+
def get_tiles_in_area(
|
|
842
|
+
self, pos: Vector2, area: Vector2, z: int = 0
|
|
843
|
+
) -> list[TiledTile]:
|
|
844
|
+
tiles = []
|
|
845
|
+
tl = vfloor(pos)
|
|
846
|
+
br = tl + vfloor(area.elementwise() + 1)
|
|
847
|
+
|
|
848
|
+
for y in range(int(tl.y), int(br.y)):
|
|
849
|
+
for x in range(int(tl.x), int(br.x)):
|
|
850
|
+
tiles.extend(self.get_tiles(x, y, z))
|
|
851
|
+
|
|
852
|
+
return tiles
|
|
853
|
+
|
|
854
|
+
def _collect_tile(self, layer: _TiledLayer, x: int, y: int) -> TiledTile | None:
|
|
855
|
+
tid = layer.get_index(x, y)
|
|
856
|
+
if tid <= 0:
|
|
857
|
+
return None
|
|
858
|
+
|
|
859
|
+
info = self._cache.get(tid)
|
|
860
|
+
if info is None:
|
|
861
|
+
if not self._load_to_cache(tid):
|
|
862
|
+
return None
|
|
863
|
+
info = self._cache[tid]
|
|
864
|
+
|
|
865
|
+
return info.tile
|
|
866
|
+
|
|
867
|
+
def get_objects(self) -> list[TiledObject]:
|
|
868
|
+
all_objects = []
|
|
869
|
+
for group in self._groups:
|
|
870
|
+
all_objects.extend(group._objects)
|
|
871
|
+
|
|
872
|
+
return all_objects
|
|
873
|
+
|
|
874
|
+
def prerender_layers(self) -> None:
|
|
875
|
+
"""Compute possible animation states for all layers.
|
|
876
|
+
|
|
877
|
+
Prerendered means that the tile layers will be drawn to a surface once.
|
|
878
|
+
During the simulation, the full layer image will be drawn instead of
|
|
879
|
+
each tile individually. Normally, this results in a noticable speed-up.
|
|
880
|
+
However, when using animated tiles, those animations have to be pre-
|
|
881
|
+
computed as well.
|
|
882
|
+
|
|
883
|
+
The current implementation will find every possible combination of tile
|
|
884
|
+
animations and render an image for that combination. This works well and
|
|
885
|
+
accurate if the frame times of the animated tiles are equal or multiples
|
|
886
|
+
of each other. Otherwise, the number of frame to precompute will grow
|
|
887
|
+
exponentially.
|
|
888
|
+
|
|
889
|
+
"""
|
|
890
|
+
for layer in self._layers:
|
|
891
|
+
layer._is_prerendered = True
|
|
892
|
+
indices = layer.get_all_indices()
|
|
893
|
+
|
|
894
|
+
data = build_frame_table(self, layer)
|
|
895
|
+
schedule = build_prerender_schedule(data)
|
|
896
|
+
|
|
897
|
+
for idx in range(len(schedule)):
|
|
898
|
+
renderable = TiledLayerRenderable(
|
|
899
|
+
pygame.Surface(self.world_size.elementwise() * self.tile_size),
|
|
900
|
+
self.tile_size,
|
|
901
|
+
layer.get("layer", 0),
|
|
902
|
+
layer.elevation,
|
|
903
|
+
)
|
|
904
|
+
if layer.speed.x != 0:
|
|
905
|
+
renderable.wrap_x = True
|
|
906
|
+
if layer.speed.y != 0:
|
|
907
|
+
renderable.wrap_y = True
|
|
908
|
+
|
|
909
|
+
renderable.image.fill((254, 253, 252))
|
|
910
|
+
|
|
911
|
+
# Draw current frame state to surface
|
|
912
|
+
for y in range(int(self.world_size.y)):
|
|
913
|
+
for x in range(int(self.world_size.x)):
|
|
914
|
+
tid = indices[int(y * self.world_size.x + x)]
|
|
915
|
+
if (info := self._cache.get(tid)) is None:
|
|
916
|
+
continue
|
|
917
|
+
tile_position = Vector2(x, y).elementwise() * self.tile_size
|
|
918
|
+
src_pos = Vector2(info.tile.current_offset())
|
|
919
|
+
renderable.image.blit(
|
|
920
|
+
info.tileset.image,
|
|
921
|
+
tile_position,
|
|
922
|
+
((src_pos, self.tile_size)),
|
|
923
|
+
)
|
|
924
|
+
renderable.image.set_colorkey((254, 253, 252))
|
|
925
|
+
|
|
926
|
+
# Compute next animation state
|
|
927
|
+
frame_t = schedule[idx][0]
|
|
928
|
+
renderable.duration = frame_t
|
|
929
|
+
layer.add_prerendered_frame(renderable)
|
|
930
|
+
if len(schedule) > 1:
|
|
931
|
+
for tsinfo in self._tilesets:
|
|
932
|
+
tsinfo.tileset.start_new_frame()
|
|
933
|
+
tsinfo.tileset.update(frame_t)
|
|
934
|
+
|
|
935
|
+
for tsinfo in self._tilesets:
|
|
936
|
+
tsinfo.tileset.reset()
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
class MapManager:
|
|
940
|
+
def __init__(self) -> None:
|
|
941
|
+
self._tilemaps: dict[str, TiledTilemap] = {}
|
|
942
|
+
self._tilesets: dict[str, TiledTileset] = {}
|
|
943
|
+
self._templates: dict[str, TiledTemplate] = {}
|
|
944
|
+
self._images: dict[str, pygame.Surface] = {}
|
|
945
|
+
self._loaded_paths: dict[str, str] = {}
|
|
946
|
+
|
|
947
|
+
def load(self, files: list[str | Path]) -> None:
|
|
948
|
+
for f in files:
|
|
949
|
+
fp = Path(f).resolve()
|
|
950
|
+
|
|
951
|
+
if not fp.exists():
|
|
952
|
+
LOG.warning("File path '%s' does not exist! Skipping", fp)
|
|
953
|
+
continue
|
|
954
|
+
|
|
955
|
+
if fp.is_dir():
|
|
956
|
+
self._load_dir(fp)
|
|
957
|
+
else:
|
|
958
|
+
self._load_file(fp)
|
|
959
|
+
|
|
960
|
+
def _load_file(self, fp: Path, skip_invalid: bool = False) -> None:
|
|
961
|
+
fp = fp.resolve()
|
|
962
|
+
if str(fp) in self._loaded_paths:
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
_, suf = fp.parts[-1].rsplit(".")
|
|
966
|
+
if suf.lower() == "tmx":
|
|
967
|
+
self._load_tilemap(fp)
|
|
968
|
+
elif suf.lower() == "tsx":
|
|
969
|
+
self._load_tileset(fp)
|
|
970
|
+
elif suf.lower() == "tx":
|
|
971
|
+
pass # Template
|
|
972
|
+
elif not skip_invalid:
|
|
973
|
+
msg = (
|
|
974
|
+
f"File {fp} has an unsupported file ending {suf} . "
|
|
975
|
+
"Please provide .tmx, .tsx, or .tx"
|
|
976
|
+
)
|
|
977
|
+
raise ValueError(msg)
|
|
978
|
+
|
|
979
|
+
def _load_dir(self, fp: Path) -> None:
|
|
980
|
+
pass
|
|
981
|
+
|
|
982
|
+
def _load_tilemap(self, path: Path) -> str:
|
|
983
|
+
path = path.resolve()
|
|
984
|
+
name = path.stem
|
|
985
|
+
if name in self._tilemaps:
|
|
986
|
+
return name
|
|
987
|
+
|
|
988
|
+
tm = TiledTilemap(self, name)
|
|
989
|
+
|
|
990
|
+
LOG.info("Attempting to load tilemap '%s' from TMX file '%s' ...", name, path)
|
|
991
|
+
tm.load_from_path(path)
|
|
992
|
+
LOG.info("Map '%s' successfully loaded.", name)
|
|
993
|
+
|
|
994
|
+
self._tilemaps[name] = tm
|
|
995
|
+
self._loaded_paths[str(path)] = name
|
|
996
|
+
return name
|
|
997
|
+
|
|
998
|
+
def _load_tileset(self, path: Path) -> str:
|
|
999
|
+
path = path.resolve()
|
|
1000
|
+
name = path.stem
|
|
1001
|
+
if name in self._tilesets:
|
|
1002
|
+
return name
|
|
1003
|
+
|
|
1004
|
+
ts = TiledTileset(self, name)
|
|
1005
|
+
|
|
1006
|
+
LOG.info("Attempting to load tileset '%s' from TSX file '%s' ...", name, path)
|
|
1007
|
+
ts.load_from_path(path)
|
|
1008
|
+
LOG.info("Tileset '%s' successfully loaded.", name)
|
|
1009
|
+
|
|
1010
|
+
self._tilesets[name] = ts
|
|
1011
|
+
self._loaded_paths[str(path)] = name
|
|
1012
|
+
return name
|
|
1013
|
+
|
|
1014
|
+
def _load_template(self, path: Path) -> str:
|
|
1015
|
+
path = path.resolve()
|
|
1016
|
+
name = path.stem
|
|
1017
|
+
if name in self._templates:
|
|
1018
|
+
return name
|
|
1019
|
+
|
|
1020
|
+
tpl = TiledTemplate(self, name)
|
|
1021
|
+
|
|
1022
|
+
LOG.info("Attempting to load template '%s' from TX file '%s' ...", name, path)
|
|
1023
|
+
tpl.load_from_path(path)
|
|
1024
|
+
|
|
1025
|
+
self._templates[name] = tpl
|
|
1026
|
+
self._loaded_paths[str(path)] = name
|
|
1027
|
+
return name
|
|
1028
|
+
|
|
1029
|
+
def _load_image(self, path: Path) -> str:
|
|
1030
|
+
path = path.resolve()
|
|
1031
|
+
name = path.stem
|
|
1032
|
+
if name in self._images:
|
|
1033
|
+
return name
|
|
1034
|
+
self._images[name] = pygame.image.load(path).convert_alpha()
|
|
1035
|
+
self._loaded_paths[str(path)] = name
|
|
1036
|
+
return name
|
|
1037
|
+
|
|
1038
|
+
def get_map(self, path: str | Path) -> TiledTilemap:
|
|
1039
|
+
name = str(path)
|
|
1040
|
+
if name in self._tilemaps:
|
|
1041
|
+
return self._tilemaps[name]
|
|
1042
|
+
|
|
1043
|
+
if name in self._loaded_paths:
|
|
1044
|
+
return self._tilemaps[self._loaded_paths[name]]
|
|
1045
|
+
|
|
1046
|
+
LOG.info("Tilemap '%s' not found. Attempting to read from disk ...", name)
|
|
1047
|
+
try:
|
|
1048
|
+
self._load_tilemap(Path(name))
|
|
1049
|
+
except ValueError:
|
|
1050
|
+
LOG.exception(
|
|
1051
|
+
"Invalid tilemap name format '%s. File should end with .tmx", name
|
|
1052
|
+
)
|
|
1053
|
+
raise
|
|
1054
|
+
|
|
1055
|
+
return self._tilemaps[self._loaded_paths[name]]
|
|
1056
|
+
|
|
1057
|
+
def get_tileset(self, path: str | Path) -> TiledTileset:
|
|
1058
|
+
name = str(path)
|
|
1059
|
+
if name in self._tilesets:
|
|
1060
|
+
return self._tilesets[name]
|
|
1061
|
+
|
|
1062
|
+
if name in self._loaded_paths:
|
|
1063
|
+
return self._tilesets[self._loaded_paths[name]]
|
|
1064
|
+
|
|
1065
|
+
LOG.info("Tileset '%s' not found. Attempting to read from disk ...", name)
|
|
1066
|
+
try:
|
|
1067
|
+
self._load_tileset(Path(name))
|
|
1068
|
+
except ValueError:
|
|
1069
|
+
LOG.exception(
|
|
1070
|
+
"Invalid tileset name format '%s'. File should end with .tsx", name
|
|
1071
|
+
)
|
|
1072
|
+
raise
|
|
1073
|
+
|
|
1074
|
+
return self._tilesets[self._loaded_paths[name]]
|
|
1075
|
+
|
|
1076
|
+
def get_template(self, name: str | Path) -> TiledTemplate:
|
|
1077
|
+
name = str(name)
|
|
1078
|
+
if name in self._templates:
|
|
1079
|
+
return self._templates[name]
|
|
1080
|
+
if name in self._loaded_paths:
|
|
1081
|
+
return self._templates[self._loaded_paths[name]]
|
|
1082
|
+
|
|
1083
|
+
LOG.info("Template '%s' not found. Attempt to read from disk ...", name)
|
|
1084
|
+
try:
|
|
1085
|
+
self._load_template(Path(name))
|
|
1086
|
+
except ValueError:
|
|
1087
|
+
LOG.exception(
|
|
1088
|
+
"Invalid template name format '%s'. File should end with .tx", name
|
|
1089
|
+
)
|
|
1090
|
+
raise
|
|
1091
|
+
|
|
1092
|
+
return self._templates[self._loaded_paths[name]]
|
|
1093
|
+
|
|
1094
|
+
def get_image(self, name: str | Path) -> pygame.Surface:
|
|
1095
|
+
name = str(name)
|
|
1096
|
+
if name in self._images:
|
|
1097
|
+
return self._images[name]
|
|
1098
|
+
if name in self._loaded_paths:
|
|
1099
|
+
return self._images[self._loaded_paths[name]]
|
|
1100
|
+
|
|
1101
|
+
LOG.info("Image '%s' not found. Attempting to read from disk ...", name)
|
|
1102
|
+
self._load_image(Path(name))
|
|
1103
|
+
|
|
1104
|
+
return self._images[self._loaded_paths[name]]
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def read_properties_to_dict(xtree: Element):
|
|
1108
|
+
return {
|
|
1109
|
+
p.attrib["name"]: convert_from_str(
|
|
1110
|
+
p.attrib["value"], p.attrib.get("type", "str")
|
|
1111
|
+
)
|
|
1112
|
+
for pd in xtree.findall("properties")
|
|
1113
|
+
for p in pd.findall("property")
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def build_frame_table(
|
|
1118
|
+
tm: TiledTilemap, layer: _TiledLayer
|
|
1119
|
+
) -> dict[int, dict[int, float]]:
|
|
1120
|
+
"""Build the frame table as preprocessing step for the prerender schedule.
|
|
1121
|
+
|
|
1122
|
+
Args:
|
|
1123
|
+
tm: The tilemap object.
|
|
1124
|
+
layer: The currently processed layer.
|
|
1125
|
+
|
|
1126
|
+
Returns:
|
|
1127
|
+
The frame table containing the frame durations for each tile ID.
|
|
1128
|
+
"""
|
|
1129
|
+
data = {}
|
|
1130
|
+
indices = layer.get_all_indices()
|
|
1131
|
+
|
|
1132
|
+
for y in range(int(tm.world_size.y)):
|
|
1133
|
+
for x in range(int(tm.world_size.x)):
|
|
1134
|
+
tid = indices[int(y * tm.world_size.x + x)]
|
|
1135
|
+
info = tm._cache.get(tid)
|
|
1136
|
+
if info is None:
|
|
1137
|
+
if not tm._load_to_cache(tid):
|
|
1138
|
+
continue
|
|
1139
|
+
info = tm._cache[tid]
|
|
1140
|
+
if not info.tile.animated:
|
|
1141
|
+
continue
|
|
1142
|
+
|
|
1143
|
+
frame_ticks = {}
|
|
1144
|
+
for idx, frame in enumerate(info.tile.frames):
|
|
1145
|
+
frame_ticks[idx] = frame.duration
|
|
1146
|
+
data[tid] = frame_ticks
|
|
1147
|
+
return data
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def build_prerender_schedule(
|
|
1151
|
+
frame_table: dict[int, dict[int, float]],
|
|
1152
|
+
) -> list[tuple[float, dict[int, int]]]:
|
|
1153
|
+
"""Build the prerender schedule that combines frame durations of all tiles.
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
frame_table: The frame table for the current layer, computed by
|
|
1157
|
+
:func:`build_frame_table`.
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
The prerender schedule that contains the durations that are required to
|
|
1161
|
+
capture all possible tile state combinations.
|
|
1162
|
+
"""
|
|
1163
|
+
if not frame_table:
|
|
1164
|
+
return [(0.0, {})]
|
|
1165
|
+
|
|
1166
|
+
tiles: dict[int, dict[str, Any]] = {}
|
|
1167
|
+
for tile_id, frames in frame_table.items():
|
|
1168
|
+
tiles[tile_id] = {
|
|
1169
|
+
"frames": list(frames.items()),
|
|
1170
|
+
"frame_idx": 0,
|
|
1171
|
+
"remaining": frames[0],
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
def snapshot():
|
|
1175
|
+
return tuple(
|
|
1176
|
+
(tile_id, state["frames"][state["frame_idx"]][0])
|
|
1177
|
+
for tile_id, state in sorted(tiles.items())
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
seen_states = set()
|
|
1181
|
+
schedule = []
|
|
1182
|
+
|
|
1183
|
+
while True:
|
|
1184
|
+
state = snapshot()
|
|
1185
|
+
if state in seen_states:
|
|
1186
|
+
break
|
|
1187
|
+
seen_states.add(state)
|
|
1188
|
+
|
|
1189
|
+
# Find next event
|
|
1190
|
+
dt = min(state["remaining"] for state in tiles.values())
|
|
1191
|
+
|
|
1192
|
+
schedule.append((dt, state))
|
|
1193
|
+
|
|
1194
|
+
# Update tiles
|
|
1195
|
+
for state in tiles.values():
|
|
1196
|
+
state["remaining"] -= dt
|
|
1197
|
+
if state["remaining"] == 0:
|
|
1198
|
+
state["frame_idx"] = (state["frame_idx"] + 1) % len(state["frames"])
|
|
1199
|
+
state["remaining"] = state["frames"][state["frame_idx"]][1]
|
|
1200
|
+
return schedule
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def convert_from_str(val: str, vtype: str):
|
|
1204
|
+
if vtype == "bool":
|
|
1205
|
+
return strtobool(val)
|
|
1206
|
+
elif vtype == "float":
|
|
1207
|
+
return float(val)
|
|
1208
|
+
elif vtype == "int":
|
|
1209
|
+
return int(val)
|
|
1210
|
+
return val
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def strtobool(val: str) -> bool:
|
|
1214
|
+
"""Convert a string representation of truth to true (1) or false (0).
|
|
1215
|
+
|
|
1216
|
+
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
|
|
1217
|
+
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
|
1218
|
+
'val' is anything else.
|
|
1219
|
+
"""
|
|
1220
|
+
val = val.lower()
|
|
1221
|
+
if val in ("y", "yes", "t", "true", "on", "1"):
|
|
1222
|
+
return True
|
|
1223
|
+
elif val in ("n", "no", "f", "false", "off", "0", ""):
|
|
1224
|
+
return False
|
|
1225
|
+
else:
|
|
1226
|
+
msg = f"Invalid truth value {val}"
|
|
1227
|
+
raise ValueError(msg)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def vfloor(vec: Vector2, inplace: bool = False) -> Vector2:
|
|
1231
|
+
if inplace:
|
|
1232
|
+
vec.x = math.floor(vec.x)
|
|
1233
|
+
vec.y = math.floor(vec.y)
|
|
1234
|
+
return vec
|
|
1235
|
+
return Vector2(math.floor(vec.x), math.floor(vec.y))
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def vclamp(val: Vector2, low: Vector2, high: Vector2) -> Vector2:
|
|
1239
|
+
return Vector2(max(low.x, min(high.x, val.x)), max(low.y, min(high.y, val.y)))
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def vmax(val: Vector2, other: Vector2) -> Vector2:
|
|
1243
|
+
return Vector2(max(val.x, other.x), max(val.y, other.y))
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def vmin(val: Vector2, other: Vector2) -> Vector2:
|
|
1247
|
+
return Vector2(min(val.x, other.x), min(val.y, other.y))
|