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>
This commit is contained in:
190
tools/export_unity_levels.py
Normal file
190
tools/export_unity_levels.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user