tilemap-parser 3.1.15__tar.gz → 3.1.17__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 (36) hide show
  1. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/PKG-INFO +1 -1
  2. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/pyproject.toml +1 -1
  3. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/map_loader.py +31 -1
  4. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/tile_collision.py +45 -19
  5. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
  6. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser.egg-info/SOURCES.txt +1 -0
  7. tilemap_parser-3.1.17/tests/test_object_surfaces.py +200 -0
  8. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/tests/test_tile_collision.py +55 -0
  9. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/LICENSE +0 -0
  10. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/README.md +0 -0
  11. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/setup.cfg +0 -0
  12. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/__init__.py +0 -0
  13. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/__init__.py +0 -0
  14. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/animation.py +0 -0
  15. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/collision.py +0 -0
  16. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/collision_loader.py +0 -0
  17. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/map_parse.py +0 -0
  18. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/node_parse.py +0 -0
  19. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/parser/particle.py +0 -0
  20. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/__init__.py +0 -0
  21. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/animation_player.py +0 -0
  22. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/area_node.py +0 -0
  23. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/collision_cache.py +0 -0
  24. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/object_collision.py +0 -0
  25. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/particles.py +0 -0
  26. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/runtime/renderer.py +0 -0
  27. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/utils/__init__.py +0 -0
  28. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser/utils/geometry.py +0 -0
  29. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  30. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser.egg-info/requires.txt +0 -0
  31. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/src/tilemap_parser.egg-info/top_level.txt +0 -0
  32. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/tests/test_collision.py +0 -0
  33. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/tests/test_geometry.py +0 -0
  34. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/tests/test_map_loader.py +0 -0
  35. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/tests/test_object_collision.py +0 -0
  36. {tilemap_parser-3.1.15 → tilemap_parser-3.1.17}/tests/test_render_scale.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.15
3
+ Version: 3.1.17
4
4
  Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
5
5
  Author: tilemap parser contributors
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tilemap-parser"
7
- version = "3.1.15"
7
+ version = "3.1.17"
8
8
  description = "Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Tuple, Union
8
8
  import pygame
9
9
  from pygame import Rect, Surface
10
10
 
11
- from ..parser.map_parse import MapParseError, ParsedLayer, ParsedMap, ParsedTile, parse_map_file
11
+ from ..parser.map_parse import MapParseError, ParsedLayer, ParsedMap, ParsedObject, ParsedTile, parse_map_file
12
12
  from ..parser.node_parse import parse_nodes_dict
13
13
  from .area_node import AreaNode
14
14
  from .particles import ParticleEmitterNode
@@ -234,6 +234,36 @@ class TilemapData:
234
234
  return None
235
235
  return self.get_tile_surface(tile.ttype, tile.variant)
236
236
 
237
+ def get_object_surface(self, obj: ParsedObject, *, copy_surface: bool = True) -> Optional[Surface]:
238
+ return self.get_image(variant=obj.variant, ttype=obj.ttype, copy_surface=copy_surface)
239
+
240
+ def get_object_surface_by_id(
241
+ self, layer_id_or_name: Union[int, str], object_id: int, *, copy_surface: bool = True
242
+ ) -> Optional[Tuple[Surface, int, int]]:
243
+ layer = self.get_layer(layer_id_or_name)
244
+ if layer is None or layer.layer_type != "object":
245
+ return None
246
+ obj = layer.objects.get(object_id)
247
+ if obj is None:
248
+ return None
249
+ surf = self.get_object_surface(obj, copy_surface=copy_surface)
250
+ if surf is None:
251
+ return None
252
+ return surf, obj.area.x, obj.area.y
253
+
254
+ def get_object_surfaces(
255
+ self, layer_id_or_name: Union[int, str], *, copy_surface: bool = True
256
+ ) -> List[Tuple[Surface, int, int, int]]:
257
+ layer = self.get_layer(layer_id_or_name)
258
+ if layer is None or layer.layer_type != "object":
259
+ return []
260
+ result: List[Tuple[Surface, int, int, int]] = []
261
+ for oid, obj in layer.objects.items():
262
+ surf = self.get_object_surface(obj, copy_surface=copy_surface)
263
+ if surf is not None:
264
+ result.append((surf, obj.area.x, obj.area.y, oid))
265
+ return result
266
+
237
267
  def get_tileset_animation(self, ttype: int) -> Optional[dict]:
