File size: 4,808 Bytes
dd96d2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29a88f8
dd96d2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29a88f8
 
dd96d2f
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""
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",
    )