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:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

189
tools/export_all_levels.py Normal file
View 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()