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

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
从 Cocos 工程导出 levels-database.json权威数据源
地图 / 主题level-prefabs/Level{id}.prefab 上的 LevelMapDatagroundJson / borderJson / theme
实体 spawn / boundary保留 assets/level-data/levels-database.json 中已有条目(关卡编辑器维护)
Unity 主站仅作 ID 对照参考,不参与本导出。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from level_id import LEVEL_ID_BASE, level_db_path, normalize_level_id, prefab_resource_path
LEVEL_MAP_TYPE = "d4e5fanuMlNDh8qO0xdbn+K"
PREFAB_NAME_RE = re.compile(r"^Level(\d+)\.prefab$", re.I)
def parse_prefab_map_data(prefab_path: Path) -> dict | None:
try:
objs = json.loads(prefab_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
print(f"warn: skip {prefab_path.name}: {e}", file=sys.stderr)
return None
if not isinstance(objs, list):
return None
for obj in objs:
if not isinstance(obj, dict):
continue
if obj.get("__type__") != LEVEL_MAP_TYPE and "groundJson" not in obj:
continue
if "groundJson" not in obj and "levelID" not in obj:
continue
level_id = int(obj.get("levelID") or 0)
ground = {}
border = {}
try:
ground = json.loads(obj.get("groundJson") or "{}")
except json.JSONDecodeError:
pass
try:
border = json.loads(obj.get("borderJson") or "{}")
except json.JSONDecodeError:
pass
theme = str(obj.get("theme") or "").strip() or None
return {
"levelID": level_id,
"ground": ground if isinstance(ground, dict) else {},
"border": border if isinstance(border, dict) else {},
"theme": theme,
}
return None
def load_existing_db(path: Path) -> dict:
if not path.is_file():
return {"levels": {}}
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {"levels": {}}
def merge_entry(level_id: int, map_data: dict | None, prev: dict | None) -> dict:
ext_id = normalize_level_id(level_id)
out: dict = {
"levelID": ext_id,
"boundary": (prev or {}).get("boundary") or {"x": 20, "y": 20},
"spawns": list((prev or {}).get("spawns") or []),
"cocosPrefab": prefab_resource_path(ext_id),
}
if prev and prev.get("unityPrefab"):
out["unityPrefab"] = prev["unityPrefab"]
if map_data:
if map_data.get("ground"):
out["ground"] = map_data["ground"]
if map_data.get("border"):
out["border"] = map_data["border"]
if map_data.get("theme"):
out["theme"] = map_data["theme"]
if map_data.get("levelID") and map_data["levelID"] > 0:
out["levelID"] = normalize_level_id(map_data["levelID"])
elif prev:
for k in ("ground", "border", "theme", "entityTextures"):
if prev.get(k) is not None:
out[k] = prev[k]
return out
def export_db(project: Path, output: Path, prefab_dir: Path, level_id: int | None = None) -> dict:
existing = load_existing_db(output)
prev_levels = existing.get("levels") or {}
levels: dict[str, dict] = dict(prev_levels)
prefab_files = sorted(prefab_dir.glob("Level*.prefab"), key=lambda p: p.name)
if level_id is not None:
target = normalize_level_id(level_id)
prefab_files = [
p for p in prefab_files
if (m := PREFAB_NAME_RE.match(p.name)) and int(m.group(1)) == target
]
if not prefab_files and level_id is None:
print(f"error: no prefabs in {prefab_dir}", file=sys.stderr)
sys.exit(1)
if level_id is not None and not prefab_files:
print(f"error: prefab Level{normalize_level_id(level_id)} not found in {prefab_dir}", file=sys.stderr)
sys.exit(1)
for pf in prefab_files:
m = PREFAB_NAME_RE.match(pf.name)
if not m:
continue
lid = int(m.group(1))
map_data = parse_prefab_map_data(pf)
prev = prev_levels.get(str(lid)) or prev_levels.get(str(normalize_level_id(lid)))
levels[str(normalize_level_id(lid))] = merge_entry(lid, map_data, prev)
if level_id is None:
# 保留仅有 DB 条目、尚无预制体的关卡(编辑器先写 spawns
for key, prev in prev_levels.items():
if key not in levels:
try:
lid = int(key)
except ValueError:
continue
levels[key] = merge_entry(lid, None, prev)
prev_stats = existing.get("stats") or {}
total_with_maps = sum(
1 for v in levels.values() if (v.get("ground") or v.get("border"))
)
return {
"version": 2,
"generatedAt": datetime.now(timezone.utc).isoformat(),
"source": "Cocos level-prefabs LevelMapData + levels-database spawns",
"levelIdBase": LEVEL_ID_BASE,
"stats": {
"total": len(levels),
"withPrefabTilemap": total_with_maps,
"withBoundaryRing": prev_stats.get("withBoundaryRing", 0),
},
"levels": dict(sorted(levels.items(), key=lambda kv: int(kv[0]))),
}
def resolve_prefab_dir(project: Path) -> Path:
"""迁移后预制体在 bundle-level-prefabs/level-prefabs否则仍在 resources 下。"""
bundle = project / "assets/bundle-level-prefabs/level-prefabs"
legacy = project / "assets/resources/level-prefabs"
if bundle.is_dir():
return bundle
return legacy
def main():
ap = argparse.ArgumentParser(description="从 Cocos 预制体导出 levels-database.json")
ap.add_argument(
"--project",
default=str(Path(__file__).resolve().parent.parent),
help="Cocos 工程根目录",
)
ap.add_argument(
"--output",
default="",
help="输出 JSON默认 assets/level-data/levels-database.json",
)
ap.add_argument(
"--prefab-dir",
default="",
help="关卡预制体目录(默认自动检测 bundle-level-prefabs 或 resources",
)
ap.add_argument(
"--level-id",
type=int,
default=None,
help="仅导出/合并指定关卡(增量更新 levels-database.json",
)
args = ap.parse_args()
project = Path(args.project)
output = Path(args.output) if args.output else level_db_path(project)
prefab_dir = Path(args.prefab_dir) if args.prefab_dir else resolve_prefab_dir(project)
data = export_db(project, output, prefab_dir, args.level_id)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Exported {data['stats']['total']} levels -> {output}")
print(f" with prefab tilemap: {data['stats']['withPrefabTilemap']}")
if __name__ == "__main__":
main()