238
268
  if 0 <= ttype < len(self.parsed.tilesets):
239
269
  anim = self.parsed.tilesets[ttype].animation
@@ -441,6 +441,7 @@ class CollisionRunner:
441
441
  self.gravity = 800.0
442
442
  self.max_fall_speed = 600.0
443
443
  self.jump_strength = -400.0
444
+ self.horizontal_speed = 200.0
444
445
 
445
446
  self.ground_snap_tolerance = 2.0
446
447
  self.step_height = 4.0
@@ -797,6 +798,7 @@ class CollisionRunner:
797
798
  dt: float,
798
799
  input_x: float = 0.0,
799
800
  jump_pressed: bool = False,
801
+ velocity: Optional[Vector2] = None,
800
802
  ) -> CollisionResult:
801
803
  """
802
804
  Move sprite with platformer physics (gravity, jumping).
@@ -808,8 +810,11 @@ class CollisionRunner:
808
810
  tileset_collision: Tileset collision data
809
811
  tile_map: Dictionary mapping (tile_x, tile_y) to tile_id
810
812
  dt: Delta time in seconds
811
- input_x: Horizontal input (-1 to 1)
812
- jump_pressed: Whether jump button is pressed
813
+ input_x: Horizontal input (-1 to 1) for built-in movement
814
+ jump_pressed: Whether jump button is pressed for built-in movement
815
+ velocity: Optional explicit velocity (vx, vy). When provided, the
816
+ runner skips built-in input/gravity/jump velocity calculation
817
+ and only resolves collision for that velocity.
813
818
 
814
819
  Returns:
815
820
  CollisionResult with final position and collision info
@@ -824,15 +829,19 @@ class CollisionRunner:
824
829
  result.final_x = sprite.x
825
830
  result.final_y = sprite.y
826
831
 
827
- if not getattr(sprite, "on_ground", False):
828
- sprite.vy += self.gravity * dt
829
- if sprite.vy > self.max_fall_speed:
830
- sprite.vy = self.max_fall_speed
832
+ if velocity is not None:
833
+ sprite.vx = velocity[0]
834
+ sprite.vy = velocity[1]
835
+ else:
836
+ if not getattr(sprite, "on_ground", False):
837
+ sprite.vy += self.gravity * dt
838
+ if sprite.vy > self.max_fall_speed:
839
+ sprite.vy = self.max_fall_speed
831
840
 
832
- if jump_pressed and getattr(sprite, "on_ground", False):
833
- sprite.vy = self.jump_strength
841
+ if jump_pressed and getattr(sprite, "on_ground", False):
842
+ sprite.vy = self.jump_strength
834
843
 
835
- sprite.vx = input_x * 200.0
844
+ sprite.vx = input_x * self.horizontal_speed
836
845
 
837
846
  delta_x = sprite.vx * dt
838
847
  delta_y = sprite.vy * dt
@@ -1325,6 +1334,7 @@ class CollisionRunner:
1325
1334
  dt: float,
1326
1335
  input_x: float = 0.0,
1327
1336
  jump_pressed: bool = False,
1337
+ velocity: Optional[Vector2] = None,
1328
1338
  ) -> CollisionResult:
1329
1339
  """
1330
1340
  Slope-aware platformer movement.
@@ -1360,6 +1370,13 @@ class CollisionRunner:
1360
1370
  jump_pressed:
1361
1371
  True if jump was pressed during this frame.
1362
1372
 
1373
+ velocity:
1374
+ Optional explicit velocity (vx, vy). When provided, the runner
1375
+ skips built-in input/gravity/jump velocity calculation and only
1376
+ resolves collision for that velocity. This is the preferred
1377
+ path for dash, knockback, wind, moving-platform carry, or a
1378
+ custom controller.
1379
+
1363
1380
  Returns:
1364
1381
  CollisionResult describing the resolved movement and collision
1365
1382
  state after simulation.
@@ -1381,18 +1398,25 @@ class CollisionRunner:
1381
1398
  was_on_ground = getattr(sprite, "on_ground", False)
1382
1399
  jumped = False
1383
1400
 
