/** * 关卡数据库 — 由 Cocos 工程导出(level-prefabs LevelMapData + 编辑器 spawns) * * - 编辑器 ▶ 预览:从 assets/level-data/ 加载(不在 resources bundle,fetch 不可用) * - 主站 / 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; } declare global { interface Window { /** 主站 scratch-gui 注入:/unity/levels-database.json */ __tfrhLevelsDatabaseUrl?: string; /** loader 预注入的 JSON(可选) */ __tfrhLevelsDatabaseJson?: LevelDatabaseFile; } } let fileCache: LevelDatabaseFile | null = null; let levelsMap: Record = {}; let sortedIds: number[] = []; let loadPromise: Promise | 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 { 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; }) .then((json) => { ingestFile(json); validateIngested(); console.log( `[LevelDatabase] 已加载 ${sortedIds.length} 关 (` + `${sortedIds[0] ?? '?'}–${sortedIds[sortedIds.length - 1] ?? '?'})`, abs, ); }); } async function loadFromNetwork(): Promise { 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 { 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 { 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 { 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); }