import { Node, UITransform, Vec3, Layers, Sprite } from 'cc'; import { cellToWorldCenter, parseTileNodeName } from '../core/GridCoords'; import { CommonDefine } from '../core/Define'; import { GameManager } from '../manager/GameManager'; import { getTilePivot } from '../visual/TilePivots'; import { getTileDrawSize, resolveTilePixelSize } from '../visual/TileSizes'; import { LevelConfig } from './LevelTypes'; import { getLevelRuntimeContext } from './LevelRuntimeContext'; const UI_LAYER = Layers.Enum.UI_2D; /** * 遮挡分类(仅在关卡加载时按格子坐标排序一次) * - walkable : Baseblock / JumpBlock,永不在 actor 之上 * - wall : Border / WallBlock,与 actor 按 iso 深度比较 * - actor : 角色 / 载具 * - pickable : 可拾取物(Prop),永不在墙砖/路径砖之下;玩家仍在其上 * - scenery : 其它 */ type DrawKind = 'walkable' | 'wall' | 'actor' | 'pickable' | 'scenery'; const DRAW_RANK = { groundProp: 10, groundTile: 20, borderTile: 22, vehicle: 28, coin: 30, propDecor: 30, player: 40, } as const; interface DrawEntry { node: Node; x: number; y: number; rank: number; kind: DrawKind; } function safeNodeName(node: Node | null | undefined): string { return (node?.isValid ? node.name : '') || ''; } function isPlayerEntityName(name: string | undefined | null): boolean { if (!name) return false; return name === 'Player' || /^Player[AB]\d$/.test(name); } function isVehicleEntityName(name: string | undefined | null): boolean { if (!name) return false; return name === 'Vehicle' || /^Vehicle[AB]\d$/.test(name); } function isCoinEntityName(name: string | undefined | null): boolean { if (!name) return false; return name === 'Prop' || name.startsWith('Prop_'); } function isActorEntity(node: Node): boolean { return isPlayerEntityName(safeNodeName(node)) || isVehicleEntityName(safeNodeName(node)); } /** 可拾取金币 / 道具(PropController) */ function isPickableProp(node: Node): boolean { if (!node?.isValid) return false; if (node.getComponent('PropController')) return true; return isCoinEntityName(safeNodeName(node)); } function isEntityDrawNode(node: Node): boolean { const n = safeNodeName(node); if (!n) return false; return isActorEntity(node) || isPickableProp(node) || n === 'PropDecor' || n.startsWith('PropDecor_'); } function isWalkablePathTile(tileName: string): boolean { return tileName === CommonDefine.BlockBase || tileName === CommonDefine.BlockJump; } function isWallTileName(tileName: string): boolean { return tileName === 'WallBlock' || tileName === 'kuai11'; } function resolveGroundTileName(cellX: number, cellY: number, config?: LevelConfig): string { return config?.ground?.[`${cellX},${cellY}`] ?? CommonDefine.BlockBase; } function classifyTileKind( parsed: { layer: 'ground' | 'border'; x: number; y: number }, config?: LevelConfig, ): DrawKind { if (parsed.layer === 'border') return 'wall'; const name = resolveGroundTileName(parsed.x, parsed.y, config); if (isWalkablePathTile(name)) return 'walkable'; if (isWallTileName(name)) return 'wall'; return 'scenery'; } function classifyEntityKind(node: Node): DrawKind { if (isPickableProp(node)) return 'pickable'; if (isActorEntity(node)) return 'actor'; return 'scenery'; } export interface EntityDrawOrderOptions { /** @deprecated 保留字段避免旧调用报错 */ preferVehicleOverPlayer?: boolean; } function entityRank(node: Node): number { const n = safeNodeName(node); if (isPlayerEntityName(n)) return DRAW_RANK.player; if (isVehicleEntityName(n)) return DRAW_RANK.vehicle; if (isCoinEntityName(n)) return DRAW_RANK.coin; if (n === 'PropDecor' || n.startsWith('PropDecor_')) return DRAW_RANK.propDecor; return DRAW_RANK.groundProp; } function tileRank(kind: DrawKind, layer: 'ground' | 'border'): number { if (kind === 'walkable') return DRAW_RANK.groundTile; if (kind === 'wall') return DRAW_RANK.borderTile; return layer === 'border' ? DRAW_RANK.borderTile : DRAW_RANK.groundTile; } /** 等距深度:返回值 > 0 表示 a 应排在 b 之后(更靠前) */ export function compareIsoDrawOrder(cellX: number, cellY: number, otherX: number, otherY: number): number { const ka = cellX + cellY; const kb = otherX + otherY; if (ka !== kb) return kb - ka; return otherX - cellX; } function wallFaceSamples(wallX: number, wallY: number): { x: number; y: number }[] { return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }]; } function compareWallActorIso(actor: DrawEntry, wall: DrawEntry): number { let actorAhead = 0; for (const face of wallFaceSamples(wall.x, wall.y)) { const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y); if (iso < 0) return iso; if (iso > actorAhead) actorAhead = iso; } return actorAhead; } function compareDrawEntries(a: DrawEntry, b: DrawEntry): number { if (a.kind === 'walkable' && b.kind === 'actor') return -1; if (b.kind === 'walkable' && a.kind === 'actor') return 1; // 可拾取物:永远在墙砖/路径砖/载具之上;玩家仍在其上 if (a.kind === 'pickable' && b.kind === 'actor') { return isPlayerEntityName(safeNodeName(b.node)) ? -1 : 1; } if (a.kind === 'actor' && b.kind === 'pickable') { return isPlayerEntityName(safeNodeName(a.node)) ? 1 : -1; } if (a.kind === 'pickable' && b.kind !== 'pickable') return 1; if (b.kind === 'pickable' && a.kind !== 'pickable') return -1; if (a.kind === 'actor' && b.kind === 'wall') { const wallIso = compareWallActorIso(a, b); if (wallIso !== 0) return wallIso; } if (a.kind === 'wall' && b.kind === 'actor') { const wallIso = compareWallActorIso(b, a); if (wallIso !== 0) return -wallIso; } const iso = compareIsoDrawOrder(a.x, a.y, b.x, b.y); if (iso !== 0) return iso; return a.rank - b.rank; } /** 实体所在关卡根(Ground/Border/Tiles 的父节点,不是 Tiles 容器本身) */ export function resolveLevelDrawRoot(from: Node): Node | null { let cur: Node | null = from; while (cur?.isValid) { if (cur.getChildByName('Ground') || cur.getChildByName('Border') || cur.getChildByName('Tiles')) { return cur; } if (/^Level_\d+/.test(cur.name)) return cur; cur = cur.parent; } return null; } /** 实体逻辑格(仅 spawn / committed,移动中不重算) */ function getEntityLogicCell(node: Node): { x: number; y: number } { const propCtrl = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null; const propCell = propCtrl?.getSpawnCell?.(); if (propCell) return { x: propCell.x, y: propCell.y }; const m = /^Prop_(-?\d+)_(-?\d+)$/.exec(safeNodeName(node)); if (m) return { x: Number(m[1]), y: Number(m[2]) }; const vehicle = node.getComponent('VehicleController') as { getPlayer?: () => { getCommittedCell?: () => Vec3 | null; getSpawnCell?: () => Vec3 | null } | null; } | null; const rider = vehicle?.getPlayer?.() ?? null; if (rider) { const riderCell = rider.getCommittedCell?.() ?? rider.getSpawnCell?.(); if (riderCell) return { x: riderCell.x, y: riderCell.y }; } const mov = node.getComponent('Movement') as { getCommittedCell?: () => Vec3 | null; getSpawnCell?: () => Vec3 | null; } | null; if (mov) { const cell = mov.getCommittedCell?.() ?? mov.getSpawnCell?.(); if (cell) return { x: cell.x, y: cell.y }; } const gm = GameManager.instance; if (gm) { const c = gm.worldToCell(node.position); return { x: c.x, y: c.y }; } return { x: 0, y: 0 }; } /** 关卡内实体(可能在 levelRoot 或 Tiles 下) */ export function forEachLevelEntityNode(levelRoot: Node, fn: (node: Node) => void) { if (!levelRoot?.isValid) return; for (const ch of levelRoot.children) { if (ch?.isValid && isEntityDrawNode(ch)) fn(ch); } const tiles = levelRoot.getChildByName('Tiles'); if (!tiles?.isValid) return; for (const ch of tiles.children) { if (ch?.isValid && isEntityDrawNode(ch)) fn(ch); } } export function findLevelChildByName(levelRoot: Node, name: string): Node | null { if (!levelRoot?.isValid) return null; const direct = levelRoot.getChildByName(name); if (direct) return direct; return levelRoot.getChildByName('Tiles')?.getChildByName(name) ?? null; } function resolveLevelConfig(): LevelConfig | undefined { return getLevelRuntimeContext()?.getCurLevel() ?? GameManager.instance?.getCurLevel?.() ?? undefined; } function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set): DrawEntry[] { const config = resolveLevelConfig(); const entries: DrawEntry[] = []; const pushTile = (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => { if (seen.has(node)) return; seen.add(node); const kind = classifyTileKind(parsed, config); entries.push({ node, x: parsed.x, y: parsed.y, rank: tileRank(kind, parsed.layer), kind, }); }; forEachTileChild(levelRoot, pushTile); for (const ch of tilesRoot.children) { if (!ch?.isValid || seen.has(ch)) continue; const parsed = parseTileNodeName(ch.name); if (parsed) pushTile(ch, parsed); } return entries; } function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set): DrawEntry[] { const entries: DrawEntry[] = []; const pushEntity = (node: Node | null | undefined) => { if (!node?.isValid || seen.has(node) || !isEntityDrawNode(node)) return; seen.add(node); const cell = getEntityLogicCell(node); entries.push({ node, x: cell.x, y: cell.y, rank: entityRank(node), kind: classifyEntityKind(node), }); }; for (const ch of levelRoot.children) pushEntity(ch); for (const ch of tilesRoot.children) pushEntity(ch); return entries; } function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) { const sorted = entries.slice().sort(compareDrawEntries); const inSort = new Set(); for (const { node } of sorted) { if (!node.isValid) continue; inSort.add(node); if (isEntityDrawNode(node)) ensureEntityUILayer(node); if (node.parent !== tilesRoot) { const lp = node.position.clone(); node.parent = tilesRoot; node.setPosition(lp); } } for (let i = 0; i < sorted.length; i++) { const node = sorted[i].node; if (!node.isValid) continue; if (node.getSiblingIndex() !== i) node.setSiblingIndex(i); } let tail = sorted.length; for (const ch of [...tilesRoot.children]) { if (!ch.isValid || inSort.has(ch)) continue; if (ch.getSiblingIndex() !== tail) ch.setSiblingIndex(tail); tail++; } } function finalizeLevelRootOrder(levelRoot: Node, tilesRoot: Node) { for (const name of ['Ground', 'Border'] as const) { const layer = levelRoot.getChildByName(name); if (layer && layer.children.length === 0) { layer.setSiblingIndex(0); } } tilesRoot.setSiblingIndex(levelRoot.children.length - 1); } /** * 关卡加载 / 重置时按格子坐标排序一次;移动过程中不再改 sibling。 */ export function sortIsoTiles(levelRoot: Node) { if (!levelRoot?.isValid) return; const tilesRoot = ensureTilesRoot(levelRoot); const seen = new Set(); const tileEntries = collectTileEntries(levelRoot, tilesRoot, seen); const entityEntries = collectEntityEntries(levelRoot, tilesRoot, seen); applyDrawOrder(tilesRoot, [...tileEntries, ...entityEntries]); finalizeLevelRootOrder(levelRoot, tilesRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoEntityDrawOrder(levelRoot: Node) { sortIsoTiles(levelRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoDrawOrder(levelRoot: Node) { sortIsoTiles(levelRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoDrawOrderImmediate(levelRoot: Node) { sortIsoTiles(levelRoot); } export function bringEntityNodesToFront(levelRoot: Node, _opts?: EntityDrawOrderOptions) { sortIsoTiles(levelRoot); } export function ensureEntityUILayer(node: Node) { const walk = (n: Node) => { n.layer = UI_LAYER; for (const ch of n.children) walk(ch); }; walk(node); } function ensureTilesRoot(levelRoot: Node): Node { let tilesRoot = levelRoot.getChildByName('Tiles'); if (!tilesRoot) { tilesRoot = new Node('Tiles'); tilesRoot.layer = UI_LAYER; tilesRoot.parent = levelRoot; setupLayerContainer(tilesRoot); } tilesRoot.setSiblingIndex(levelRoot.children.length - 1); return tilesRoot; } const TILE_LAYER_NAMES = ['Ground', 'Border', 'Tiles'] as const; function forEachTileChild(levelRoot: Node, fn: (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => void) { for (const layerName of TILE_LAYER_NAMES) { const layer = levelRoot.getChildByName(layerName); if (!layer) continue; for (const ch of layer.children) { if (!ch?.isValid || !ch.active) continue; const parsed = parseTileNodeName(ch.name); if (!parsed) continue; fn(ch, parsed); } } } export function alignTileNode(node: Node, cellX: number, cellY: number, tileName: string, theme?: string) { const w = cellToWorldCenter(new Vec3(cellX, cellY, 0)); let ui = node.getComponent(UITransform); if (!ui) ui = node.addComponent(UITransform); const spr = node.getComponent(Sprite); const source = resolveTilePixelSize(tileName, spr?.spriteFrame ?? null, theme); const draw = getTileDrawSize(tileName, source.width, source.height, theme); const pivot = getTilePivot(tileName, theme); ui.setContentSize(draw.width, draw.height); ui.setAnchorPoint(pivot.x, pivot.y); node.setPosition(w.x, w.y, 0); node.setScale(1, 1, 1); } export function setupLayerContainer(layer: Node) { let ui = layer.getComponent(UITransform); if (!ui) ui = layer.addComponent(UITransform); ui.setAnchorPoint(0, 0); ui.setContentSize(1, 1); layer.setPosition(0, 0, 0); } function tileNameFromConfig( layer: 'ground' | 'border', key: string, config: LevelConfig, ): string { if (layer === 'ground') { return config.ground?.[key] ?? CommonDefine.BlockBase; } let v = config.border?.[key]; if (v === true || v === undefined) return 'WallBlock'; if (typeof v === 'string') return v; return 'WallBlock'; } export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) { if (!levelRoot?.isValid) return; const theme = config.theme; const ground = levelRoot.getChildByName('Ground'); const border = levelRoot.getChildByName('Border'); if (ground) { setupLayerContainer(ground); for (const ch of ground.children) { const parsed = parseTileNodeName(ch.name); if (!parsed || parsed.layer !== 'ground') continue; const key = `${parsed.x},${parsed.y}`; alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig('ground', key, config), theme); } } if (border) { setupLayerContainer(border); for (const ch of border.children) { const parsed = parseTileNodeName(ch.name); if (!parsed || parsed.layer !== 'border') continue; const key = `${parsed.x},${parsed.y}`; alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig('border', key, config), theme); } } const tilesRoot = levelRoot.getChildByName('Tiles'); if (tilesRoot) { setupLayerContainer(tilesRoot); for (const ch of tilesRoot.children) { const parsed = parseTileNodeName(ch.name); if (!parsed) continue; const key = `${parsed.x},${parsed.y}`; const layer = parsed.layer; alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig(layer, key, config), theme); } } sortIsoTiles(levelRoot); }