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,273 @@
#!/usr/bin/env node
/**
* 项目运行包:扁平结构,直接供 scratch-gui static/unity 本地加载。
*
* node tools/package-for-project.js <cocosBuildDir> <outDir> [options]
*
* 选项:
* --minify-db 压缩 levels-database.json默认开启
* --no-minify-db 保留格式化 JSON
* --brotli-db 额外生成 levels-database.json.br
* --preload-resources 启动时预加载 resources 包(默认关闭,按需加载)
* --preload-levels 启动时预加载 level-prefabs 包(默认关闭)
* --report 打印体积报告(默认开启)
* --no-report
*
* 输出结构:
* Build/mstest5.loader.js + 占位 .br
* index.js, application.js, cocos-bridge.js
* cocos-js/, src/, assets/
*/
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const {
minifyLevelsDatabase,
brotliCompressFile,
patchPreloadSettings,
printPackageReport,
formatBytes,
} = require('./package-optimize');
const argv = process.argv.slice(2);
const positional = [];
const flags = new Set();
for (const a of argv) {
if (a.startsWith('--')) flags.add(a);
else positional.push(a);
}
const buildDir = path.resolve(positional[0] || '');
const outDir = path.resolve(positional[1] || '');
const opts = {
minifyDb: !flags.has('--no-minify-db'),
brotliDb: flags.has('--brotli-db'),
preloadResources: flags.has('--preload-resources'),
preloadLevelPrefabs: flags.has('--preload-levels'),
report: !flags.has('--no-report'),
};
const PRODUCT = 'mstest5';
const UNITY_BASE = '/unity/';
const templateDir = path.join(__dirname, '../web-template');
if (!buildDir || !outDir) {
console.error('Usage: package-for-project.js <cocosBuildDir> <outDir> [--minify-db] [--brotli-db] ...');
process.exit(1);
}
function rmrf(p) {
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
}
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
function copyFile(s, d) { mkdirp(path.dirname(d)); fs.copyFileSync(s, d); }
function copyDir(s, d) {
if (!fs.existsSync(s)) return;
mkdirp(d);
for (const ent of fs.readdirSync(s, { withFileTypes: true })) {
const sp = path.join(s, ent.name);
const dp = path.join(d, ent.name);
if (ent.isDirectory()) copyDir(sp, dp);
else copyFile(sp, dp);
}
}
function patchText(file, fn) {
const t = fs.readFileSync(file, 'utf8');
fs.writeFileSync(file, fn(t), 'utf8');
}
function brotliStub(dst) {
fs.writeFileSync(dst, zlib.brotliCompressSync(Buffer.from([0])));
}
const required = ['index.js', 'application.js', 'src', 'cocos-js', 'assets'];
for (const r of required) {
if (!fs.existsSync(path.join(buildDir, r))) {
console.error('Missing in Cocos build:', r, 'at', buildDir);
process.exit(1);
}
}
console.log('>>> Cocos 构建:', buildDir);
console.log('>>> 输出(项目运行包):', outDir);
console.log('>>> 优化:', JSON.stringify({
minifyDb: opts.minifyDb,
brotliDb: opts.brotliDb,
preloadResources: opts.preloadResources,
preloadLevelPrefabs: opts.preloadLevelPrefabs,
}));
mkdirp(outDir);
for (const item of [
'Build', 'index.js', 'application.js', 'cocos-bridge.js',
'cocos-js', 'src', 'assets',
'StreamingAssets', 'TemplateData', 'index.html',
'levels-database.json', 'levels-database.json.br',
]) {
rmrf(path.join(outDir, item));
}
for (const item of ['index.js', 'application.js']) {
copyFile(path.join(buildDir, item), path.join(outDir, item));
}
copyDir(path.join(buildDir, 'src'), path.join(outDir, 'src'));
copyDir(path.join(buildDir, 'cocos-js'), path.join(outDir, 'cocos-js'));
copyDir(path.join(buildDir, 'assets'), path.join(outDir, 'assets'));
const EMBEDDED_RESOLUTION_PATCH =
'!(function(){v.resizeWithBrowserSize(!0);v.setDesignResolutionSize(G,H,C.FIXED_WIDTH)})()';
function patchMainIndexRuntime(t) {
return t
.replace(/return\[\]\.concat\(l\)\},entityFlipX/g, 'return Array.from(l)},entityFlipX')
.replace(/return\[\]\.concat\(n\)\},e\.ensureUILayerTree/g, 'return Array.from(n)},e.ensureUILayerTree')
.replace(/e\("applyEmbeddedDesignResolution",\(function\(\)\{[^}]+\}\)\)/g, 'e("applyEmbeddedDesignResolution",(function(){t.resizeWithBrowserSize(!0),t.setDesignResolutionSize(s,o,n.FIXED_WIDTH)}))')
.replace(/v\.setDesignResolutionSize\(G,H,C\.(?:SHOW_ALL|FIXED_WIDTH|FIXED_HEIGHT)\)/g, EMBEDDED_RESOLUTION_PATCH)
.replace(/setDesignResolutionSize\(G,H,C\.(?:SHOW_ALL|FIXED_WIDTH|FIXED_HEIGHT)\)/g, EMBEDDED_RESOLUTION_PATCH)
.replace(
/e\.syncMapDataComponent=function\(e,n\)\{var r,t,o=e\.getComponent\(p\);o&&\(o\.levelID=n\.levelID,o\.theme=T\(n\),o\.groundJson=JSON\.stringify\(null!=\(r=n\.ground\)\?r:\{\}\),o\.borderJson=JSON\.stringify\(null!=\(t=n\.border\)\?t:\{\}\)\)\}/,
'e.syncMapDataComponent=function(e,n){var r,t,o=e.getComponent(p);if(!o)return;o.levelID=n.levelID;var i=n.theme&&String(n.theme).trim();o.theme=i?T(n):o.theme||T(n);r=n.ground;r&&Object.keys(r).length&&(o.groundJson=JSON.stringify(r));t=n.border;t&&Object.keys(t).length&&(o.borderJson=JSON.stringify(t))}',
)
.replace(
/e\.prepare=function\(\)\{var e=r\(\(function\*\(e,n\)\{e\.name="Level_"\+n\.levelID,f\.purgeRuntimeGrids\(e\),this\.ensureUILayerTree\(e\);var r=T\(n\),t=m\(e,n\);/,
'e.prepare=function(){var e=r((function*(e,n){e.name="Level_"+n.levelID,f.purgeRuntimeGrids(e),this.ensureUILayerTree(e);var _md=e.getComponent(p);if(_md){try{var _g=JSON.parse(_md.groundJson||"{}");(!n.ground||!Object.keys(n.ground).length)&&Object.keys(_g).length&&(n.ground=_g);var _b=JSON.parse(_md.borderJson||"{}");(!n.border||!Object.keys(n.border).length)&&Object.keys(_b).length&&(n.border=_b)}catch(_e){}(!n.theme||!String(n.theme).trim())&&_md.theme&&(n.theme=_md.theme)}var r=T(n),t=m(e,n);',
)
.replace(
/function p\(e\)\{var r=e\.children\.filter\(\(function\(e\)\{return e\.isValid&&y\.test\(e\.name\)\}\)\);/,
'function p(e){if(!e||!e.isValid||!e.children)return;var r=e.children.filter((function(e){return e.isValid&&y.test(e.name)}));',
)
.replace(
/this\.applyMapDataFromConfig\(n\),yield J\.prepare\(o,n\);var s=n\.theme\|\|"silu";/,
'yield J.prepare(o,n),this.applyMapDataFromConfig(n),this.curConfig=n;var s=n.theme||"silu";',
)
.replace(
/c\.moveTowards\(i,e,this\.targetPosition,this\.moveSpeed\*t\)/g,
'c.moveTowards(i,e,this.targetPosition,this.moveSpeed*t*((null!=(S=globalThis.__tfrhGameSpeed)&&S>0)?S:1))',
)
.replace(
/if\(!\(t<=0\)\)if\(this\.frameTimer\+=e,!\(this\.frameTimer<t\)\)/,
'if(!(t<=0))if(this.frameTimer+=e*((null!=(z=globalThis.__tfrhGameSpeed)&&z>0)?z:1),!(this.frameTimer<t))',
)
.replace(
/a\.onPlaySpeedChange=function\(\)\{this\.speedIndex=\(this\.speedIndex\+1\)%l\.SPEEDS\.length,d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],this\.setPlaySpeedSprite\(this\.resolveHudTheme\(\)\)\}/,
'a.onPlaySpeedChange=function(){this.speedIndex=(this.speedIndex+1)%l.SPEEDS.length;var e=l.SPEEDS[this.speedIndex];d.globalGameTimeScale=e,globalThis.__tfrhGameSpeed=e,console.log("[UIMain] 倍速 x"+e),this.setPlaySpeedSprite(this.resolveHudTheme())}',
)
.replace(
/a\.onLoad=function\(\)\{d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],this\.buildUI\(\)/,
'a.onLoad=function(){d.globalGameTimeScale=l.SPEEDS[this.speedIndex],globalThis.__tfrhGameSpeed=l.SPEEDS[this.speedIndex],this.buildUI()',
)
.replace(
/a\.onRevertGame=function\(\)\{var e;this\.speedIndex=0,d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],null==\(e=w\.instance\)\|\|e\.resetLevel\(\)\}/,
'a.onRevertGame=function(){var e;this.speedIndex=0,d.globalGameTimeScale=l.SPEEDS[this.speedIndex],globalThis.__tfrhGameSpeed=l.SPEEDS[this.speedIndex],null==(e=w.instance)||e.resetLevel()}',
);
}
const mainIndex = path.join(outDir, 'assets', 'main', 'index.js');
if (fs.existsSync(mainIndex)) {
patchText(mainIndex, (t) => patchMainIndexRuntime(t));
}
copyFile(
path.join(templateDir, 'cocos-bridge.js'),
path.join(outDir, 'cocos-bridge.js'),
);
const levelsDbSrc = path.join(__dirname, '../assets/level-data/levels-database.json');
const levelsDbDst = path.join(outDir, 'levels-database.json');
if (fs.existsSync(levelsDbSrc)) {
if (opts.minifyDb) {
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
console.log(`>>> levels-database.json 压缩: ${formatBytes(before)}${formatBytes(after)}`);
} else {
copyFile(levelsDbSrc, levelsDbDst);
}
if (opts.brotliDb) {
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
console.log(`>>> levels-database.json.br: ${formatBytes(raw)}${formatBytes(br)}`);
}
console.log('>>> 已附带 levels-database.jsonCocos 关卡库)');
} else {
console.warn('>>> 警告: 未找到 levels-database.json请先运行 tools/sync-level-db.sh');
}
patchText(path.join(outDir, 'application.js'), (t) =>
t.replace(/this\.settingsPath\s*=\s*'[^']*'/, `this.settingsPath = '${UNITY_BASE}src/settings.json'`),
);
function patchSettingsForFrontend(settingsFile) {
patchText(settingsFile, (t) => {
const j = JSON.parse(t);
j.assets = j.assets || {};
j.assets.server = UNITY_BASE;
patchPreloadSettings(j, {
preloadResources: opts.preloadResources,
preloadLevelPrefabs: opts.preloadLevelPrefabs,
});
if (j.rendering) {
j.rendering.effectSettingsPath = `${UNITY_BASE}src/effect.bin`;
}
if (j.scripting && Array.isArray(j.scripting.scriptPackages)) {
j.scripting.scriptPackages = j.scripting.scriptPackages.map((p) => {
const rel = String(p).replace(/^\.\.\//, '').replace(/^\//, '');
return `${UNITY_BASE}${rel}`;
});
}
return JSON.stringify(j);
});
}
const settingsPath = path.join(outDir, 'src', 'settings.json');
if (fs.existsSync(settingsPath)) {
patchSettingsForFrontend(settingsPath);
const preload = JSON.parse(fs.readFileSync(settingsPath, 'utf8')).assets?.preloadBundles || [];
console.log('>>> 预加载 bundles:', preload.map((b) => b.bundle).join(', ') || '(none)');
}
const importMapPath = path.join(outDir, 'src', 'import-map.json');
if (fs.existsSync(importMapPath)) {
patchText(importMapPath, (t) => {
const j = JSON.parse(t);
j.imports = j.imports || {};
if (j.imports.cc) j.imports.cc = `${UNITY_BASE}cocos-js/cc.js`;
return JSON.stringify(j);
});
}
const buildOut = path.join(outDir, 'Build');
mkdirp(buildOut);
const loaderTpl = fs.readFileSync(path.join(templateDir, 'mstest5-cocos-loader.js'), 'utf8');
fs.writeFileSync(
path.join(buildOut, `${PRODUCT}.loader.js`),
loaderTpl.replace(/__PRODUCT_NAME__/g, PRODUCT),
'utf8',
);
for (const stub of ['data', 'framework.js', 'wasm']) {
brotliStub(path.join(buildOut, `${PRODUCT}.${stub}.br`));
}
if (opts.report) {
const preloadNote = [
'main',
opts.preloadResources ? 'resources' : null,
opts.preloadLevelPrefabs ? 'level-prefabs' : null,
].filter(Boolean).join(', ');
printPackageReport(outDir, { preloadNote });
const hasLevelBundle = fs.existsSync(path.join(outDir, 'assets', 'level-prefabs'));
if (!hasLevelBundle) {
console.log('\n>>> 提示: 尚未拆分 level-prefabs 分包。运行 migrate-level-prefab-bundle.sh 后重建可显著减小 resources 体积。');
}
}
console.log('\n>>> 项目运行包文件:');
for (const rel of [
'Build/mstest5.loader.js',
'index.js',
'application.js',
'cocos-bridge.js',
'cocos-js/',
'src/',
'assets/',
'levels-database.json',
]) {
console.log(' ', rel);
}
console.log('\n完成。flat 包与 CDN 不一致,生产请用: bash tools/package-for-project.sh');
console.log(' ~/tfrh/001code/001code-html/scratch-gui/static/unity/import-to-unity.sh', outDir);