Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
189
tools/export_all_levels.py
Normal file
189
tools/export_all_levels.py
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
从 Unity 全量导出关卡到单一 JSON 数据库(方便后期增删查改)。
|
||||
|
||||
数据来源:
|
||||
- Assets/Scripts/Core/Levels*.cs → spawns / boundary / levelPath
|
||||
- Assets/Prefabs/Level/LevelN.prefab → Ground / Border Tilemap
|
||||
|
||||
用法:
|
||||
python3 tools/export_all_levels.py \\
|
||||
--unity-root "/path/to/主站" \\
|
||||
--output assets/level-data/levels-database.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from export_unity_levels import (
|
||||
LEVEL_RE,
|
||||
BOUND_RE,
|
||||
SPAWN_RE,
|
||||
POS_RE,
|
||||
PATH_RE,
|
||||
PDIR_RE,
|
||||
VDIR_RE,
|
||||
kind_from_path,
|
||||
parse_spawns,
|
||||
build_border_cache,
|
||||
)
|
||||
from export_unity_prefab_maps import parse_level_prefab
|
||||
|
||||
from level_id import LEVEL_ID_BASE, normalize_level_id, prefab_resource_path
|
||||
|
||||
|
||||
LEVEL_PATH_RE = re.compile(
|
||||
r'levelPath\s*=\s*"?([^",\s]+Level\d+\.prefab)"?', re.I
|
||||
)
|
||||
|
||||
|
||||
def parse_level_block(chunk: str, lid: int) -> dict:
|
||||
bound = BOUND_RE.search(chunk)
|
||||
bx, by = (10, 10)
|
||||
if bound:
|
||||
bx, by = int(bound.group(1)), int(bound.group(2))
|
||||
path_m = LEVEL_PATH_RE.search(chunk)
|
||||
level_path = path_m.group(1) if path_m else f"Assets/Prefabs/Level/Level{lid}.prefab"
|
||||
ext_id = normalize_level_id(lid)
|
||||
return {
|
||||
"levelID": ext_id,
|
||||
"boundary": {"x": bx, "y": by},
|
||||
"spawns": parse_spawns(chunk),
|
||||
"unityPrefab": level_path.replace("\\", "/"),
|
||||
"cocosPrefab": prefab_resource_path(ext_id),
|
||||
}
|
||||
|
||||
|
||||
def parse_levels_cs_file(path: Path) -> dict[int, dict]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
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)
|
||||
levels[lid] = parse_level_block(text[start:end], lid)
|
||||
return levels
|
||||
|
||||
|
||||
def parse_all_levels_cs(core_dir: Path) -> dict[int, dict]:
|
||||
merged: dict[int, dict] = {}
|
||||
for cs in sorted(core_dir.glob("Levels*.cs")):
|
||||
part = parse_levels_cs_file(cs)
|
||||
for lid, L in part.items():
|
||||
if lid in merged:
|
||||
print(f"warn: duplicate level id {lid} in {cs.name}, keep first", file=sys.stderr)
|
||||
continue
|
||||
merged[lid] = L
|
||||
return merged
|
||||
|
||||
|
||||
def prefab_path_for_level(L: dict, prefab_dir: Path) -> Path | None:
|
||||
rel = L.get("unityPrefab", "")
|
||||
if rel:
|
||||
name = Path(rel).name
|
||||
p = prefab_dir / name
|
||||
if p.is_file():
|
||||
return p
|
||||
p = prefab_dir / f"Level{L['levelID']}.prefab"
|
||||
return p if p.is_file() else None
|
||||
|
||||
|
||||
def merge_prefab_maps(L: dict, prefab_dir: Path) -> None:
|
||||
p = prefab_path_for_level(L, prefab_dir)
|
||||
if not p:
|
||||
bx, by = L["boundary"]["x"], L["boundary"]["y"]
|
||||
ring = build_border_cache({L["levelID"]: {**L, "borderKey": f"{bx},{by}"}})
|
||||
L["border"] = ring[f"{bx},{by}"]
|
||||
L["_mapSource"] = "boundary_ring"
|
||||
return
|
||||
maps = parse_level_prefab(p)
|
||||
if maps.get("ground"):
|
||||
L["ground"] = maps["ground"]
|
||||
if maps.get("border"):
|
||||
L["border"] = maps["border"]
|
||||
L["_mapSource"] = "unity_prefab"
|
||||
|
||||
|
||||
def strip_internal(L: dict) -> dict:
|
||||
out = {
|
||||
"levelID": L["levelID"],
|
||||
"boundary": L["boundary"],
|
||||
"spawns": L["spawns"],
|
||||
}
|
||||
if L.get("unityPrefab"):
|
||||
out["unityPrefab"] = L["unityPrefab"]
|
||||
if L.get("cocosPrefab"):
|
||||
out["cocosPrefab"] = L["cocosPrefab"]
|
||||
if L.get("ground"):
|
||||
out["ground"] = L["ground"]
|
||||
if L.get("border"):
|
||||
out["border"] = L["border"]
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--unity-root", required=True, help="Unity 项目根目录(含 Assets)")
|
||||
ap.add_argument("--output", required=True, help="输出 levels-database.json")
|
||||
ap.add_argument("--limit", type=int, default=0, help="仅导出前 N 个关卡(调试用)")
|
||||
ap.add_argument("--skip-prefab-maps", action="store_true", help="不解析 Unity prefab 瓦片(快,地图由 Cocos 预制体承担)")
|
||||
args = ap.parse_args()
|
||||
|
||||
unity = Path(args.unity_root)
|
||||
core = unity / "Assets/Scripts/Core"
|
||||
prefab_dir = unity / "Assets/Prefabs/Level"
|
||||
if not core.is_dir():
|
||||
print(f"Core dir not found: {core}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Parsing Levels*.cs …")
|
||||
levels = parse_all_levels_cs(core)
|
||||
ids = sorted(levels.keys())
|
||||
if args.limit > 0:
|
||||
ids = ids[: args.limit]
|
||||
levels = {k: levels[k] for k in ids}
|
||||
print(f"Level definitions: {len(levels)}")
|
||||
|
||||
from_prefab = 0
|
||||
from_ring = 0
|
||||
if args.skip_prefab_maps:
|
||||
print("Skipping prefab Tilemaps (--skip-prefab-maps)")
|
||||
else:
|
||||
print("Merging prefab Tilemaps …")
|
||||
for i, lid in enumerate(ids):
|
||||
if (i + 1) % 200 == 0:
|
||||
print(f" … {i + 1}/{len(ids)}")
|
||||
merge_prefab_maps(levels[lid], prefab_dir)
|
||||
if levels[lid].get("_mapSource") == "unity_prefab":
|
||||
from_prefab += 1
|
||||
else:
|
||||
from_ring += 1
|
||||
|
||||
payload = {
|
||||
"version": 1,
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"source": "Unity Levels*.cs + Assets/Prefabs/Level/*.prefab",
|
||||
"stats": {
|
||||
"total": len(levels),
|
||||
"withPrefabTilemap": from_prefab,
|
||||
"withBoundaryRing": from_ring,
|
||||
},
|
||||
"levelIdBase": LEVEL_ID_BASE,
|
||||
"levels": {str(levels[lid]["levelID"]): strip_internal(levels[lid]) for lid in ids},
|
||||
}
|
||||
|
||||
out = Path(args.output)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"Wrote {out} ({out.stat().st_size // 1024} KB)")
|
||||
print(f" prefab tilemap: {from_prefab}, fallback ring: {from_ring}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user