Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene. Co-authored-by: Cursor <cursoragent@cursor.com>
191 lines
5.9 KiB
Python
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()
|