tilemap-parser 3.1.9__tar.gz → 3.1.12__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.9 → tilemap_parser-3.1.12}/PKG-INFO +1 -1
  2. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/pyproject.toml +1 -1
  3. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/__init__.py +9 -0
  4. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/parser/__init__.py +14 -0
  5. tilemap_parser-3.1.12/src/tilemap_parser/parser/particle.py +154 -0
  6. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/__init__.py +16 -0
  7. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/map_loader.py +23 -9
  8. tilemap_parser-3.1.12/src/tilemap_parser/runtime/particles.py +570 -0
  9. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/renderer.py +70 -25
  10. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser.egg-info/PKG-INFO +1 -1
  11. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser.egg-info/SOURCES.txt +2 -0
  12. tilemap_parser-3.1.12/tests/test_map_loader.py +275 -0
  13. tilemap_parser-3.1.9/tests/test_map_loader.py +0 -122
  14. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/LICENSE +0 -0
  15. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/README.md +0 -0
  16. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/setup.cfg +0 -0
  17. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/parser/animation.py +0 -0
  18. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/parser/collision.py +0 -0
  19. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/parser/collision_loader.py +0 -0
  20. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/parser/map_parse.py +0 -0
  21. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/parser/node_parse.py +0 -0
  22. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/animation_player.py +0 -0
  23. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/area_node.py +0 -0
  24. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/collision_cache.py +0 -0
  25. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/object_collision.py +0 -0
  26. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/runtime/tile_collision.py +0 -0
  27. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/utils/__init__.py +0 -0
  28. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser/utils/geometry.py +0 -0
  29. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser.egg-info/dependency_links.txt +0 -0
  30. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser.egg-info/requires.txt +0 -0
  31. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/src/tilemap_parser.egg-info/top_level.txt +0 -0
  32. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/tests/test_collision.py +0 -0
  33. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/tests/test_geometry.py +0 -0
  34. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/tests/test_object_collision.py +0 -0
  35. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/tests/test_render_scale.py +0 -0
  36. {tilemap_parser-3.1.9 → tilemap_parser-3.1.12}/tests/test_tile_collision.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-parser
