Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
199 lines
7.0 KiB
Python
199 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
从 Cocos 工程导出 levels-database.json(权威数据源)。
|
||
|
||
地图 / 主题:level-prefabs/Level{id}.prefab 上的 LevelMapData(groundJson / 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()
|