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(['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[], 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 { 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[] = []; 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} 块`); } }