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,155 @@
#!/usr/bin/env node
/**
* 将 build/assets/level-prefabs 拆为:
* - shell: config.json + index.js进 assets_all 首屏)
* - 每关一包: assets/level-prefabs/import/.../*.json
*
* 输出 levels-manifest.json 供 loader 按 levelId 按需下载。
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { formatBytes } = require('./package-optimize');
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
function hashFileMd5(filePath) {
return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
}
function zipDir(srcDir, outFile) {
const absOut = path.resolve(outFile);
mkdirp(path.dirname(absOut));
if (fs.existsSync(absOut)) fs.unlinkSync(absOut);
execSync(`cd "${path.resolve(srcDir)}" && zip -0 -q -r "${absOut}" .`, { stdio: 'pipe' });
}
function walkJsonFiles(dir, out = []) {
if (!fs.existsSync(dir)) return out;
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walkJsonFiles(p, out);
else if (ent.name.endsWith('.json')) out.push(p);
}
return out;
}
/** 扫描 import/*.json从预制体名 Level{id} 建立 levelId → 相对路径 */
function indexImportFiles(levelPrefabsDir) {
const importRoot = path.join(levelPrefabsDir, 'import');
const byLevelId = new Map();
for (const abs of walkJsonFiles(importRoot)) {
const rel = path.relative(levelPrefabsDir, abs).split(path.sep).join('/');
const txt = fs.readFileSync(abs, 'utf8');
const m = /"Level(\d+)"/.exec(txt);
if (!m) continue;
const levelId = m[1];
if (byLevelId.has(levelId)) {
console.warn(`>>> 警告: Level${levelId} 重复 import保留 ${byLevelId.get(levelId)}`);
continue;
}
byLevelId.set(levelId, rel);
}
return byLevelId;
}
/** 从 config.json 校验 path ↔ uuid */
function readConfigLevels(levelPrefabsDir) {
const cfg = JSON.parse(fs.readFileSync(path.join(levelPrefabsDir, 'config.json'), 'utf8'));
const out = new Map();
for (const [idx, entry] of Object.entries(cfg.paths || {})) {
const m = /^level-prefabs\/Level(\d+)$/.exec(entry[0]);
if (!m) continue;
out.set(m[1], {
path: entry[0],
pathIndex: idx,
uuid: (cfg.uuids || [])[+idx],
});
}
return out;
}
/**
* @param {string} levelPrefabsDir Cocos 构建产物 assets/level-prefabs
* @param {string} webglDir 输出 WebGL/*.bundle
* @param {string} manifestOutPath 输出 levels-manifest.json
*/
function splitLevelBundles(levelPrefabsDir, webglDir, manifestOutPath, packTmpDir) {
if (!fs.existsSync(path.join(levelPrefabsDir, 'config.json'))) {
throw new Error(`缺少 level-prefabs/config.json: ${levelPrefabsDir}`);
}
const configLevels = readConfigLevels(levelPrefabsDir);
const importByLevel = indexImportFiles(levelPrefabsDir);
mkdirp(webglDir);
const stageBase = packTmpDir || path.join(webglDir, '..', '.level-pack-tmp');
mkdirp(stageBase);
const manifest = {
version: 1,
shell: ['assets/level-prefabs/config.json', 'assets/level-prefabs/index.js'],
levels: {},
};
let packed = 0;
let totalBytes = 0;
const missing = [];
for (const [levelId, meta] of configLevels) {
const importRel = importByLevel.get(levelId);
if (!importRel) {
missing.push(levelId);
continue;
}
const stageRoot = path.join(stageBase, levelId);
const assetRoot = path.join(stageRoot, 'assets', 'level-prefabs');
mkdirp(path.dirname(path.join(assetRoot, importRel)));
fs.copyFileSync(
path.join(levelPrefabsDir, importRel),
path.join(assetRoot, importRel),
);
const zipTmp = path.join(stageBase, `${levelId}.zip`);
zipDir(stageRoot, zipTmp);
const hash = hashFileMd5(zipTmp);
const bundleName = `defaultlocalgroup_level_${levelId}_${hash}.bundle`;
fs.copyFileSync(zipTmp, path.join(webglDir, bundleName));
const size = fs.statSync(zipTmp).size;
totalBytes += size;
manifest.levels[levelId] = {
bundle: bundleName,
path: meta.path,
uuid: meta.uuid,
files: [`assets/level-prefabs/${importRel}`],
bytes: size,
};
packed += 1;
fs.rmSync(stageRoot, { recursive: true, force: true });
fs.unlinkSync(zipTmp);
}
fs.rmSync(stageBase, { recursive: true, force: true });
fs.writeFileSync(manifestOutPath, JSON.stringify(manifest), 'utf8');
console.log(`>>> 关卡分包: ${packed} 关, 合计 ${formatBytes(totalBytes)}, 均 ${formatBytes(Math.round(totalBytes / Math.max(packed, 1)))}/关`);
if (missing.length) {
console.warn(`>>> 警告: ${missing.length} 关在 config 中无 import 文件 (例: ${missing.slice(0, 5).join(', ')})`);
}
return { manifest, packed, missing, totalBytes };
}
module.exports = {
splitLevelBundles,
indexImportFiles,
readConfigLevels,
};
if (require.main === module) {
const levelDir = path.resolve(process.argv[2]);
const webgl = path.resolve(process.argv[3]);
const manifest = path.resolve(process.argv[4] || path.join(path.dirname(webgl), 'levels-manifest.json'));
splitLevelBundles(levelDir, webgl, manifest);
}