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,282 @@
/**
* 关卡数据库 — 由 Cocos 工程导出level-prefabs LevelMapData + 编辑器 spawns
*
* - 编辑器 ▶ 预览:从 assets/level-data/ 加载(不在 resources bundlefetch 不可用)
* - 主站 / Web 运行fetch /unity/levels-database.json
*/
import { assetManager, JsonAsset } from 'cc';
import { PREVIEW } from 'cc/env';
import { LevelConfig } from './LevelTypes';
import { LEVEL_ID_BASE } from './LevelIds';
/** assets/level-data/levels-database.json与 .meta 一致,勿打进 resources */
const EDITOR_DB_UUID = '114420ca-c8e3-4204-a040-18282ef5e964';
export interface LevelDatabaseFile {
version: number;
generatedAt?: string;
source?: string;
levelIdBase?: number;
stats?: { total: number; withPrefabTilemap?: number; withBoundaryRing?: number };
levels: Record<string, LevelConfig>;
}
declare global {
interface Window {
/** 主站 scratch-gui 注入:/unity/levels-database.json */
__tfrhLevelsDatabaseUrl?: string;
/** loader 预注入的 JSON可选 */
__tfrhLevelsDatabaseJson?: LevelDatabaseFile;
}
}
let fileCache: LevelDatabaseFile | null = null;
let levelsMap: Record<number, LevelConfig> = {};
let sortedIds: number[] = [];
let loadPromise: Promise<void> | null = null;
function rebuildIndex() {
sortedIds = Object.keys(levelsMap)
.map((k) => parseInt(k, 10))
.filter((n) => !Number.isNaN(n))
.sort((a, b) => a - b);
}
function ingestFile(data: LevelDatabaseFile) {
fileCache = data;
levelsMap = {};
for (const [k, cfg] of Object.entries(data.levels ?? {})) {
const id = parseInt(k, 10);
if (!Number.isNaN(id)) {
levelsMap[id] = { ...cfg, levelID: id };
}
}
rebuildIndex();
}
function resolveRemoteUrl(): string | null {
if (typeof window === 'undefined') return null;
const url = window.__tfrhLevelsDatabaseUrl;
return url?.trim() || null;
}
function validateIngested(): void {
const total = sortedIds.length;
const minId = sortedIds[0] ?? 0;
if (total < 100 || minId < LEVEL_ID_BASE) {
throw new Error(
`关卡库过旧 (${total} 关),请用主站导出的 levels-database.json`
+ '运行 bash tools/sync-level-db.sh 后重新 package-for-project',
);
}
}
function canUseHttpFetch(): boolean {
if (typeof window === 'undefined') return false;
const proto = window.location.protocol;
return proto === 'http:' || proto === 'https:';
}
function fetchCandidates(): string[] {
const out: string[] = [];
const configured = resolveRemoteUrl();
if (configured && /^https?:\/\//i.test(configured)) {
out.push(configured);
}
if (canUseHttpFetch()) {
out.push(new URL('levels-database.json', window.location.href).href);
out.push(new URL('/unity/levels-database.json', window.location.origin).href);
}
return [...new Set(out)];
}
function loadFromRemote(url: string): Promise<void> {
const abs = /^https?:\/\//i.test(url)
? url
: new URL(url, typeof window !== 'undefined' ? window.location.href : 'http://localhost/').href;
return fetch(abs, { credentials: 'same-origin' })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status} for ${abs}`);
return res.json() as Promise<LevelDatabaseFile>;
})
.then((json) => {
ingestFile(json);
validateIngested();
console.log(
`[LevelDatabase] 已加载 ${sortedIds.length} 关 (`
+ `${sortedIds[0] ?? '?'}${sortedIds[sortedIds.length - 1] ?? '?'})`,
abs,
);
});
}
async function loadFromNetwork(): Promise<void> {
const candidates = fetchCandidates();
if (!candidates.length) {
throw new Error('[LevelDatabase] 当前环境无法 fetch且无预注入关卡库');
}
const errors: unknown[] = [];
for (const url of candidates) {
try {
await loadFromRemote(url);
return;
} catch (e) {
errors.push(e);
}
}
const detail = errors.map((e) => String(e)).join('; ');
throw new Error(
`[LevelDatabase] 无法加载关卡库,已尝试: ${candidates.join(', ')}; ${detail}`,
);
}
/** 编辑器预览packages:// 协议下用 assetManager 读工程内 JSON不进 resources bundle */
function loadFromEditorAsset(): Promise<void> {
return new Promise((resolve, reject) => {
assetManager.loadAny({ uuid: EDITOR_DB_UUID }, (err: Error | null, asset: JsonAsset) => {
if (err || !asset?.json) {
reject(err ?? new Error('assets/level-data/levels-database.json 未找到'));
return;
}
ingestFile(asset.json as LevelDatabaseFile);
validateIngested();
console.log(
`[LevelDatabase] 已加载编辑器关卡库 ${sortedIds.length} 关 (`
+ `${sortedIds[0] ?? '?'}${sortedIds[sortedIds.length - 1] ?? '?'})`,
);
resolve();
});
});
}
async function waitForInjection(maxMs = 2500): Promise<LevelDatabaseFile | null> {
if (typeof window === 'undefined') return null;
const step = 50;
const tries = Math.ceil(maxMs / step);
for (let i = 0; i < tries; i++) {
const json = window.__tfrhLevelsDatabaseJson;
if (json) return json;
await new Promise((r) => setTimeout(r, step));
}
return null;
}
/** 异步加载AppBootstrap 启动时调用) */
export function loadLevelDatabase(): Promise<void> {
if (fileCache) return Promise.resolve();
if (loadPromise) return loadPromise;
loadPromise = (async () => {
try {
if (typeof window !== 'undefined' && window.__tfrhLevelsDatabaseJson) {
ingestFile(window.__tfrhLevelsDatabaseJson);
validateIngested();
console.log(`[LevelDatabase] 已注入 ${sortedIds.length}`);
return;
}
const injected = PREVIEW ? await waitForInjection(500) : null;
if (injected) {
ingestFile(injected);
validateIngested();
console.log(`[LevelDatabase] 已注入 ${sortedIds.length}`);
return;
}
if (!canUseHttpFetch()) {
await loadFromEditorAsset();
return;
}
if (PREVIEW) {
try {
await loadFromEditorAsset();
return;
} catch (e) {
console.warn('[LevelDatabase] 编辑器资源加载失败,尝试 fetch', e);
}
}
await loadFromNetwork();
} catch (e) {
loadPromise = null;
throw e;
}
})();
return loadPromise;
}
export function isLevelDatabaseReady(): boolean {
return fileCache !== null;
}
// --- 查 ---
export function getLevelConfig(levelID: number): LevelConfig | null {
return levelsMap[levelID] ?? null;
}
/** 是否在 Cocos 导出的关卡库中 */
export function hasLevel(levelID: number): boolean {
return levelID in levelsMap;
}
export function getLevelIds(): number[] {
return [...sortedIds];
}
export function getLevelCount(): number {
return sortedIds.length;
}
export const MIN_LEVEL_ID = (): number => sortedIds[0] ?? LEVEL_ID_BASE;
export const MAX_LEVEL_ID = (): number => sortedIds[sortedIds.length - 1] ?? LEVEL_ID_BASE;
export function nextLevelId(cur: number): number {
const i = sortedIds.indexOf(cur);
if (i < 0) return sortedIds[0] ?? cur;
return sortedIds[(i + 1) % sortedIds.length];
}
export function prevLevelId(cur: number): number {
const i = sortedIds.indexOf(cur);
if (i < 0) return sortedIds[0] ?? cur;
return sortedIds[(i - 1 + sortedIds.length) % sortedIds.length];
}
// --- 增改(运行时 / 编辑器脚本) ---
export function setLevel(config: LevelConfig): void {
levelsMap[config.levelID] = { ...config };
if (fileCache) {
fileCache.levels[String(config.levelID)] = levelsMap[config.levelID];
}
rebuildIndex();
}
export function addLevel(config: LevelConfig): void {
setLevel(config);
}
/** 删 */
export function removeLevel(levelID: number): boolean {
if (!(levelID in levelsMap)) return false;
delete levelsMap[levelID];
if (fileCache?.levels) {
delete fileCache.levels[String(levelID)];
}
rebuildIndex();
return true;
}
/** 导出当前内存数据(可写回 JSON 文件) */
export function exportDatabaseJson(): string {
const payload: LevelDatabaseFile = fileCache ?? {
version: 1,
generatedAt: new Date().toISOString(),
source: 'runtime',
levels: {},
};
payload.levels = Object.fromEntries(
sortedIds.map((id) => [String(id), levelsMap[id]]),
);
payload.stats = { total: sortedIds.length };
return JSON.stringify(payload, null, 2);
}