Files
cocos/tools/export_unity_levels.py
刘宇飞 cba5105908 Initial Cocos Creator port of main-site Unity WebGL game.
Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 14:57:46 +08:00

191 lines
5.9 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
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 "nprop" in p:
return "prop_decor"
if "prop" in p:
return "prop"
return "prop_decor"
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),
}
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']}")
return indent + "{ " + ", ".join(parts) + " }"
def emit_ts(levels: dict[int, dict], border_cache: dict[str, dict]) -> str:
lines = [
"/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */",
"import { Direction } from '../core/Define';",
"import { LevelConfig, SpawnConfig } from './LevelTypes';",
"",
"const BORDER_CACHE: Record<string, Record<string, boolean>> = " + json.dumps(border_cache, separators=(',', ':')) + ";",
"",
"function withBorder(key: string, cfg: Omit<LevelConfig, 'border'>): LevelConfig {",
" return { ...cfg, border: BORDER_CACHE[key] };",
"}",
"",
"export const LEVELS_600: Record<number, LevelConfig> = {",
]
for lid in sorted(levels.keys()):
L = levels[lid]
lines.append(f" {lid}: withBorder('{L['borderKey']}', {{")
lines.append(f" levelID: {lid},")
lines.append(f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }},")
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")
args = ap.parse_args()
text = Path(args.input).read_text(encoding="utf-8")
levels = parse_file(text)
if not levels:
print("No levels parsed", file=sys.stderr)
sys.exit(1)
border_cache = build_border_cache(levels)
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(emit_ts(levels, border_cache), encoding="utf-8")
if args.border_cache:
Path(args.border_cache).write_text(json.dumps(border_cache, indent=2), encoding="utf-8")
print(f"Exported {len(levels)} levels -> {out}")
if __name__ == "__main__":
main()