Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
7.7 KiB
TypeScript
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} 块`);
|
|
}
|
|
}
|