Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
241 lines
7.7 KiB
Python
241 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
从 Unity Levels*.cs 导出 Cocos LevelRegistry 数据。
|
||
用法:
|
||
python3 tools/export_unity_levels.py \
|
||
--input "/path/to/Levels600.cs" \
|
||
--output assets/scripts/level/levels-600.generated.ts \
|
||
--border-cache assets/scripts/level/border-cache.json
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from export_unity_prefab_maps import parse_level_prefab
|
||
|
||
DIR = {
|
||
"Direction.North": "Direction.North",
|
||
"Direction.East": "Direction.East",
|
||
"Direction.South": "Direction.South",
|
||
"Direction.West": "Direction.West",
|
||
}
|
||
|
||
LEVEL_RE = re.compile(
|
||
r"\{(\d+),new Level\(\)\{LevelID\s*=\s*\d+,spawns\s*=\s*new List<Spawn>\(\)\{",
|
||
re.MULTILINE,
|
||
)
|
||
|
||
SPAWN_RE = re.compile(
|
||
r"new Spawn\(\)\{([^}]+(?:\{[^}]*\})?)\}",
|
||
re.MULTILINE,
|
||
)
|
||
|
||
POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),0\)")
|
||
PATH_RE = re.compile(r'path\s*=\s*"?([^",\s]+)"?')
|
||
PDIR_RE = re.compile(r"playerDirection\s*=\s*(Direction\.\w+)")
|
||
VDIR_RE = re.compile(r"vehicleDirection\s*=\s*(Direction\.\w+)")
|
||
BOUND_RE = re.compile(r"boundary\s*=\s*new Vector3Int\((\d+),(\d+),0\)")
|
||
|
||
|
||
def kind_from_path(path: str) -> str:
|
||
p = path.lower()
|
||
if "player" in p and "multplay" not in p:
|
||
return "player"
|
||
if "vehicle" in p:
|
||
return "vehicle"
|
||
if "enemy" in p:
|
||
return "enemy"
|
||
if "prop" in p:
|
||
return "prop"
|
||
return "prop_decor"
|
||
|
||
|
||
def prop_placement_from_path(path: str) -> str | None:
|
||
if "nprop" in path.lower():
|
||
return "ground"
|
||
if "prop" in path.lower():
|
||
return "block"
|
||
return None
|
||
|
||
|
||
def parse_spawns(block: str) -> list[dict]:
|
||
spawns = []
|
||
for m in SPAWN_RE.finditer(block):
|
||
body = m.group(1)
|
||
pm = POS_RE.search(body)
|
||
if not pm:
|
||
continue
|
||
path_m = PATH_RE.search(body)
|
||
path = path_m.group(1) if path_m else ""
|
||
item: dict = {
|
||
"x": int(pm.group(1)),
|
||
"y": int(pm.group(2)),
|
||
"kind": kind_from_path(path),
|
||
}
|
||
placement = prop_placement_from_path(path)
|
||
if placement and item["kind"] == "prop":
|
||
item["propPlacement"] = placement
|
||
pdir = PDIR_RE.search(body)
|
||
vdir = VDIR_RE.search(body)
|
||
if pdir:
|
||
item["playerDirection"] = pdir.group(1)
|
||
if vdir:
|
||
item["vehicleDirection"] = vdir.group(1)
|
||
spawns.append(item)
|
||
return spawns
|
||
|
||
|
||
def ring_border_key(bx: int, by: int) -> str:
|
||
return f"{bx},{by}"
|
||
|
||
|
||
def parse_file(text: str) -> dict[int, dict]:
|
||
levels: dict[int, dict] = {}
|
||
matches = list(LEVEL_RE.finditer(text))
|
||
for i, m in enumerate(matches):
|
||
lid = int(m.group(1))
|
||
start = m.end()
|
||
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||
chunk = text[start:end]
|
||
bound = BOUND_RE.search(chunk)
|
||
bx, by = (10, 10)
|
||
if bound:
|
||
bx, by = int(bound.group(1)), int(bound.group(2))
|
||
spawns = parse_spawns(chunk)
|
||
levels[lid] = {
|
||
"levelID": lid,
|
||
"boundary": {"x": bx, "y": by},
|
||
"borderKey": ring_border_key(bx, by),
|
||
"spawns": spawns,
|
||
}
|
||
return levels
|
||
|
||
|
||
def spawn_to_ts(s: dict, indent: str) -> str:
|
||
parts = [
|
||
f"x: {s['x']}",
|
||
f"y: {s['y']}",
|
||
f"kind: '{s['kind']}'",
|
||
]
|
||
if "playerDirection" in s:
|
||
parts.append(f"playerDirection: {s['playerDirection']}")
|
||
if "vehicleDirection" in s:
|
||
parts.append(f"vehicleDirection: {s['vehicleDirection']}")
|
||
if s.get("propPlacement"):
|
||
parts.append(f"propPlacement: '{s['propPlacement']}'")
|
||
return indent + "{ " + ", ".join(parts) + " }"
|
||
|
||
|
||
def emit_ts(levels: dict[int, dict], export_const: str = "LEVELS_600") -> str:
|
||
lines = [
|
||
"/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */",
|
||
"/* spawns 来自 Levels*.cs;ground/border 来自 Assets/Prefabs/Level/LevelN.prefab Tilemap */",
|
||
"import { Direction } from '../core/Define';",
|
||
"import { LevelConfig } from './LevelTypes';",
|
||
"",
|
||
f"export const {export_const}: Record<number, LevelConfig> = {{",
|
||
]
|
||
for lid in sorted(levels.keys()):
|
||
L = levels[lid]
|
||
lines.append(f" {lid}: {{")
|
||
lines.append(f" levelID: {lid},")
|
||
lines.append(
|
||
f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }},"
|
||
)
|
||
if L.get("ground"):
|
||
lines.append(
|
||
" ground: "
|
||
+ json.dumps(L["ground"], ensure_ascii=False, separators=(",", ": "))
|
||
+ ","
|
||
)
|
||
if L.get("border"):
|
||
lines.append(
|
||
" border: "
|
||
+ json.dumps(L["border"], ensure_ascii=False, separators=(",", ": "))
|
||
+ ","
|
||
)
|
||
lines.append(" spawns: [")
|
||
for s in L["spawns"]:
|
||
lines.append(spawn_to_ts(s, " ") + ",")
|
||
lines.append(" ],")
|
||
lines.append(" },")
|
||
lines.append("};")
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def build_border_cache(levels: dict[int, dict]) -> dict[str, dict]:
|
||
cache: dict[str, dict] = {}
|
||
for L in levels.values():
|
||
key = L["borderKey"]
|
||
if key in cache:
|
||
continue
|
||
bx, by = L["boundary"]["x"], L["boundary"]["y"]
|
||
border: dict[str, bool] = {}
|
||
for x in range(-bx, bx + 1):
|
||
for y in range(-by, by + 1):
|
||
if abs(x) == bx or abs(y) == by:
|
||
border[f"{x},{y}"] = True
|
||
cache[key] = border
|
||
return cache
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--input", required=True, help="Unity Levels*.cs path")
|
||
ap.add_argument("--output", required=True, help="Output .ts path")
|
||
ap.add_argument("--border-cache", default="", help="Optional border cache json")
|
||
ap.add_argument("--limit", type=int, default=0, help="Max levels to export (0 = all)")
|
||
ap.add_argument("--export-const", default="LEVELS_600", help="TS export const name")
|
||
ap.add_argument(
|
||
"--prefab-dir",
|
||
default="",
|
||
help="Unity Assets/Prefabs/Level 目录,合并 LevelN.prefab 的 Tilemap",
|
||
)
|
||
args = ap.parse_args()
|
||
|
||
text = Path(args.input).read_text(encoding="utf-8")
|
||
levels = parse_file(text)
|
||
if args.limit > 0:
|
||
sorted_ids = sorted(levels.keys())[: args.limit]
|
||
levels = {k: levels[k] for k in sorted_ids}
|
||
if not levels:
|
||
print("No levels parsed", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
prefab_dir = Path(args.prefab_dir) if args.prefab_dir else None
|
||
merged_prefab = 0
|
||
if prefab_dir:
|
||
for lid, L in levels.items():
|
||
maps = parse_level_prefab(prefab_dir / f"Level{lid}.prefab")
|
||
if maps:
|
||
L["ground"] = maps.get("ground", {})
|
||
L["border"] = maps.get("border", {})
|
||
merged_prefab += 1
|
||
else:
|
||
ring = build_border_cache({lid: L})[L["borderKey"]]
|
||
L["border"] = ring
|
||
print(f"Merged Tilemap from {merged_prefab} prefabs")
|
||
else:
|
||
border_cache = build_border_cache(levels)
|
||
for L in levels.values():
|
||
L["border"] = border_cache[L["borderKey"]]
|
||
|
||
out = Path(args.output)
|
||
out.parent.mkdir(parents=True, exist_ok=True)
|
||
out.write_text(emit_ts(levels, args.export_const), encoding="utf-8")
|
||
|
||
if args.border_cache:
|
||
cache = {str(lid): L.get("border", {}) for lid, L in levels.items()}
|
||
Path(args.border_cache).write_text(json.dumps(cache, indent=2), encoding="utf-8")
|
||
|
||
print(f"Exported {len(levels)} levels -> {out}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|