Initial Cocos Creator port of main-site Unity WebGL game.

Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 14:57:46 +08:00
commit cba5105908
88 changed files with 13798 additions and 0 deletions

92
tools/deploy-to-main.sh Normal file
View File

@@ -0,0 +1,92 @@
#!/bin/bash
# 将 Cocos Web 构建产物 + cocos-bridge 合并到主站静态目录
#
# 用法:
# ./tools/deploy-to-main.sh \
# --build build/web-desktop \
# --target "/path/to/主站静态目录/cocos"
#
# 示例(部署到 Unity 项目 Template 旁,便于本地对比):
# ./tools/deploy-to-main.sh \
# --build build/web-desktop \
# --target "/Users/liuyufei/tfrh/主站文件/主站/Template/cocos"
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_DIR=""
TARGET_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--build) BUILD_DIR="$2"; shift 2 ;;
--target) TARGET_DIR="$2"; shift 2 ;;
-h|--help)
echo "Usage: $0 --build <build/web-desktop> --target <主站/cocos目录>"
exit 0
;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
if [[ -z "$BUILD_DIR" || -z "$TARGET_DIR" ]]; then
echo "Error: 需要 --build 和 --target"
echo "Example:"
echo " $0 --build build/web-desktop --target /path/to/main-site/cocos"
exit 1
fi
BUILD_DIR="$(cd "$BUILD_DIR" 2>/dev/null && pwd || true)"
if [[ -z "$BUILD_DIR" || ! -f "$BUILD_DIR/index.html" ]]; then
echo "Error: 构建目录无效或缺少 index.html: $BUILD_DIR"
echo "请先在 Cocos Creator 中执行: 项目 → 构建发布 → Web Desktop"
exit 1
fi
mkdir -p "$TARGET_DIR"
echo ">>> 复制构建产物: $BUILD_DIR -> $TARGET_DIR"
rsync -a --delete \
--exclude '.DS_Store' \
"$BUILD_DIR/" "$TARGET_DIR/"
echo ">>> 复制 cocos-bridge.js"
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$TARGET_DIR/../cocos-bridge.js" 2>/dev/null || \
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$TARGET_DIR/cocos-bridge.js"
# 若 target 是 .../cocosbridge 放在上一级(与 index 同级)
if [[ -f "$TARGET_DIR/cocos-bridge.js" ]]; then
BRIDGE_PATH="$TARGET_DIR/cocos-bridge.js"
else
BRIDGE_PATH="$(dirname "$TARGET_DIR")/cocos-bridge.js"
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$BRIDGE_PATH"
fi
echo ">>> 生成联调页 cocos-demo.html"
DEMO="$PROJECT_DIR/web-template/main-site.html"
if [[ -f "$DEMO" ]]; then
cp "$DEMO" "$(dirname "$TARGET_DIR")/cocos-demo.html" 2>/dev/null || \
cp "$DEMO" "$TARGET_DIR/cocos-demo.html"
fi
cat <<EOF
部署完成。
目录结构建议:
$(dirname "$TARGET_DIR")/
cocos-bridge.js ← JS 回调 + unityInstance 兼容
cocos/ ← 构建产物(本脚本输出)
index.html
src/ ...
cocos-demo.html ← 可选联调页
主站 index.html 需:
1. 在 <head> 或游戏脚本前引入: <script src="cocos-bridge.js"></script>
2. 用 cocos/index.html 中的 script 标签替换原 Unity Build/*.js 段
3. 保留 processData / externalResult 等全局函数
本地预览(需先 cd 到含 cocos 的目录):
npx --yes serve "$(dirname "$TARGET_DIR")" -p 8080
EOF

24
tools/export_all.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# 批量导出 Unity Levels*.cs → Cocos generated ts
set -e
UNITY_ROOT="${1:-/Users/liuyufei/tfrh/主站文件/主站}"
OUT_DIR="$(dirname "$0")/../assets/scripts/level"
PY="$(dirname "$0")/export_unity_levels.py"
mkdir -p "$OUT_DIR"
export_one() {
local file="$1"
local name="$2"
echo "Exporting $file -> levels-${name}.generated.ts"
python3 "$PY" \
--input "$UNITY_ROOT/Assets/Scripts/Core/${file}" \
--output "$OUT_DIR/levels-${name}.generated.ts"
}
export_one "Levels600.cs" "600"
# 可按需取消注释:
# export_one "Levels1000.cs" "1000"
# export_one "Levels10000.cs" "10000"
echo "Done. Update LevelRegistry.ts to import new LEVELS_* maps."

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
从 Unity Levels*.cs 导出 Cocos LevelRegistry 数据。
用法:
python3 tools/export_unity_levels.py \
--input "/path/to/Levels600.cs" \
--output assets/scripts/level/levels-600.generated.ts \
--border-cache assets/scripts/level/border-cache.json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
DIR = {
"Direction.North": "Direction.North",
"Direction.East": "Direction.East",
"Direction.South": "Direction.South",
"Direction.West": "Direction.West",
}
LEVEL_RE = re.compile(
r"\{(\d+),new Level\(\)\{LevelID\s*=\s*\d+,spawns\s*=\s*new List<Spawn>\(\)\{",
re.MULTILINE,
)
SPAWN_RE = re.compile(
r"new Spawn\(\)\{([^}]+(?:\{[^}]*\})?)\}",
re.MULTILINE,
)
POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),0\)")
PATH_RE = re.compile(r'path\s*=\s*"?([^",\s]+)"?')
PDIR_RE = re.compile(r"playerDirection\s*=\s*(Direction\.\w+)")
VDIR_RE = re.compile(r"vehicleDirection\s*=\s*(Direction\.\w+)")
BOUND_RE = re.compile(r"boundary\s*=\s*new Vector3Int\((\d+),(\d+),0\)")
def kind_from_path(path: str) -> str:
p = path.lower()
if "player" in p and "multplay" not in p:
return "player"
if "vehicle" in p:
return "vehicle"
if "enemy" in p:
return "enemy"
if "nprop" in p:
return "prop_decor"
if "prop" in p:
return "prop"
return "prop_decor"
def parse_spawns(block: str) -> list[dict]:
spawns = []
for m in SPAWN_RE.finditer(block):
body = m.group(1)
pm = POS_RE.search(body)
if not pm:
continue
path_m = PATH_RE.search(body)
path = path_m.group(1) if path_m else ""
item: dict = {
"x": int(pm.group(1)),
"y": int(pm.group(2)),
"kind": kind_from_path(path),
}
pdir = PDIR_RE.search(body)
vdir = VDIR_RE.search(body)
if pdir:
item["playerDirection"] = pdir.group(1)
if vdir:
item["vehicleDirection"] = vdir.group(1)
spawns.append(item)
return spawns
def ring_border_key(bx: int, by: int) -> str:
return f"{bx},{by}"
def parse_file(text: str) -> dict[int, dict]:
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)
chunk = text[start:end]
bound = BOUND_RE.search(chunk)
bx, by = (10, 10)
if bound:
bx, by = int(bound.group(1)), int(bound.group(2))
spawns = parse_spawns(chunk)
levels[lid] = {
"levelID": lid,
"boundary": {"x": bx, "y": by},
"borderKey": ring_border_key(bx, by),
"spawns": spawns,
}
return levels
def spawn_to_ts(s: dict, indent: str) -> str:
parts = [
f"x: {s['x']}",
f"y: {s['y']}",
f"kind: '{s['kind']}'",
]
if "playerDirection" in s:
parts.append(f"playerDirection: {s['playerDirection']}")
if "vehicleDirection" in s:
parts.append(f"vehicleDirection: {s['vehicleDirection']}")
return indent + "{ " + ", ".join(parts) + " }"
def emit_ts(levels: dict[int, dict], border_cache: dict[str, dict]) -> str:
lines = [
"/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */",
"import { Direction } from '../core/Define';",
"import { LevelConfig, SpawnConfig } from './LevelTypes';",
"",
"const BORDER_CACHE: Record<string, Record<string, boolean>> = " + json.dumps(border_cache, separators=(',', ':')) + ";",
"",
"function withBorder(key: string, cfg: Omit<LevelConfig, 'border'>): LevelConfig {",
" return { ...cfg, border: BORDER_CACHE[key] };",
"}",
"",
"export const LEVELS_600: Record<number, LevelConfig> = {",
]
for lid in sorted(levels.keys()):
L = levels[lid]
lines.append(f" {lid}: withBorder('{L['borderKey']}', {{")
lines.append(f" levelID: {lid},")
lines.append(f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }},")
lines.append(" spawns: [")
for s in L["spawns"]:
lines.append(spawn_to_ts(s, " ") + ",")
lines.append(" ],")
lines.append(" }),")
lines.append("};")
lines.append("")
return "\n".join(lines)
def build_border_cache(levels: dict[int, dict]) -> dict[str, dict]:
cache: dict[str, dict] = {}
for L in levels.values():
key = L["borderKey"]
if key in cache:
continue
bx, by = L["boundary"]["x"], L["boundary"]["y"]
border: dict[str, bool] = {}
for x in range(-bx, bx + 1):
for y in range(-by, by + 1):
if abs(x) == bx or abs(y) == by:
border[f"{x},{y}"] = True
cache[key] = border
return cache
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--input", required=True, help="Unity Levels*.cs path")
ap.add_argument("--output", required=True, help="Output .ts path")
ap.add_argument("--border-cache", default="", help="Optional border cache json")
args = ap.parse_args()
text = Path(args.input).read_text(encoding="utf-8")
levels = parse_file(text)
if not levels:
print("No levels parsed", file=sys.stderr)
sys.exit(1)
border_cache = build_border_cache(levels)
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(emit_ts(levels, border_cache), encoding="utf-8")
if args.border_cache:
Path(args.border_cache).write_text(json.dumps(border_cache, indent=2), encoding="utf-8")
print(f"Exported {len(levels)} levels -> {out}")
if __name__ == "__main__":
main()