1384
- if jump_pressed and was_on_ground:
1385
- sprite.vy = self.jump_strength
1386
- sprite.on_ground = False
1387
- jumped = True
1388
- elif not was_on_ground:
1389
- sprite.vy += self.gravity * dt
1390
- if sprite.vy > self.max_fall_speed:
1391
- sprite.vy = self.max_fall_speed
1401
+ if velocity is not None:
1402
+ sprite.vx = velocity[0]
1403
+ sprite.vy = velocity[1]
1404
+ if sprite.vy < 0.0:
1405
+ sprite.on_ground = False
1406
+ jumped = True
1392
1407
  else:
1393
- sprite.vy = min(sprite.vy, 0.0)
1408
+ if jump_pressed and was_on_ground:
1409
+ sprite.vy = self.jump_strength
1410
+ sprite.on_ground = False
1411
+ jumped = True
1412
+ elif not was_on_ground:
1413
+ sprite.vy += self.gravity * dt
1414
+ if sprite.vy > self.max_fall_speed:
1415
+ sprite.vy = self.max_fall_speed
1416
+ else:
1417
+ sprite.vy = min(sprite.vy, 0.0)
1394
1418
 
1395
- sprite.vx = input_x * 200.0
1419
+ sprite.vx = input_x * self.horizontal_speed
1396
1420
  delta_x = sprite.vx * dt
1397
1421
  delta_y = sprite.vy * dt
1398
1422
  bottom_offset = old_bottom - old_y
@@ -1662,6 +1686,7 @@ class CollisionRunner:
1662
1686
  dt,
1663
1687
  input_x=kwargs.get("input_x", 0.0),
1664
1688
  jump_pressed=kwargs.get("jump_pressed", False),
1689
+ velocity=kwargs.get("velocity"),
1665
1690
  )
1666
1691
  elif self.mode == MovementMode.RPG:
1667
1692
  return self.move_rpg(sprite, tileset_collision, tile_map, delta_x, delta_y)
@@ -1736,6 +1761,7 @@ class CollisionRunner:
1736
1761
  runner.gravity = 800.0
1737
1762
  runner.max_fall_speed = 600.0
1738
1763
  runner.jump_strength = -400.0
1764
+ runner.horizontal_speed = 200.0
1739
1765
  runner.slide_friction = 0.1
1740
1766
  runner._game_type = "platformer"
1741
1767
  runner._strict = strict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.15
3
+ Version: 3.1.17
4
4
  Summary: Standalone parser/loader for tilemap-editor JSON maps, sprite animations, and collision detection runtime.
5
5
  Author: tilemap parser contributors
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -29,5 +29,6 @@ tests/test_collision.py
29
29
  tests/test_geometry.py
30
30
  tests/test_map_loader.py
31
31
  tests/test_object_collision.py
32
+ tests/test_object_surfaces.py
32
33
  tests/test_render_scale.py
33
34
  tests/test_tile_collision.py