3
- Version: 3.1.9
3
+ Version: 3.1.12
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.9"
7
+ version = "3.1.12"
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"
@@ -45,6 +45,15 @@ __all__ = [
45
45
  "TileLayerRenderer",
46
46
  "TilesetCollision",
47
47
  "TilemapData",
48
+ "Particle",
49
+ "ParticleEmitter",
50
+ "ParticleRenderer",
51
+ "ParticleSystem",
52
+ "ParticleSystemConfig",
53
+ "SpriteBatchRenderer",
54
+ "clear_texture_caches",
55
+ "parse_particle_dict",
56
+ "parse_particle_file",
48
57
  "aabb_overlap",
49
58
  "check_collision",
50
59
  "circle_vs_circle",
@@ -46,6 +46,14 @@ from .map_parse import (
46
46
  parse_map_json,
47
47
  )
48
48
  from .node_parse import ParsedNode, parse_nodes_dict, parse_nodes_file
49
+ from .particle import (
50
+ ALPHA_FADE_MODES,
51
+ EMISSION_SHAPES,
52
+ PARTICLE_SHAPES,
53
+ ParticleSystemConfig,
54
+ parse_particle_dict,
55
+ parse_particle_file,
56
+ )
49
57
 
50
58
  __all__ = [
51
59
  "AnimationClip",
@@ -90,4 +98,10 @@ __all__ = [
90
98
  "parse_nodes_file",
91
99
  "parse_object_collision",
92
100
  "parse_tileset_collision",
101
+ "ALPHA_FADE_MODES",
102
+ "EMISSION_SHAPES",
103
+ "PARTICLE_SHAPES",
104
+ "ParticleSystemConfig",
105
+ "parse_particle_dict",
106
+ "parse_particle_file",
93
107
  ]
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Tuple, Union
7
+
8
+ from .map_parse import (
9
+ JsonDict,
10
+ MapParseError,
11
+ _coerce_float,
12
+ _coerce_int,
13
+ _optional_dict,
14
+ _require_dict,
15
+ _require_list,
16
+ _require_str,
17
+ _ctx,
18
+ )
19
+
20
+
21
+ EMISSION_SHAPES = ["point", "rect", "circle", "line"]
22
+ PARTICLE_SHAPES = ["circle", "square", "diamond", "star", "sparkle", "smoke", "heart", "line"]
23
+ ALPHA_FADE_MODES = ["none", "fade_out", "fade_in", "fade_both"]
24
+
25
+
26
+ @dataclass
27
+ class ParticleSystemConfig:
28
+ name: str
29
+ emission_shape: str = "point"
30
+ particle_shape: str = "circle"
31
+ particle_size_min: int = 2
32
+ particle_size_max: int = 6
33
+ spawn_rate: int = 20
34
+ max_particles: int = 100
35
+ lifetime_min: float = 0.5
36
+ lifetime_max: float = 2.0
37
+ speed_min: float = 20.0
38
+ speed_max: float = 60.0
39
+ direction: float = -1.0
40
+ spread: float = 45.0
41
+ gravity_x: float = 0.0
42
+ gravity_y: float = 30.0
43
+ start_color_r: int = 255
44
+ start_color_g: int = 200
45
+ start_color_b: int = 100
46
+ start_color_a: int = 255
47
+ end_color_r: int = 255
48
+ end_color_g: int = 100
49
+ end_color_b: int = 50
50
+ end_color_a: int = 0
51
+ start_scale: float = 1.0
52
+ end_scale: float = 0.3
53
+ rotation_speed: float = 0.0
54
+ alpha_fade: str = "fade_out"
55
+
56
+ def to_dict(self) -> JsonDict:
57
+ return {
58
+ "emission_shape": self.emission_shape,
59
+ "particle_shape": self.particle_shape,
60
+ "particle_size_min": self.particle_size_min,
61
+ "particle_size_max": self.particle_size_max,
62
+ "spawn_rate": self.spawn_rate,
63
+ "max_particles": self.max_particles,
64
+ "lifetime_min": self.lifetime_min,
65
+ "lifetime_max": self.lifetime_max,
66
+ "speed_min": self.speed_min,
67
+ "speed_max": self.speed_max,
68
+ "direction": self.direction,
69
+ "spread": self.spread,
70
+ "gravity_x": self.gravity_x,
71
+ "gravity_y": self.gravity_y,
72
+ "start_color_r": self.start_color_r,
73
+ "start_color_g": self.start_color_g,
74
+ "start_color_b": self.start_color_b,
75
+ "start_color_a": self.start_color_a,
76
+ "end_color_r": self.end_color_r,
77
+ "end_color_g": self.end_color_g,
78
+ "end_color_b": self.end_color_b,
79
+ "end_color_a": self.end_color_a,
80
+ "start_scale": self.start_scale,
81
+ "end_scale": self.end_scale,
82
+ "rotation_speed": self.rotation_speed,
83
+ "alpha_fade": self.alpha_fade,
84
+ }
85
+
86
+ @classmethod
87
+ def from_dict(cls, d: JsonDict, name: str = "") -> "ParticleSystemConfig":
88
+ def g(key: str, default: Any = 0) -> Any:
89
+ return d.get(key, default)
90
+
91
+ cfg = cls(name=name)
92
+ cfg.emission_shape = str(g("emission_shape", "point"))
93
+ if cfg.emission_shape not in EMISSION_SHAPES:
94
+ cfg.emission_shape = "point"
95
+ cfg.particle_shape = str(g("particle_shape", "circle"))
96
+ if cfg.particle_shape not in PARTICLE_SHAPES:
97
+ cfg.particle_shape = "circle"
98
+ cfg.alpha_fade = str(g("alpha_fade", "fade_out"))
99
+ if cfg.alpha_fade not in ALPHA_FADE_MODES:
100
+ cfg.alpha_fade = "fade_out"
101
+ cfg.particle_size_min = max(1, int(g("particle_size_min", 2)))
102
+ cfg.particle_size_max = max(cfg.particle_size_min, int(g("particle_size_max", 6)))
103
+ cfg.spawn_rate = max(1, int(g("spawn_rate", 20)))
104
+ cfg.max_particles = max(1, int(g("max_particles", 100)))
105
+ cfg.lifetime_min = max(0.1, float(g("lifetime_min", 0.5)))
106
+ cfg.lifetime_max = max(cfg.lifetime_min, float(g("lifetime_max", 2.0)))
107
+ cfg.speed_min = max(0.0, float(g("speed_min", 20)))
108
+ cfg.speed_max = max(cfg.speed_min, float(g("speed_max", 60)))
109
+ cfg.direction = float(g("direction", -1))
110
+ cfg.spread = max(0.0, min(360.0, float(g("spread", 45))))
111
+ cfg.gravity_x = float(g("gravity_x", 0))
112
+ cfg.gravity_y = float(g("gravity_y", 30))
113
+ cfg.start_color_r = max(0, min(255, int(g("start_color_r", 255))))
114
+ cfg.start_color_g = max(0, min(255, int(g("start_color_g", 200))))
115
+ cfg.start_color_b = max(0, min(255, int(g("start_color_b", 100))))
116
+ cfg.start_color_a = max(0, min(255, int(g("start_color_a", 255))))
117
+ cfg.end_color_r = max(0, min(255, int(g("end_color_r", 255))))
118
+ cfg.end_color_g = max(0, min(255, int(g("end_color_g", 100))))
119
+ cfg.end_color_b = max(0, min(255, int(g("end_color_b", 50))))
120
+ cfg.end_color_a = max(0, min(255, int(g("end_color_a", 0))))
121
+ cfg.start_scale = max(0.1, float(g("start_scale", 1.0)))
122
+ cfg.end_scale = max(0.1, float(g("end_scale", 0.3)))
123
+ cfg.rotation_speed = float(g("rotation_speed", 0))
124
+ return cfg
125
+
126
+
127
+ def _parse_system(raw: Any, ctx: str) -> ParticleSystemConfig:
128
+ d = _require_dict(raw, ctx)
129
+ name = str(d.get("name", ""))
130
+ cfg_raw = _optional_dict(d.get("config"), f"{ctx}.config")
131
+ if cfg_raw is None:
132
+ cfg_raw = {}
133
+ return ParticleSystemConfig.from_dict(cfg_raw, name=name)
134
+
135
+
136
+ def parse_particle_dict(root: JsonDict) -> List[ParticleSystemConfig]:
137
+ root = _require_dict(root, "root")
138
+ raw_systems = _require_list(root.get("particle_systems", []), "root.particle_systems")
139
+ return [_parse_system(item, f"root.particle_systems[{i}]") for i, item in enumerate(raw_systems)]
140
+
141
+
142
+ def parse_particle_file(path: Union[str, Path]) -> List[ParticleSystemConfig]:
143
+ p = Path(path)
144
+ if not p.is_file():
145
+ raise MapParseError(f"Not a file: {p}")
146
+ try:
147
+ text = p.read_text(encoding="utf-8")
148
+ except OSError as e:
149
+ raise MapParseError(f"Cannot read {p}: {e}") from e
150
+ try:
151
+ payload = json.loads(text)
152
+ except json.JSONDecodeError as e:
153
+ raise MapParseError(f"Invalid JSON in {p}: {e}") from e
154
+ return parse_particle_dict(payload)
@@ -24,6 +24,15 @@ from .object_collision import (
24
24
  )
25
25
  from .renderer import LayerRenderStats, TileLayerRenderer
26
26
  from .area_node import AreaNode
27
+ from .particles import (
28
+ Particle,
29
+ ParticleEmitter,
30
+ ParticleEmitterNode,
31
+ ParticleRenderer,
32
+ ParticleSystem,
33
+ SpriteBatchRenderer,
34
+ clear_texture_caches,
35
+ )
27
36
 
28
37
  __all__ = [
29
38
  "AnimationPlayer",
@@ -49,4 +58,11 @@ __all__ = [
49
58
  "load_map",
50
59
  "load_object_collision",
51
60
  "load_tileset_collision",
61
+ "Particle",
62
+ "ParticleEmitter",
63
+ "ParticleEmitterNode",
64
+ "ParticleRenderer",
65
+ "ParticleSystem",
66
+ "SpriteBatchRenderer",
67
+ "clear_texture_caches",
52
68
  ]
@@ -10,6 +10,8 @@ from pygame import Rect, Surface
10
10
 
11
11
  from ..parser.map_parse import MapParseError, ParsedLayer, ParsedMap, ParsedTile, parse_map_file
12
12
  from ..parser.node_parse import parse_nodes_dict
13
+ from .area_node import AreaNode
14
+ from .particles import ParticleEmitterNode
13
15
 
14
16
  PathLike = Union[str, Path]
15
17
 
@@ -29,6 +31,8 @@ class TilemapData:
29
31
  self.resolved_paths = resolved_paths
30
32
  self.warnings = warnings
31
33
  self.map_path = map_path
34
+ self.area_nodes: List[AreaNode] = []
35
+ self.particle_emitters: List[ParticleEmitterNode] = []
32
36
  self._tw, self._th = parsed.meta.tile_size
33
37
  self._build_path_index()
34
38
  self._normalize_tile_ttypes()
@@ -40,6 +44,7 @@ class TilemapData:
40
44
  *,
41
45
  extra_search_base: Optional[Path] = None,
42
46
  skip_missing_images: bool = True,
47
+ nodes_dir: Optional[PathLike] = None,
43
48
  ) -> "TilemapData":
44
49
  p = Path(path)
45
50
  parsed = parse_map_file(p)
@@ -69,12 +74,16 @@ class TilemapData:
69
74
  surfaces.append(None)
70
75
 
71
76
  nodes_name = f"{p.stem}.nodes.json"
72
- nodes_candidates = [
73
- map_dir / nodes_name,
74
- map_dir.parent / "nodes" / nodes_name,
75
- ]
76
- if extra_search_base is not None:
77
- nodes_candidates.append(extra_search_base / "nodes" / nodes_name)
77
+ nodes_candidates: List[Path] = []
78
+ if nodes_dir is not None:
79
+ nodes_candidates.append(Path(nodes_dir) / nodes_name)
80
+ else:
81
+ nodes_candidates = [
82
+ map_dir / nodes_name,
83
+ map_dir.parent / "nodes" / nodes_name,
84
+ ]
85
+ if extra_search_base is not None:
86
+ nodes_candidates.append(extra_search_base / "nodes" / nodes_name)
78
87
  for nodes_path in nodes_candidates:
79
88
  if nodes_path.is_file():
80
89
  try:
@@ -89,7 +98,12 @@ class TilemapData:
89
98
  warnings.append(f"Failed to load nodes: {e}")
90
99
  break
91
100
 
92
- return cls(parsed, surfaces, resolved_paths, warnings, map_path=p)
101
+ result = cls(parsed, surfaces, resolved_paths, warnings, map_path=p)
102
+ result.area_nodes = [AreaNode(n) for n in parsed.nodes if n.node_type == "area"]
103
+ result.particle_emitters = [
104
+ ParticleEmitterNode(n) for n in parsed.nodes if n.node_type == "particle_emitter"
105
+ ]
106
+ return result
93
107
 
94
108
  def _build_path_index(self) -> None:
95
109
  self._path_to_index: Dict[str, int] = {}
@@ -246,5 +260,5 @@ def _resolve_resource_path(path_str: str, map_dir: Path, extra_search_base: Opti
246
260
  return candidate
247
261
 
248
262
 
249
- def load_map(path: PathLike, *, extra_search_base: Optional[Path] = None, skip_missing_images: bool = True) -> TilemapData:
250
- return TilemapData.load(path, extra_search_base=extra_search_base, skip_missing_images=skip_missing_images)
263
+ def load_map(path: PathLike, *, extra_search_base: Optional[Path] = None, skip_missing_images: bool = True, nodes_dir: Optional[PathLike] = None) -> TilemapData:
264
+ return TilemapData.load(path, extra_search_base=extra_search_base, skip_missing_images=skip_missing_images, nodes_dir=nodes_dir)