Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.9 KiB
Python
190 lines
5.9 KiB
Python
#!/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()
|