Spaces:
Sleeping
Sleeping
| """ | |
| Compile walkable polygons into a single figure: one exterior boundary + holes (internal non-walkable zones). | |
| Produces compiled_map.json used by pathfinding for walkable test and optional nav points. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| def _signed_area(polygon: list[tuple[float, float]]) -> float: | |
| """Signed area (positive = CCW).""" | |
| n = len(polygon) | |
| if n < 3: | |
| return 0.0 | |
| area = 0.0 | |
| for i in range(n): | |
| j = (i + 1) % n | |
| area += polygon[i][0] * polygon[j][1] | |
| area -= polygon[j][0] * polygon[i][1] | |
| return area / 2.0 | |
| def _centroid(polygon: list[tuple[float, float]]) -> tuple[float, float]: | |
| n = len(polygon) | |
| if n == 0: | |
| return (0.0, 0.0) | |
| cx = sum(p[0] for p in polygon) / n | |
| cy = sum(p[1] for p in polygon) / n | |
| return (cx, cy) | |
| def _point_in_polygon(x: float, y: float, polygon: list[tuple[float, float]]) -> bool: | |
| n = len(polygon) | |
| inside = False | |
| j = n - 1 | |
| for i in range(n): | |
| xi, yi = polygon[i] | |
| xj, yj = polygon[j] | |
| if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi): | |
| inside = not inside | |
| j = i | |
| return inside | |
| def compile_walkable( | |
| polygons: list[list[tuple[float, float]]], | |
| ) -> tuple[list[tuple[float, float]], list[list[tuple[float, float]]]]: | |
| """ | |
| From a list of polygons (each list of (x,y) in 0-100 space), | |
| return (exterior, holes). | |
| Exterior = polygon with largest absolute area. | |
| Holes = polygons whose centroid is inside the exterior. | |
| """ | |
| if not polygons: | |
| return ([], []) | |
| # Filter to valid polygons | |
| valid = [p for p in polygons if len(p) >= 3] | |
| if not valid: | |
| return ([], []) | |
| # Exterior = largest area | |
| by_area = [(abs(_signed_area(p)), p) for p in valid] | |
| by_area.sort(key=lambda x: -x[0]) | |
| exterior = by_area[0][1] | |
| others = [p for _, p in by_area[1:]] | |
| holes: list[list[tuple[float, float]]] = [] | |
| for poly in others: | |
| cx, cy = _centroid(poly) | |
| if _point_in_polygon(cx, cy, exterior): | |
| holes.append(poly) | |
| return (exterior, holes) | |
| def build_nav_points( | |
| polygons: list[list[tuple[float, float]]], | |
| scale_x: float, | |
| scale_y: float, | |
| step: float = 2.0, | |
| ) -> list[list[float]]: | |
| """ | |
| Generate navigation points (in game coordinates) for ALL walkable polygons. | |
| Used for pathfinding graph: units can path between these points. | |
| """ | |
| if not polygons: | |
| return [] | |
| all_xs = [p[0] for poly in polygons for p in poly] | |
| all_ys = [p[1] for poly in polygons for p in poly] | |
| min_x, max_x = min(all_xs), max(all_xs) | |
| min_y, max_y = min(all_ys), max(all_ys) | |
| def is_walkable_100(x: float, y: float) -> bool: | |
| return any(_point_in_polygon(x, y, poly) for poly in polygons) | |
| points: list[list[float]] = [] | |
| x = min_x | |
| while x <= max_x: | |
| y = min_y | |
| while y <= max_y: | |
| if is_walkable_100(x, y): | |
| points.append([x * scale_x / 100.0, y * scale_y / 100.0]) | |
| y += step | |
| x += step | |
| return points | |
| def run_compiler( | |
| walkable_path: Path, | |
| output_path: Path, | |
| map_width: float = 80.0, | |
| map_height: float = 80.0, | |
| nav_step: float = 1.5, | |
| ) -> None: | |
| """Load walkable.json, compile to exterior + holes, write compiled_map.json.""" | |
| scale_x = map_width / 100.0 | |
| scale_y = map_height / 100.0 | |
| if not walkable_path.exists(): | |
| raise FileNotFoundError(f"Walkable file not found: {walkable_path}") | |
| with open(walkable_path, encoding="utf-8") as f: | |
| data = json.load(f) | |
| raw = data.get("polygons", []) | |
| if not raw and data.get("polygon"): | |
| raw = [data["polygon"]] | |
| polygons: list[list[tuple[float, float]]] = [] | |
| for poly in raw: | |
| if len(poly) < 3: | |
| continue | |
| polygons.append([(float(p[0]), float(p[1])) for p in poly]) | |
| exterior, holes = compile_walkable(polygons) | |
| nav_points_game = build_nav_points(polygons, map_width, map_height, step=nav_step) | |
| out = { | |
| "exterior": [[round(x, 4), round(y, 4)] for x, y in exterior], | |
| "holes": [[[round(x, 4), round(y, 4)] for x, y in h] for h in holes], | |
| "nav_points": [[round(x, 4), round(y, 4)] for x, y in nav_points_game], | |
| # All walkable zones in 0-100 space (for visualization and multi-zone support) | |
| "walkable_zones": [[[round(p[0], 4), round(p[1], 4)] for p in poly] for poly in polygons], | |
| } | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| json.dump(out, f, indent=2) | |
| if __name__ == "__main__": | |
| static_dir = Path(__file__).resolve().parent.parent / "static" | |
| run_compiler( | |
| static_dir / "walkable.json", | |
| static_dir / "compiled_map.json", | |
| ) | |