@@ -0,0 +1,200 @@
1
+ import json
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ import pygame
6
+ import pytest
7
+
8
+ import sys
9
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
10
+
11
+ from tilemap_parser.runtime.map_loader import TilemapData
12
+ from tilemap_parser.parser.map_parse import ParsedObject, ParsedObjectArea
13
+
14
+
15
+ MINIMAL_MAP_META = {
16
+ "tile_size": "16;16",
17
+ "map_size": "10;10",
18
+ "version": "1.1",
19
+ }
20
+
21
+
22
+ def _make_minimal_png(path: Path, size: tuple[int, int] = (32, 16)) -> None:
23
+ surf = pygame.Surface(size)
24
+ surf.fill((255, 0, 255))
25
+ pygame.image.save(surf, str(path))
26
+
27
+
28
+ def _make_map_json_with_objects(tileset_path: str, data_dir: Path) -> Path:
29
+ payload = {
30
+ "meta": {**MINIMAL_MAP_META},
31
+ "resources": {"tilesets": [{"path": tileset_path, "type": "tile"}]},
32
+ "project_state": {"rules": [], "groups": []},
33
+ "data": {
34
+ "ongrid": {},
35
+ "layers": [
36
+ {
37
+ "name": "Object Layer 1",
38
+ "type": "object",
39
+ "visible": True,
40
+ "locked": False,
41
+ "opacity": 1.0,
42
+ "z_index": 1,
43
+ "properties": {},
44
+ "tiles": {},
45
+ "objects": {
46
+ "1": {
47
+ "area": {"x": 10, "y": 20, "w": 16, "h": 16},
48
+ "ttype": 0,
49
+ "tileset_type": "object",
50
+ "variant": 0,
51
+ "properties": {},
52
+ },
53
+ "2": {
54
+ "area": {"x": 50, "y": 60, "w": 32, "h": 32},
55
+ "ttype": 0,
56
+ "tileset_type": "object",
57
+ "variant": 1,
58
+ "properties": {"name": "foo"},
59
+ },
60
+ },
61
+ "next_object_id": 3,
62
+ },
63
+ {
64
+ "name": "Tile Layer",
65
+ "type": "tile",
66
+ "visible": True,
67
+ "locked": False,
68
+ "opacity": 1.0,
69
+ "z_index": 0,
70
+ "properties": {},
71
+ "tiles": {},
72
+ "objects": {},
73
+ },
74
+ ],
75
+ },
76
+ }
77
+ map_path = data_dir / "test_map_objects.json"
78
+ with open(map_path, "w") as f:
79
+ json.dump(payload, f, indent=2)
80
+ return map_path
81
+
82
+
83
+ @pytest.fixture(autouse=True)
84
+ def pygame_init():
85
+ pygame.display.init()
86
+ pygame.display.set_mode((1, 1))
87
+ yield
88
+ pygame.quit()
89
+
90
+
91
+ @pytest.fixture
92
+ def map_data():
93
+ with tempfile.TemporaryDirectory() as tmp:
94
+ tmp = Path(tmp)
95
+ data_dir = tmp / "data"
96
+ assets_dir = tmp / "assets"
97
+ data_dir.mkdir()
98
+ assets_dir.mkdir()
99
+ png = assets_dir / "tileset.png"
100
+ _make_minimal_png(png)
101
+ map_path = _make_map_json_with_objects("../assets/tileset.png", data_dir)
102
+ td = TilemapData.load(map_path)
103
+ yield td, data_dir, assets_dir, png
104
+
105
+
106
+ class TestObjectSurfaces:
107
+ def test_get_object_surface_returns_surface(self, map_data):
108
+ td, *_ = map_data
109
+ obj = ParsedObject(
110
+ area=ParsedObjectArea(x=10, y=20, w=16, h=16),
111
+ ttype=0,
112
+ tileset_type="object",
113
+ variant=0,
114
+ )
115
+ surf = td.get_object_surface(obj)
116
+ assert surf is not None
117
+ assert isinstance(surf, pygame.Surface)
118
+ assert surf.get_size() == (16, 16)
119
+
120
+ def test_get_object_surface_from_parsed_object(self, map_data):
121
+ td, *_ = map_data
122
+ layer = td.get_layer("Object Layer 1")
123
+ assert layer is not None
124
+ obj = layer.objects.get(1)
125
+ assert obj is not None
126
+ surf = td.get_object_surface(obj)
127
+ assert surf is not None
128
+ assert isinstance(surf, pygame.Surface)
129
+
130
+ def test_get_object_surface_by_id_returns_surface_xy(self, map_data):
131
+ td, *_ = map_data
132
+ result = td.get_object_surface_by_id("Object Layer 1", 1)
133
+ assert result is not None
134
+ surf, x, y = result
135
+ assert isinstance(surf, pygame.Surface)
136
+ assert x == 10
137
+ assert y == 20
138
+
139
+ def test_get_object_surface_by_id_second_object(self, map_data):
140
+ td, *_ = map_data
141
+ result = td.get_object_surface_by_id("Object Layer 1", 2)
142
+ assert result is not None
143
+ surf, x, y = result
144
+ assert isinstance(surf, pygame.Surface)
145
+ assert surf.get_size() == (16, 16)
146
+ assert x == 50
147
+ assert y == 60
148
+
149
+ def test_get_object_surface_by_id_bad_layer_returns_none(self, map_data):
150
+ td, *_ = map_data
151
+ result = td.get_object_surface_by_id("Tile Layer", 1)
152
+ assert result is None
153
+
154
+ def test_get_object_surface_by_id_bad_layer_name(self, map_data):
155
+ td, *_ = map_data
156
+ result = td.get_object_surface_by_id("Nonexistent", 1)
157
+ assert result is None
158
+
159
+ def test_get_object_surface_by_id_bad_object_id(self, map_data):
160
+ td, *_ = map_data
161
+ result = td.get_object_surface_by_id("Object Layer 1", 999)
162
+ assert result is None
163
+
164
+ def test_get_object_surfaces_returns_all(self, map_data):
165
+ td, *_ = map_data
166
+ results = td.get_object_surfaces("Object Layer 1")
167
+ assert len(results) == 2
168
+
169
+ surf1, x1, y1, oid1 = results[0]
170
+ assert isinstance(surf1, pygame.Surface)
171
+ assert x1 == 10
172
+ assert y1 == 20
173
+ assert oid1 == 1
174
+
175
+ surf2, x2, y2, oid2 = results[1]
176
+ assert isinstance(surf2, pygame.Surface)
177
+ assert x2 == 50
178
+ assert y2 == 60
179
+ assert oid2 == 2
180
+
181
+ def test_get_object_surfaces_bad_layer(self, map_data):
182
+ td, *_ = map_data
183
+ results = td.get_object_surfaces("Tile Layer")
184
+ assert results == []
185
+
186
+ def test_get_object_surfaces_nonexistent_layer(self, map_data):
187
+ td, *_ = map_data
188
+ results = td.get_object_surfaces("Nonexistent")
189
+ assert results == []
190
+
191
+ def test_get_object_surface_with_out_of_range_ttype_returns_none(self, map_data):
192
+ td, *_ = map_data
193
+ obj = ParsedObject(
194
+ area=ParsedObjectArea(x=0, y=0, w=16, h=16),
195
+ ttype=999,
196
+ tileset_type="object",
197
+ variant=0,
198
+ )
199
+ surf = td.get_object_surface(obj)
200
+ assert surf is None
@@ -267,6 +267,43 @@ class TestMovePlatformer:
267
267
  assert sprite.x > 137
