Files
cocos/assets/scripts/level/LevelDisplay.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

189 lines
7.7 KiB
TypeScript

import { Node, UITransform, Layers, Sprite } from 'cc';
import { LevelConfig } from './LevelTypes';
import { CommonDefine } from '../core/Define';
import { VisualAssets, normalizeTheme } from '../visual/VisualAssets';
import { layoutLevelTiles, sortIsoTiles } from './TileLayout';
import { LevelTileLayout } from './LevelTileLayout';
import { LevelMapData } from './LevelMapData';
import { GridSnapHelper } from './GridSnapHelper';
import { getThemeBorderDecorKey } from '../theme/ThemeRegistry';
import { centerLevelRoot, syncTileNodesFromConfig } from './LevelTileSync';
import { mergeLevelConfigWithMapData } from './LevelConfigMerge';
const UI_LAYER = Layers.Enum.UI_2D;
function resolveLevelTheme(config: LevelConfig): string {
return normalizeTheme(config.theme || 'silu');
}
/** 关卡预制体在 Canvas 下正确显示:统一 UI 层、按 JSON 对齐格子、刷新贴图 */
export class LevelDisplay {
/** 每关使用 config.theme 独立贴图,与全局 UI 主题无关 */
static async prepare(levelRoot: Node, config: LevelConfig) {
levelRoot.name = `Level_${config.levelID}`;
GridSnapHelper.purgeRuntimeGrids(levelRoot);
this.ensureUILayerTree(levelRoot);
config = mergeLevelConfigWithMapData(config, levelRoot);
const theme = resolveLevelTheme(config);
const tileCount = syncTileNodesFromConfig(levelRoot, config);
layoutLevelTiles(levelRoot, config);
const tileNames = this.collectTileNames(config);
await VisualAssets.preloadLevelTiles(theme, tileNames);
await this.refreshTileSprites(levelRoot, config, theme);
layoutLevelTiles(levelRoot, config);
centerLevelRoot(levelRoot, config);
let layout = levelRoot.getComponent(LevelTileLayout);
if (!layout) layout = levelRoot.addComponent(LevelTileLayout);
layout.setRuntimeConfig(config);
layout.applyLayout();
layout.scheduleOnce(() => {
if (!levelRoot?.isValid) return;
layoutLevelTiles(levelRoot, config);
centerLevelRoot(levelRoot, config);
GridSnapHelper.purgeRuntimeGrids(levelRoot);
}, 0);
this.syncMapDataComponent(levelRoot, config);
GridSnapHelper.purgeRuntimeGrids(levelRoot);
console.log(`[LevelDisplay] 关卡 ${config.levelID} 同步 ${tileCount} 格,地图主题=${theme}`);
}
static syncMapDataComponent(levelRoot: Node, config: LevelConfig) {
const md = levelRoot.getComponent(LevelMapData);
if (!md) return;
md.levelID = config.levelID;
md.theme = resolveLevelTheme(config);
if (config.ground && Object.keys(config.ground).length > 0) {
md.groundJson = JSON.stringify(config.ground);
}
if (config.border && Object.keys(config.border).length > 0) {
md.borderJson = JSON.stringify(config.border);
}
}
static collectTileNames(config: LevelConfig): string[] {
const names = new Set<string>(['Baseblock', 'JumpBlock', 'WallBlock', 'kuai11']);
const decorKey = getThemeBorderDecorKey(config.theme);
if (decorKey) names.add(decorKey);
for (const v of Object.values(config.ground ?? {})) {
const tile = typeof v === 'string' ? v.trim() : (typeof v === 'number' ? String(v) : '');
if (tile) names.add(tile);
}
for (const v of Object.values(config.border ?? {})) {
if (v === true) continue;
const tile = typeof v === 'string' ? v.trim() : (typeof v === 'number' ? String(v) : '');
if (tile) names.add(tile);
}
return Array.from(names);
}
static ensureUILayerTree(root: Node) {
const walk = (node: Node | null | undefined) => {
if (!node?.isValid) return;
node.layer = UI_LAYER;
node.active = true;
let ui = node.getComponent(UITransform);
if (!ui) ui = node.addComponent(UITransform);
if (node === root || node.name === 'Ground' || node.name === 'Border') {
ui.setAnchorPoint(0, 0);
ui.setContentSize(1, 1);
}
for (const ch of node.children) walk(ch);
};
walk(root);
}
static sortIsoLayers(levelRoot: Node) {
sortIsoTiles(levelRoot);
}
private static queueTileSprite(
jobs: Promise<void>[],
node: Node,
tileName: string,
cellX: number,
cellY: number,
theme: string,
) {
jobs.push(VisualAssets.applyNamedTile(node, tileName, 255, cellX, cellY, theme));
}
/** 仅刷新砖块贴图(实体已由 GameController 单独处理) */
static async refreshTiles(levelRoot: Node, config: LevelConfig): Promise<void> {
const theme = resolveLevelTheme(config);
await VisualAssets.preloadLevelTiles(theme, this.collectTileNames(config));
await this.refreshTileSprites(levelRoot, config, theme);
}
static async refreshTileSprites(levelRoot: Node, config: LevelConfig, theme: string) {
const jobs: Promise<void>[] = [];
const processLayer = (layer: Node | null, isBorder: boolean) => {
if (!layer) return;
for (const ch of layer.children) {
if (!ch?.active) continue;
const m = isBorder
? /^b_(-?\d+)_(-?\d+)$/.exec(ch.name)
: /^g_(-?\d+)_(-?\d+)$/.exec(ch.name);
if (!m) continue;
const key = `${m[1]},${m[2]}`;
const cx = parseInt(m[1], 10);
const cy = parseInt(m[2], 10);
let tileName: string;
if (isBorder) {
tileName = config.border?.[key] as string | boolean | undefined;
if (tileName === true || tileName === undefined) tileName = 'WallBlock';
if (typeof tileName !== 'string') tileName = 'WallBlock';
} else {
tileName = config.ground?.[key] ?? CommonDefine.BlockBase;
}
this.queueTileSprite(jobs, ch, tileName, cx, cy, theme);
}
};
processLayer(levelRoot.getChildByName('Ground'), false);
processLayer(levelRoot.getChildByName('Border'), true);
const tiles = levelRoot.getChildByName('Tiles');
if (tiles) {
for (const ch of tiles.children) {
if (!ch.active) continue;
const mg = /^g_(-?\d+)_(-?\d+)$/.exec(ch.name);
const mb = /^b_(-?\d+)_(-?\d+)$/.exec(ch.name);
if (mg) {
const key = `${mg[1]},${mg[2]}`;
this.queueTileSprite(
jobs, ch,
config.ground?.[key] ?? CommonDefine.BlockBase,
parseInt(mg[1], 10), parseInt(mg[2], 10), theme,
);
} else if (mb) {
const key = `${mb[1]},${mb[2]}`;
let tileName = config.border?.[key];
if (tileName === true || tileName === undefined) tileName = 'WallBlock';
if (typeof tileName !== 'string') tileName = 'WallBlock';
this.queueTileSprite(
jobs, ch, tileName,
parseInt(mb[1], 10), parseInt(mb[2], 10), theme,
);
}
}
}
await Promise.all(jobs);
layoutLevelTiles(levelRoot, config);
let n = 0;
const count = (node: Node) => {
if (node.active && node.getComponent(Sprite)?.spriteFrame) n++;
for (const c of node.children) count(c);
};
count(levelRoot);
console.log(`[LevelDisplay] 关卡 ${config.levelID} 地图主题=${theme} 已刷新 ${n}`);
}
}