Files
cocos/assets/scripts/level/LevelDatabase.ts
刘宇飞 d393302388 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>
2026-06-16 15:30:58 +08:00

283 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 关卡数据库 — 由 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);
}