268
268
  assert result.hit_wall_x is False
269
269
 
270
+ def test_explicit_velocity_controls_horizontal_movement(self):
271
+ tile_map = {}
272
+ sprite = MockSprite(x=100, y=100)
273
+ sprite.vx = 0
274
+ sprite.vy = 0
275
+ sprite.on_ground = True
276
+
277
+ result = self.runner.move_platformer(
278
+ sprite,
279
+ self.tileset,
280
+ tile_map,
281
+ dt=0.016,
282
+ input_x=0,
283
+ velocity=(600.0, 0.0),
284
+ )
285
+
286
+ assert result.final_x == pytest.approx(109.6)
287
+ assert sprite.vx == pytest.approx(600.0)
288
+
289
+ def test_move_convenience_forwards_explicit_velocity(self):
290
+ sprite = MockSprite(x=100, y=100)
291
+ sprite.vx = 0
292
+ sprite.vy = 0
293
+ sprite.on_ground = True
294
+
295
+ result = self.runner.move(
296
+ sprite,
297
+ self.tileset,
298
+ {},
299
+ dt=0.016,
300
+ input_x=0,
301
+ velocity=(600.0, 0.0),
302
+ )
303
+
304
+ assert result.final_x == pytest.approx(109.6)
305
+ assert sprite.vx == pytest.approx(600.0)
306
+
270
307
 
271
308
  class TestMovePlatformerWithSlide:
272
309
  def setup_method(self):
@@ -334,6 +371,24 @@ class TestMovePlatformerWithSlide:
334
371
  assert sprite.x == 103
335
372
  assert result.hit_wall_x is True
336
373
 
374
+ def test_explicit_velocity_controls_dash_without_input_scaling(self):
375
+ sprite = MockSprite(x=100, y=100)
376
+ sprite.vx = 0
377
+ sprite.vy = 0
378
+ sprite.on_ground = True
379
+
380
+ result = self.runner.move_platformer_with_slide(
381
+ sprite,
382
+ self.tileset,
383
+ {},
384
+ dt=0.016,
385
+ input_x=0,
386
+ velocity=(600.0, 0.0),
387
+ )
388
+
389
+ assert result.final_x == pytest.approx(109.6)
390
+ assert sprite.vx == pytest.approx(600.0)
391
+
337
392
 
338
393
  class TestMoveRpg:
339
394
  def setup_method(self):
File without changes