55
tools/patch-main-index.js Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* 根据 Cocos 构建出的 index.html生成可嵌入主站的片段说明
*
* node tools/patch-main-index.js build/web-desktop
*/
const fs = require('fs');
const path = require('path');
const buildDir = process.argv[2];
if (!buildDir) {
console.error('Usage: node patch-main-index.js <build/web-desktop>');
process.exit(1);
}
const indexPath = path.join(buildDir, 'index.html');
if (!fs.existsSync(indexPath)) {
console.error('Not found:', indexPath);
process.exit(1);
}
const html = fs.readFileSync(indexPath, 'utf8');
// 提取所有 script 标签Cocos 启动链)
const scripts = [];
const re = /<script[^>]*src=["']([^"']+)["'][^>]*><\/script>/gi;
let m;
while ((m = re.exec(html)) !== null) {
scripts.push(m[1]);
}
// 提取内联 scriptSystem.import 等)
const inline = html.match(/<script>([\s\S]*?)<\/script>/gi) || [];
console.log('=== 复制到主站 index.html替换 Unity Build 段)===\n');
console.log('<!-- ① 主站公共:放在 head 或 body 开头 -->');
console.log('<script src="cocos-bridge.js"></script>\n');
console.log('<!-- ② Cocos 启动脚本(路径相对主站页面,若游戏在 /cocos/ 子目录则加前缀 cocos/ -->');
for (const src of scripts) {
const rel = src.startsWith('http') ? src : `cocos/${src.replace(/^\.\//, '')}`;
console.log(`<script src="${rel}" charset="utf-8"></script>`);
}
console.log('\n<!-- ③ 容器(若主站已有 #GameDiv 可保留原 id -->');
const canvasMatch = html.match(/<canvas[^>]*id=["']([^"']+)["'][^>]*>/i);
const divMatch = html.match(/id=["'](GameDiv|Cocos3dGameContainer)["']/i);
console.log('<!-- Cocos 构建页中的 canvas id:', canvasMatch ? canvasMatch[1] : 'GameCanvas', '-->');
if (inline.length) {
console.log('\n<!-- ④ 内联启动(从 build/index.html 复制以下 block -->');
inline.forEach((block) => console.log(block));
}
console.log('\n=== 删除原 Unity 段 ===');
console.log('- Build/build.loader.js');
console.log('- createUnityInstance(...)');

View File

@@ -0,0 +1,30 @@
# Unity 关卡迁移到 Cocos
Unity 原项目约 1.1 万个 `Level*.prefab`Cocos 版使用 **JSON/TS 配置** 代替 Tilemap Prefab。
## 配置格式
```typescript
registerLevel({
levelID: 601,
boundary: { x: 20, y: 20 },
border: { "10,0": true, ... }, // 可选
ground: { "0,0": "Baseblock", "1,0": "JumpBlock" }, // 可选
spawns: [
{ x: 0, y: 0, kind: 'player', playerDirection: 0 },
{ x: 3, y: 0, kind: 'prop' },
],
});
```
## 批量迁移步骤
1.`Assets/Scripts/Core/Levels*.cs` 提取 `LevelID``spawns``boundary`
2. 若有 Tiled 导出或 Tilemap 脚本,生成 `ground` / `border` 稀疏表
3. 写入 `assets/scripts/level/LevelRegistry.ts` 或独立 `assets/resources/levels/{id}.json`
4.`LevelRegistry.ts``getLevelConfig` 中合并 JSON 加载
## 资源迁移
- 角色/载具贴图:放入 `assets/resources/textures/`,替换 `GameManager.attachVisual` 为 Sprite 渲染
- 音频:放入 `assets/resources/audio/`