tilemap-parser 1.1.1__tar.gz → 2.0.0__tar.gz

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 (18) hide show
  1. {tilemap_parser-1.1.1/src/tilemap_parser.egg-info → tilemap_parser-2.0.0}/PKG-INFO +1 -1
  2. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/pyproject.toml +2 -1
  3. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/collision.py +70 -36
  4. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0/src/tilemap_parser.egg-info}/PKG-INFO +1 -1
  5. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser.egg-info/SOURCES.txt +2 -1
  6. tilemap_parser-2.0.0/tests/test_collision.py +387 -0
  7. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/LICENSE +0 -0
  8. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/README.md +0 -0
  9. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/setup.cfg +0 -0
  10. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/__init__.py +0 -0
  11. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/animation.py +0 -0
  12. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/collision_runner.py +0 -0
  13. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/map_loader.py +0 -0
  14. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/map_parse.py +0 -0
  15. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser/renderer.py +0 -0
  16. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  17. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser.egg-info/requires.txt +0 -0
  18. {tilemap_parser-1.1.1 → tilemap_parser-2.0.0}/src/tilemap_parser.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 1.1.1
3
+ Version: 2.0.0
4
4
  Summary: Standalone parser/loader for tilemap-editor JSON maps and sprite animation JSON.
5
5
  Author: tilemap parser contributors
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tilemap-parser"
7
- version = "1.1.1"
7
+ version = "2.0.0"
8
8
  description = "Standalone parser/loader for tilemap-editor JSON maps and sprite animation JSON."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -26,4 +26,5 @@ include-package-data = false
26
26
  [tool.setuptools.packages.find]
27
27
  where = ["src"]
28
28
  include = ["tilemap_parser*"]
29
+ exclude = ["webdocs*", "tests*"]
29
30
  namespaces = false
@@ -217,22 +217,24 @@ def parse_character_collision(data: JsonDict) -> CharacterCollision:
217
217
 
218
218
 
219
219
  def load_tileset_collision(
220
- tileset_path: Union[str, Path],
220
+ collision_path: Union[str, Path],
221
221
  ) -> Optional[TilesetCollision]:
222
222
  """
223
- Load tileset collision data from file.
223
+ Load tileset collision data from a collision JSON file.
224
+
225
+ The editor stores tileset collision files at:
226
+ <data_root>/collision/<tileset_stem>.collision.json
224
227
 
225
228
  Args:
226
- tileset_path: Path to tileset image file
229
+ collision_path: Direct path to the .collision.json file.
227
230
 
228
231
  Returns:
229
- TilesetCollision object or None if collision file doesn't exist
232
+ TilesetCollision object, or None if the file does not exist.
230
233
 
231
234
  Raises:
232
- CollisionParseError: If collision file exists but is invalid
235
+ CollisionParseError: If the file exists but cannot be parsed.
233
236
  """
234
- tileset_path = Path(tileset_path)
235
- collision_path = tileset_path.with_suffix(".collision.json")
237
+ collision_path = Path(collision_path)
236
238
 
237
239
  if not collision_path.exists():
238
240
  return None
