Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
5.1 KiB
TypeScript
147 lines
5.1 KiB
TypeScript
import { assetManager, Prefab, resources } from 'cc';
|
||
import { PREVIEW } from 'cc/env';
|
||
import { ensureResourcesBundle } from '../core/ResourcesBundle';
|
||
import {
|
||
getLevelPrefabUuid,
|
||
loadLevelPrefabUuidIndex,
|
||
parseLevelIdFromPrefabPath,
|
||
shouldTryEditorUuidLoad,
|
||
} from './LevelPrefabUuidIndex';
|
||
|
||
const LEVEL_PREFAB_BUNDLE = 'level-prefabs';
|
||
|
||
let cachedLevelBundle: assetManager.Bundle | null = null;
|
||
let levelBundlePromise: Promise<assetManager.Bundle> | null = null;
|
||
|
||
function loadLevelPrefabBundle(): Promise<assetManager.Bundle> {
|
||
if (cachedLevelBundle) return Promise.resolve(cachedLevelBundle);
|
||
if (levelBundlePromise) return levelBundlePromise;
|
||
levelBundlePromise = new Promise((resolve, reject) => {
|
||
assetManager.loadBundle(LEVEL_PREFAB_BUNDLE, (err, bundle) => {
|
||
levelBundlePromise = null;
|
||
if (err || !bundle) {
|
||
reject(err ?? new Error(`bundle "${LEVEL_PREFAB_BUNDLE}" unavailable`));
|
||
return;
|
||
}
|
||
cachedLevelBundle = bundle;
|
||
resolve(bundle);
|
||
});
|
||
});
|
||
return levelBundlePromise;
|
||
}
|
||
|
||
function loadPrefabFromBundle(bundle: assetManager.Bundle, path: string): Promise<Prefab> {
|
||
return new Promise((resolve, reject) => {
|
||
bundle.load(path, Prefab, (err, prefab) => {
|
||
if (!err && prefab) resolve(prefab);
|
||
else reject(err ?? new Error(`missing prefab: ${path}`));
|
||
});
|
||
});
|
||
}
|
||
|
||
function loadPrefabFromResources(path: string): Promise<Prefab> {
|
||
return new Promise((resolve, reject) => {
|
||
resources.load(path, Prefab, (err, prefab) => {
|
||
if (!err && prefab) resolve(prefab);
|
||
else reject(err ?? new Error(`missing prefab: ${path}`));
|
||
});
|
||
});
|
||
}
|
||
|
||
function loadPrefabByUuid(uuid: string): Promise<Prefab> {
|
||
return new Promise((resolve, reject) => {
|
||
assetManager.loadAny({ uuid }, (err: Error | null, asset: Prefab) => {
|
||
if (err || !asset) {
|
||
reject(err ?? new Error(`missing prefab uuid: ${uuid}`));
|
||
return;
|
||
}
|
||
resolve(asset);
|
||
});
|
||
});
|
||
}
|
||
|
||
/** bundle / resources 内可能的路径(子目录 level-prefabs/ 或 bundle 根下 LevelN) */
|
||
function prefabPathCandidates(path: string): string[] {
|
||
const trimmed = path.trim();
|
||
const base = trimmed.replace(/^level-prefabs\//, '');
|
||
// bundle config 路径键为 level-prefabs/LevelN,裸 LevelN 会触发 loadAny 解析错误
|
||
return [...new Set([trimmed, `level-prefabs/${base}`])];
|
||
}
|
||
|
||
async function loadFirstAvailable(
|
||
loader: (p: string) => Promise<Prefab>,
|
||
paths: string[],
|
||
): Promise<Prefab> {
|
||
let lastErr: unknown;
|
||
for (const p of paths) {
|
||
try {
|
||
return await loader(p);
|
||
} catch (e) {
|
||
lastErr = e;
|
||
}
|
||
}
|
||
throw lastErr ?? new Error(`missing prefab: ${paths[0]}`);
|
||
}
|
||
|
||
async function tryLoadFromEditorUuid(path: string): Promise<Prefab | null> {
|
||
if (!shouldTryEditorUuidLoad()) return null;
|
||
await loadLevelPrefabUuidIndex();
|
||
const levelId = parseLevelIdFromPrefabPath(path);
|
||
if (levelId === undefined) return null;
|
||
const uuid = getLevelPrefabUuid(levelId);
|
||
if (!uuid) return null;
|
||
try {
|
||
const prefab = await loadPrefabByUuid(uuid);
|
||
console.log(`[LevelPrefabLoader] 编辑器 uuid 加载 Level${levelId}`);
|
||
return prefab;
|
||
} catch (e) {
|
||
console.warn(`[LevelPrefabLoader] uuid 加载 Level${levelId} 失败`, e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** 进关前由 loader 注入:按 levelId 下载对应关卡包 */
|
||
async function ensureLevelPackForPath(path: string): Promise<void> {
|
||
const levelId = parseLevelIdFromPrefabPath(path);
|
||
if (levelId === undefined) return;
|
||
const hook = (globalThis as { __tfrhEnsureLevelPack?: (id: number) => Promise<void> }).__tfrhEnsureLevelPack;
|
||
if (typeof hook === 'function') await hook(levelId);
|
||
cachedLevelBundle = null;
|
||
levelBundlePromise = null;
|
||
}
|
||
|
||
/** 优先从 level-prefabs 分包加载;编辑器预览可回退 uuid / resources */
|
||
export async function loadLevelPrefab(path: string): Promise<Prefab> {
|
||
await ensureResourcesBundle();
|
||
await ensureLevelPackForPath(path);
|
||
const candidates = prefabPathCandidates(path);
|
||
|
||
if (PREVIEW) {
|
||
const byUuid = await tryLoadFromEditorUuid(path);
|
||
if (byUuid?.isValid) return byUuid;
|
||
}
|
||
|
||
let bundleErr: unknown = null;
|
||
try {
|
||
const bundle = await loadLevelPrefabBundle();
|
||
return await loadFirstAvailable((p) => loadPrefabFromBundle(bundle, p), candidates);
|
||
} catch (err) {
|
||
bundleErr = err;
|
||
console.warn('[LevelPrefabLoader] level-prefabs bundle 加载失败', err);
|
||
}
|
||
|
||
if (!PREVIEW) {
|
||
const byUuid = await tryLoadFromEditorUuid(path);
|
||
if (byUuid?.isValid) return byUuid;
|
||
}
|
||
|
||
console.error(
|
||
'[LevelPrefabLoader] 关卡预制体未找到。请确认 bundle-level-prefabs 已标记为 Asset Bundle '
|
||
+ `"${LEVEL_PREFAB_BUNDLE}",并运行: python3 tools/bake_cocos_level_prefabs.py`,
|
||
bundleErr,
|
||
);
|
||
throw bundleErr instanceof Error
|
||
? bundleErr
|
||
: new Error(`missing prefab: ${path}`);
|
||
}
|