@@ -241,27 +243,29 @@ def load_tileset_collision(
241
243
  with open(collision_path, "r", encoding="utf-8") as f:
242
244
  data = json.load(f)
243
245
  return parse_tileset_collision(data)
244
- except (OSError, json.JSONDecodeError) as e:
246
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
245
247
  raise CollisionParseError(f"Cannot load {collision_path}: {e}") from e
246
248
 
247
249
 
248
250
  def load_character_collision(
249
- sprite_path: Union[str, Path],
251
+ collision_path: Union[str, Path],
250
252
  ) -> Optional[CharacterCollision]:
251
253
  """
252
- Load character collision data from file.
254
+ Load character collision data from a collision JSON file.
255
+
256
+ The editor stores character collision files at:
257
+ <data_root>/character_collision/<character_name>.collision.json
253
258
 
254
259
  Args:
255
- sprite_path: Path to character sprite image file
260
+ collision_path: Direct path to the .collision.json file.
256
261
 
257
262
  Returns:
258
- CharacterCollision object or None if collision file doesn't exist
263
+ CharacterCollision object, or None if the file does not exist.
259
264
 
260
265
  Raises:
261
- CollisionParseError: If collision file exists but is invalid
266
+ CollisionParseError: If the file exists but cannot be parsed.
262
267
  """
263
- sprite_path = Path(sprite_path)
264
- collision_path = sprite_path.with_suffix(".collision.json")
268
+ collision_path = Path(collision_path)
265
269
 
266
270
  if not collision_path.exists():
267
271
  return None
@@ -270,7 +274,7 @@ def load_character_collision(
270
274
  with open(collision_path, "r", encoding="utf-8") as f:
271
275
  data = json.load(f)
272
276
  return parse_character_collision(data)
273
- except (OSError, json.JSONDecodeError) as e:
277
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
274
278
  raise CollisionParseError(f"Cannot load {collision_path}: {e}") from e
275
279
 
276
280
 
@@ -280,6 +284,12 @@ class CollisionCache:
280
284
 
281
285
  Caches parsed collision data to avoid repeated file I/O and parsing.
282
286
  Useful for runtime performance in game engines.
287
+
288
+ All methods accept the direct path to the .collision.json file.
289
+
290
+ Typical paths produced by the editor:
291
+ Tileset: <data_root>/collision/<stem>.collision.json
292
+ Character: <data_root>/character_collision/<name>.collision.json
283
293
  """
284
294
 
285
295
  def __init__(self):
@@ -287,24 +297,32 @@ class CollisionCache:
287
297
  self._character_cache: Dict[str, Optional[CharacterCollision]] = {}
288
298
 
289
299
  def get_tileset_collision(
290
- self, tileset_path: Union[str, Path]
300
+ self, collision_path: Union[str, Path]
291
301
  ) -> Optional[TilesetCollision]:
292
- """Get tileset collision data (cached)"""
293
- key = str(Path(tileset_path).resolve())
302
+ """Get tileset collision data (cached).
303
+
304
+ Args:
305
+ collision_path: Direct path to the .collision.json file.
306
+ """
307
+ key = str(Path(collision_path).resolve())
294
308
 
295
309
  if key not in self._tileset_cache:
296
- self._tileset_cache[key] = load_tileset_collision(tileset_path)
310
+ self._tileset_cache[key] = load_tileset_collision(collision_path)
297
311
 
298
312
  return self._tileset_cache[key]
299
313
 
300
314
  def get_character_collision(
301
- self, sprite_path: Union[str, Path]
315
+ self, collision_path: Union[str, Path]
302
316
  ) -> Optional[CharacterCollision]:
303
- """Get character collision data (cached)"""
304
- key = str(Path(sprite_path).resolve())
317
+ """Get character collision data (cached).
318
+
319
+ Args:
320
+ collision_path: Direct path to the .collision.json file.
321
+ """
322
+ key = str(Path(collision_path).resolve())
305
323
 
306
324
  if key not in self._character_cache:
307
- self._character_cache[key] = load_character_collision(sprite_path)
325
+ self._character_cache[key] = load_character_collision(collision_path)
308
326
 
309
327
  return self._character_cache[key]
310
328
 
@@ -313,30 +331,46 @@ class CollisionCache:
313
331
  self._tileset_cache.clear()
314
332
  self._character_cache.clear()
315
333
 
316
- def preload_tileset(self, tileset_path: Union[str, Path]):
317
- """Preload tileset collision data into cache"""
318
- self.get_tileset_collision(tileset_path)
334
+ def preload_tileset(self, collision_path: Union[str, Path]):
335
+ """Preload tileset collision data into cache.
336
+
337
+ Args:
338
+ collision_path: Direct path to the .collision.json file.
339
+ """
340
+ self.get_tileset_collision(collision_path)
319
341
 
320
- def preload_character(self, sprite_path: Union[str, Path]):
321
- """Preload character collision data into cache"""
322
- self.get_character_collision(sprite_path)
342
+ def preload_character(self, collision_path: Union[str, Path]):
343
+ """Preload character collision data into cache.
344
+
345
+ Args:
346
+ collision_path: Direct path to the .collision.json file.
347
+ """
348
+ self.get_character_collision(collision_path)
323
349
 
324
350
 
325
351
  _global_cache = CollisionCache()
326
352
 
327
353
 
328
354
  def get_cached_tileset_collision(
329
- tileset_path: Union[str, Path],
355
+ collision_path: Union[str, Path],
330
356
  ) -> Optional[TilesetCollision]:
331
- """Get tileset collision using global cache"""
332
- return _global_cache.get_tileset_collision(tileset_path)
357
+ """Get tileset collision using global cache.
358
+
359
+ Args:
360
+ collision_path: Direct path to the .collision.json file.
361
+ """
362
+ return _global_cache.get_tileset_collision(collision_path)
333
363
 
334
364
 
335
365
  def get_cached_character_collision(
336
- sprite_path: Union[str, Path],
366
+ collision_path: Union[str, Path],
337
367
  ) -> Optional[CharacterCollision]:
338
- """Get character collision using global cache"""
339
- return _global_cache.get_character_collision(sprite_path)
368
+ """Get character collision using global cache.
369
+
370
+ Args:
371
+ collision_path: Direct path to the .collision.json file.
372
+ """
373
+ return _global_cache.get_character_collision(collision_path)
340
374
 
341
375
 
342
376
  def clear_collision_cache():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 1.1.1
3
+ Version: 2.0.0
4
4
  Summary: Standalone parser/loader for tilemap-editor JSON maps and sprite animation JSON.
5
5
  Author: tilemap parser contributors
6
6
  Classifier: Programming Language :: Python :: 3
@@ -12,4 +12,5 @@ src/tilemap_parser.egg-info/PKG-INFO
12
12
  src/tilemap_parser.egg-info/SOURCES.txt
13
13
  src/tilemap_parser.egg-info/dependency_links.txt
14
14
  src/tilemap_parser.egg-info/requires.txt
15
- src/tilemap_parser.egg-info/top_level.txt
15
+ src/tilemap_parser.egg-info/top_level.txt
16
+ tests/test_collision.py
@@ -0,0 +1,387 @@
1
+ """
2
+ Tests for tilemap_parser.collision — covering the refactored API where
3
+ load_tileset_collision() and load_character_collision() accept a direct
4
+ path to the .collision.json file rather than deriving it from an image path.
5
+ """
6
+
7
+ import json
8
+ import pytest
9
+ from pathlib import Path
10
+
11
+ import sys
12
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
13
+
14
+ from tilemap_parser.collision import (
15
+ # parsers
16
+ parse_tileset_collision,
17
+ parse_character_collision,
18
+ # loaders
19
+ load_tileset_collision,
20
+ load_character_collision,
21
+ # cache
22
+ CollisionCache,
23
+ get_cached_tileset_collision,
24
+ get_cached_character_collision,
25
+ clear_collision_cache,
26
+ # data classes
27
+ TilesetCollision,
28
+ CharacterCollision,
29
+ RectangleShape,
30
+ CircleShape,
31
+ CapsuleShape,
32
+ CollisionPolygon,
33
+ CollisionParseError,
34
+ )
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Fixtures
38
+ # ---------------------------------------------------------------------------
39
+
40
+ FIXTURES = Path(__file__).parent.parent / "examples" / "fixtures"
41
+ TILESET_COLLISION = FIXTURES / "collision" / "Terrain (32x32).collision.json"
42
+ CHARACTER_COLLISION = FIXTURES / "character_collision" / "hero.collision.json"
43
+
44
+ TILESET_DATA = {
45
+ "tileset_name": "test_tileset",
46
+ "tile_size": [32, 32],
47
+ "tiles": {
48
+ "0": {
49
+ "tile_id": 0,
50
+ "shapes": [
51
+ {
52
+ "vertices": [[0.0, 0.0], [32.0, 0.0], [32.0, 32.0], [0.0, 32.0]],
53
+ "one_way": False,
54
+ }
55
+ ],
56
+ },
57
+ "1": {
58
+ "tile_id": 1,
59
+ "shapes": [
60
+ {
61
+ "vertices": [[0.0, 16.0], [32.0, 16.0], [32.0, 32.0], [0.0, 32.0]],
62
+ "one_way": True,
63
+ }
64
+ ],
65
+ },
66
+ },
67
+ }
68
+
69
+ CHARACTER_RECT_DATA = {
70
+ "name": "player",
71
+ "shape": {"type": "rectangle", "width": 24.0, "height": 32.0, "offset": [4.0, 0.0]},
72
+ "properties": {"speed": 150},
73
+ }
74
+
75
+ CHARACTER_CIRCLE_DATA = {
76
+ "name": "ball",
77
+ "shape": {"type": "circle", "radius": 16.0, "offset": [0.0, 0.0]},
78
+ "properties": {},
79
+ }
80
+
81
+ CHARACTER_CAPSULE_DATA = {
82
+ "name": "snake",
83
+ "shape": {"type": "capsule", "radius": 8.0, "height": 48.0, "offset": [0.0, 0.0]},
84
+ "properties": {},
85
+ }
86
+
87
+
88
+ # ===========================================================================
89
+ # parse_tileset_collision
90
+ # ===========================================================================
91
+
92
+ class TestParseTilesetCollision:
93
+ def test_basic_parse(self):
94
+ result = parse_tileset_collision(TILESET_DATA)
95
+ assert isinstance(result, TilesetCollision)
96
+ assert result.tileset_name == "test_tileset"
97
+ assert result.tile_size == (32, 32)
98
+ assert len(result.tiles) == 2
99
+
100
+ def test_tile_has_collision(self):
101
+ result = parse_tileset_collision(TILESET_DATA)
102
+ assert result.has_collision(0)
103
+ assert result.has_collision(1)
104
+ assert not result.has_collision(99)
105
+
106
+ def test_one_way_flag(self):
107
+ result = parse_tileset_collision(TILESET_DATA)
108
+ shapes = result.get_world_shapes(1, 0, 0)
109
+ assert shapes[0].one_way is True
110
+
111
+ def test_world_shapes_transform(self):
112
+ result = parse_tileset_collision(TILESET_DATA)
113
+ shapes = result.get_world_shapes(0, 100.0, 200.0)
114
+ assert len(shapes) == 1
115
+ # All vertices should be offset by (100, 200)
116
+ for vx, vy in shapes[0].vertices:
117
+ assert vx >= 100.0
118
+ assert vy >= 200.0
119
+
120
+ def test_world_shapes_missing_tile(self):
121
+ result = parse_tileset_collision(TILESET_DATA)
122
+ assert result.get_world_shapes(99, 0, 0) == []
123
+
124
+ def test_missing_tileset_name_raises(self):
125
+ bad = {k: v for k, v in TILESET_DATA.items() if k != "tileset_name"}
126
+ with pytest.raises(CollisionParseError):
127
+ parse_tileset_collision(bad)
128
+
129
+ def test_missing_tile_size_raises(self):
130
+ bad = {k: v for k, v in TILESET_DATA.items() if k != "tile_size"}
131
+ with pytest.raises(CollisionParseError):
132
+ parse_tileset_collision(bad)
133
+
134
+ def test_empty_tiles(self):
135
+ data = {"tileset_name": "empty", "tile_size": [16, 16], "tiles": {}}
136
+ result = parse_tileset_collision(data)
137
+ assert len(result.tiles) == 0
138
+
139
+
140
+ # ===========================================================================
141
+ # parse_character_collision
142
+ # ===========================================================================
143
+
144
+ class TestParseCharacterCollision:
145
+ def test_rectangle(self):
146
+ result = parse_character_collision(CHARACTER_RECT_DATA)
147
+ assert isinstance(result, CharacterCollision)
148
+ assert result.name == "player"
149
+ assert isinstance(result.shape, RectangleShape)
150
+ assert result.shape.width == 24.0
151
+ assert result.shape.height == 32.0
152
+ assert result.shape.offset == (4.0, 0.0)
153
+ assert result.properties["speed"] == 150
154
+
155
+ def test_circle(self):
156
+ result = parse_character_collision(CHARACTER_CIRCLE_DATA)
157
+ assert isinstance(result.shape, CircleShape)
158
+ assert result.shape.radius == 16.0
159
+
160
+ def test_capsule(self):
161
+ result = parse_character_collision(CHARACTER_CAPSULE_DATA)
162
+ assert isinstance(result.shape, CapsuleShape)
163
+ assert result.shape.radius == 8.0
164
+ assert result.shape.height == 48.0
165
+
166
+ def test_unknown_shape_raises(self):
167
+ bad = {
168
+ "name": "x",
169
+ "shape": {"type": "hexagon", "radius": 10.0},
170
+ "properties": {},
171
+ }
172
+ with pytest.raises(CollisionParseError):
173
+ parse_character_collision(bad)
174
+
175
+ def test_missing_name_raises(self):
176
+ bad = {k: v for k, v in CHARACTER_RECT_DATA.items() if k != "name"}
177
+ with pytest.raises(CollisionParseError):
178
+ parse_character_collision(bad)
179
+
180
+ def test_properties_default_empty(self):
181
+ data = {
182
+ "name": "ghost",
183
+ "shape": {"type": "circle", "radius": 10.0},
184
+ }
185
+ result = parse_character_collision(data)
186
+ assert result.properties == {}
187
+
188
+
189
+ # ===========================================================================
190
+ # Shape helpers
191
+ # ===========================================================================
192
+
193
+ class TestShapeHelpers:
194
+ def test_rectangle_get_bounds(self):
195
+ shape = RectangleShape(width=24.0, height=32.0, offset=(4.0, 0.0))
196
+ left, top, right, bottom = shape.get_bounds(100.0, 200.0)
197
+ assert left == 104.0
198
+ assert top == 200.0
199
+ assert right == 128.0
200
+ assert bottom == 232.0
201
+
202
+ def test_circle_get_center(self):
203
+ shape = CircleShape(radius=16.0, offset=(2.0, 3.0))
204
+ cx, cy = shape.get_center(100.0, 200.0)
205
+ assert cx == 102.0
206
+ assert cy == 203.0
207
+
208
+ def test_capsule_top_bottom(self):
209
+ shape = CapsuleShape(radius=8.0, height=48.0, offset=(0.0, 0.0))
210
+ top = shape.get_top_center(100.0, 200.0)
211
+ bottom = shape.get_bottom_center(100.0, 200.0)
212
+ assert top == (100.0, 200.0)
213
+ assert bottom == (100.0, 248.0)
214
+
215
+ def test_collision_polygon_is_valid(self):
216
+ valid = CollisionPolygon(vertices=[(0, 0), (1, 0), (1, 1)])
217
+ invalid = CollisionPolygon(vertices=[(0, 0), (1, 0)])
218
+ assert valid.is_valid()
219
+ assert not invalid.is_valid()
220
+
221
+ def test_collision_polygon_transform(self):
222
+ poly = CollisionPolygon(vertices=[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)])
223
+ transformed = poly.transform(5.0, 7.0)
224
+ assert transformed.vertices[0] == (5.0, 7.0)
225
+ assert transformed.vertices[1] == (15.0, 7.0)
226
+ assert transformed.vertices[2] == (15.0, 17.0)
227
+
228
+
229
+ # ===========================================================================
230
+ # load_tileset_collision — direct path API
231
+ # ===========================================================================
232
+
233
+ class TestLoadTilesetCollision:
234
+ def test_loads_fixture(self):
235
+ result = load_tileset_collision(TILESET_COLLISION)
236
+ assert result is not None
237
+ assert isinstance(result, TilesetCollision)
238
+ assert result.tileset_name == "Terrain (32x32)"
239
+
240
+ def test_returns_none_for_missing_file(self, tmp_path):
241
+ result = load_tileset_collision(tmp_path / "nonexistent.collision.json")
242
+ assert result is None
243
+
244
+ def test_raises_on_invalid_json(self, tmp_path):
245
+ bad = tmp_path / "bad.collision.json"
246
+ bad.write_text("not json", encoding="utf-8")
247
+ with pytest.raises(CollisionParseError):
248
+ load_tileset_collision(bad)
249
+
250
+ def test_raises_on_wrong_schema(self, tmp_path):
251
+ bad = tmp_path / "bad.collision.json"
252
+ bad.write_text(json.dumps({"wrong": "schema"}), encoding="utf-8")
253
+ with pytest.raises(CollisionParseError):
254
+ load_tileset_collision(bad)
255
+
256
+ def test_accepts_string_path(self):
257
+ result = load_tileset_collision(str(TILESET_COLLISION))
258
+ assert result is not None
259
+
260
+ def test_fixture_tile_ids(self):
261
+ result = load_tileset_collision(TILESET_COLLISION)
262
+ assert result.has_collision(26)
263
+ assert result.has_collision(8)
264
+ assert not result.has_collision(0)
265
+
266
+ def test_non_json_file_raises(self, tmp_path):
267
+ # Passing a non-JSON file (e.g. an image) raises CollisionParseError,
268
+ # not silently returns None — the file exists so we try to parse it.
269
+ fake_image = tmp_path / "terrain.png"
270
+ fake_image.write_bytes(b"\x89PNG\r\n")
271
+ with pytest.raises(CollisionParseError):
272
+ load_tileset_collision(fake_image)
273
+
274
+
275
+ # ===========================================================================
276
+ # load_character_collision — direct path API
277
+ # ===========================================================================
278
+
279
+ class TestLoadCharacterCollision:
280
+ def test_loads_fixture(self):
281
+ result = load_character_collision(CHARACTER_COLLISION)
282
+ assert result is not None
283
+ assert isinstance(result, CharacterCollision)
284
+ assert result.name == "hero"
285
+
286
+ def test_returns_none_for_missing_file(self, tmp_path):
287
+ result = load_character_collision(tmp_path / "ghost.collision.json")
288
+ assert result is None
289
+
290
+ def test_raises_on_invalid_json(self, tmp_path):
291
+ bad = tmp_path / "bad.collision.json"
292
+ bad.write_text("{broken", encoding="utf-8")
293
+ with pytest.raises(CollisionParseError):
294
+ load_character_collision(bad)
295
+
296
+ def test_accepts_string_path(self):
297
+ result = load_character_collision(str(CHARACTER_COLLISION))
298
+ assert result is not None
299
+
300
+ def test_fixture_shape_type(self):
301
+ result = load_character_collision(CHARACTER_COLLISION)
302
+ assert isinstance(result.shape, RectangleShape)
303
+
304
+ def test_non_json_file_raises(self, tmp_path):
305
+ # Passing a non-JSON file (e.g. an image) raises CollisionParseError,
306
+ # not silently returns None — the file exists so we try to parse it.
307
+ fake_image = tmp_path / "hero.png"
308
+ fake_image.write_bytes(b"\x89PNG\r\n")
309
+ with pytest.raises(CollisionParseError):
310
+ load_character_collision(fake_image)
311
+
312
+
313
+ # ===========================================================================
314
+ # CollisionCache
315
+ # ===========================================================================
316
+
317
+ class TestCollisionCache:
318
+ def setup_method(self):
319
+ self.cache = CollisionCache()
320
+
321
+ def test_get_tileset_collision(self):
322
+ result = self.cache.get_tileset_collision(TILESET_COLLISION)
323
+ assert result is not None
324
+ assert result.tileset_name == "Terrain (32x32)"
325
+
326
+ def test_get_character_collision(self):
327
+ result = self.cache.get_character_collision(CHARACTER_COLLISION)
328
+ assert result is not None
329
+ assert result.name == "hero"
330
+
331
+ def test_missing_returns_none(self, tmp_path):
332
+ result = self.cache.get_tileset_collision(tmp_path / "nope.collision.json")
333
+ assert result is None
334
+
335
+ def test_caches_result(self):
336
+ r1 = self.cache.get_tileset_collision(TILESET_COLLISION)
337
+ r2 = self.cache.get_tileset_collision(TILESET_COLLISION)
338
+ assert r1 is r2 # same object — not re-parsed
339
+
340
+ def test_clear_removes_cache(self):
341
+ r1 = self.cache.get_tileset_collision(TILESET_COLLISION)
342
+ self.cache.clear()
343
+ r2 = self.cache.get_tileset_collision(TILESET_COLLISION)
344
+ assert r1 is not r2 # re-parsed after clear
345
+
346
+ def test_preload_tileset(self):
347
+ self.cache.preload_tileset(TILESET_COLLISION)
348
+ key = str(TILESET_COLLISION.resolve())
349
+ assert key in self.cache._tileset_cache
350
+
351
+ def test_preload_character(self):
352
+ self.cache.preload_character(CHARACTER_COLLISION)
353
+ key = str(CHARACTER_COLLISION.resolve())
354
+ assert key in self.cache._character_cache
355
+
356
+ def test_cache_key_is_resolved_path(self, tmp_path):
357
+ # Two different Path objects pointing to the same file should share a cache entry
358
+ link = tmp_path / "link.collision.json"
359
+ import shutil
360
+ shutil.copy(TILESET_COLLISION, link)
361
+ r1 = self.cache.get_tileset_collision(link)
362
+ r2 = self.cache.get_tileset_collision(link)
363
+ assert r1 is r2
364
+
365
+
366
+ # ===========================================================================
367
+ # Global cache helpers
368
+ # ===========================================================================
369
+
370
+ class TestGlobalCacheHelpers:
371
+ def setup_method(self):
372
+ clear_collision_cache()
373
+
374
+ def test_get_cached_tileset(self):
375
+ result = get_cached_tileset_collision(TILESET_COLLISION)
376
+ assert result is not None
377
+
378
+ def test_get_cached_character(self):
379
+ result = get_cached_character_collision(CHARACTER_COLLISION)
380
+ assert result is not None
381
+
382
+ def test_clear_collision_cache(self):
383
+ get_cached_tileset_collision(TILESET_COLLISION)
384
+ clear_collision_cache()
385
+ # After clear, a fresh load still works
386
+ result = get_cached_tileset_collision(TILESET_COLLISION)
387
+ assert result is not None
File without changes
File without changes
File without changes