import { _decorator, Component, Node, Vec3, UITransform, director, Enum, find, instantiate, CCInteger, Sprite, } from 'cc'; import { EDITOR, PREVIEW } from 'cc/env'; import { CELL_PIXEL } from './core/GridConstants'; import { cellToWorld as isoCellToWorld, cellToWorldCenter, worldCenterToCell, } from './core/GridCoords'; import { CommonDefine, Direction, GameState, GridType, Skin, addDirection, themeToSkin, } from './core/Define'; import { EventManager, EventType } from './core/EventManager'; import { ExternalLevelInfo, JsBridge } from './bridge/JsBridge'; import { hasLevel, getMaxLevelId, getMinLevelId, LEVEL_ID_BASE, resolveLevelConfig, nextLevelId, prevLevelId, } from './level/LevelRegistry'; import { LevelConfig, SpawnConfig, SpawnKind } from './level/LevelTypes'; import { PropPlacement, propWorldYOffset, resolvePropPlacement, entityWorldPositionForRole, worldToMoverCellForRole, } from './level/EntitySpawnPlacement'; import { EntityVisualOptions } from './visual/EntityTextureResolver'; import { getLevelPrefabResourcePath } from './level/LevelPrefabPaths'; import { loadLevelPrefab } from './level/LevelPrefabLoader'; import { PlayerController } from './controller/PlayerController'; import { VehicleController } from './controller/VehicleController'; import { PropController } from './controller/PropController'; import { VisualAssets } from './visual/VisualAssets'; import { resolveThemeId } from './theme/ThemeDatabase'; import { LevelDisplay } from './level/LevelDisplay'; import { mergeLevelConfigWithMapData } from './level/LevelConfigMerge'; import { ensureEntityUILayer, findLevelChildByName, forEachLevelEntityNode, sortIsoTiles } from './level/TileLayout'; import { Movement } from './gameplay/Movement'; import { tryLinkPlayerVehicle, tryLinkVehicleRider } from './controller/RideLink'; import { GridSnapHelper } from './level/GridSnapHelper'; import { LineGridRenderer } from './gameplay/LineGridRenderer'; import { ThemeBackground } from './theme/ThemeBackground'; import { GameAudio } from './audio/GameAudio'; import { reloadThemeDatabase } from './theme/ThemeRegistry'; import { reloadTileDisplayMeta } from './visual/TileDisplayMeta'; import { UIMain } from './ui/UIMain'; import { clearUIIconCache } from './ui/UIStyleAssets'; const { ccclass, property } = _decorator; interface GridEntry { type: GridType; node: Node; } /** * 主站唯一入口组件(原 GameManager + GameController 合并)。 * Inspector:填写 inputLevel,预览 ▶ 后点 SwitchLevel(对齐 Unity TestGame2)。 */ @ccclass('GameController') export class GameController extends Component { static instance: GameController | null = null; @property({ group: { name: '关卡切换', id: '1' }, displayName: 'inputLevel' }) inputLevel = '1'; @property({ group: { name: '关卡切换', id: '1' }, type: CCInteger, displayName: '当前关卡', readonly: true }) curLevelID = 1; @property({ group: { name: '关卡切换', id: '1' }, displayName: '关卡范围', readonly: true }) levelRangeHint = '1–80'; @property({ group: { name: '关卡切换', id: '1' }, type: CCInteger, displayName: '启动关卡 Initial Level ID' }) initialLevelID = LEVEL_ID_BASE; @property({ group: { name: '场景', id: '2' }, displayName: 'Main Level Entrance', type: Node }) mainLevelEntrance: Node | null = null; @property({ group: { name: '场景', id: '2' }, displayName: 'Cur Level', type: Node, readonly: true }) curLevel: Node | null = null; @property({ group: { name: '高级', id: '3' }, type: Enum(Skin), displayName: 'Player Skin' }) playerSkin: Skin = Skin.Silu; @property({ group: { name: '高级', id: '3' }, displayName: 'UI Style' }) uiStyle = 'default'; @property({ group: { name: '高级', id: '3' }, displayName: 'inputStyle' }) inputStyle = 'default'; @property({ group: { name: '高级', id: '3' }, displayName: '调试-跳过角色与可拾取物' }) skipPlayerAndProps = false; gameState: GameState = GameState.Run; isInputEnd = false; stepNum = 0; ready = false; private levelLoadSeq: Promise = Promise.resolve(); /** 倍速 1/2/4,作用于移动与角色动画 */ gameSpeed = 1; private creating = false; private curConfig: LevelConfig | null = null; private gridTypes = new Map(); private gridTypesForProps = new Map(); private groundCells = new Map(); private borderCells = new Set(); onLoad() { if (GameController.instance && GameController.instance !== this) { this.destroy(); return; } GameController.instance = this; this.levelRangeHint = `${getMinLevelId()}–${getMaxLevelId()}`; this.registerWebApi(); } start() { const raw = typeof this.inputLevel === 'string' ? this.inputLevel : String(this.inputLevel ?? ''); if (!raw.trim()) { this.inputLevel = String(this.curLevelID || this.initialLevelID); } } update() { if (EDITOR && !PREVIEW) return; this.resolveMainLevelEntrance(); this.syncCurLevelRef(); } onDestroy() { if (GameController.instance === this) GameController.instance = null; } // --- Inspector / 扩展按钮 --- /** Inspector 扩展 SwitchLevel 按钮(Unity TestGame2 同款) */ clickSwitchLevel() { this.switchToInputLevel(); } /** Inspector:上一关(按关卡库顺序) */ clickPrevLevel() { this.prevLevel(); } /** Inspector:下一关 */ clickNextLevel() { this.nextLevel(); } // --- 预览 / Inspector 调试(对齐 Unity TestPlayer)--- debugMove(n: number) { this.sendMessage('Player', 'CallMove', n); } debugJump() { this.sendMessage('Player', 'CallJump'); } debugRotateLeft(n = 1) { this.sendMessage('Player', 'CallRotateLeft', n); } debugRotateRight(n = 1) { this.sendMessage('Player', 'CallRotateRight', n); } debugPlayerInfo() { this.sendMessage('Player', 'CallPlayerInfo'); } debugInputEnd() { this.callSetIsInputEnd(1); } debugVehicleMove(n: number) { this.sendMessage('Vehicle', 'CallMove', n); } debugVehicleInfo() { this.sendMessage('Vehicle', 'CallVehicleInfo'); } debugResetLevel() { this.resetLevel(); } /** 读取 inputLevel 并切换 */ switchToInputLevel() { if (!this.resolveMainLevelEntrance()) { console.warn('[GameController] mainLevelEntrance 未绑定,请等 AppBootstrap 完成'); return; } this.applyOptionalParams(); this.confirmSwitchLevel(); } /** Unity: SendMessage('GameController', 'SwitchLevel', levelId) */ SwitchLevel(levelID: number) { this.confirmSwitchLevel(levelID); } switchLevel(levelID: number) { this.confirmSwitchLevel(levelID); } prevLevel() { if (!this.resolveMainLevelEntrance()) return; this.confirmSwitchLevel(prevLevelId(this.curLevelID)); } nextLevel() { if (!this.resolveMainLevelEntrance()) return; this.confirmSwitchLevel(nextLevelId(this.curLevelID)); } markReady() { this.ready = true; this.levelRangeHint = `${getMinLevelId()}–${getMaxLevelId()}`; if (typeof window !== 'undefined') { (window as unknown as { __tfrhCocosReady?: boolean }).__tfrhCocosReady = true; } } onBootstrapReady() { this.levelRangeHint = `${getMinLevelId()}–${getMaxLevelId()}`; } private applyOptionalParams() { const style = (typeof this.inputStyle === 'string' ? this.inputStyle : String(this.inputStyle ?? '')).trim() || (typeof this.uiStyle === 'string' ? this.uiStyle : String(this.uiStyle ?? '')).trim(); if (style) { this.uiStyle = style; this.changeUIStyle(style); } const player = this.findNodeByName('Player'); player?.getComponent(PlayerController)?.callChangeSkin?.(this.playerSkin); } resolveMainLevelEntrance(): boolean { if (this.mainLevelEntrance?.isValid) return true; const scene = director.getScene(); if (!scene) return false; for (const p of ['GameRoot/MainLevelEntrance', 'Canvas/GameRoot/MainLevelEntrance', 'MainLevelEntrance']) { const n = find(p, scene); if (n?.isValid) { this.mainLevelEntrance = n; return true; } } return false; } private syncCurLevelRef() { if (!this.mainLevelEntrance?.isValid) return; const isLevelRoot = (n: Node) => /^Level_?\d+$/.test(n.name); const found = this.mainLevelEntrance.children.find((c) => c?.isValid && isLevelRoot(c)); if (found) { this.curLevel = found; return; } if (this.curLevel?.isValid && this.curLevel.parent === this.mainLevelEntrance) return; this.curLevel = null; } // --- Web 对接 --- private registerWebApi() { const api = { SendMessage: (objectName: string, methodName: string, param?: string | number) => { this.sendMessage(objectName, methodName, param); }, }; if (typeof window !== 'undefined') { (window as unknown as { cocosIns?: typeof api }).cocosIns = api; (window as unknown as { unityInstance?: typeof api }).unityInstance = api; } } sendMessage(objectName: string, methodName: string, param?: string | number) { if (objectName === 'GameController' || objectName === 'GameManager') { if (methodName === 'SwitchLevel' || methodName === 'switchLevel') { const id = typeof param === 'number' ? param : parseInt(String(param ?? ''), 10); if (!Number.isNaN(id)) this.SwitchLevel(id); return; } this.invoke(this, methodName, param); return; } if (objectName === 'UIMain') { const c = this.findNodeByName('UIMain')?.getComponent(UIMain); if (c) this.invoke(c, methodName, param); return; } const node = this.findNodeByName(objectName); if (!node) { if (objectName === 'Player' && this.creating) return; console.warn(`SendMessage: 未找到 ${objectName}`); return; } for (const comp of node.getComponents(Component)) { if (this.resolveMethod(comp, methodName)) { this.invoke(comp, methodName, param); return; } } console.warn(`SendMessage: ${objectName} 无方法 ${methodName}`); } /** Unity SendMessage 使用 PascalCase;Cocos 组件多为 camelCase */ private resolveMethod(target: object, methodName: string): string | null { const rec = target as Record; if (typeof rec[methodName] === 'function') return methodName; if (methodName.length > 0) { const camel = methodName[0].toLowerCase() + methodName.slice(1); if (typeof rec[camel] === 'function') return camel; } return null; } private invoke(target: object, methodName: string, param?: string | number) { const resolved = this.resolveMethod(target, methodName); if (!resolved) return; const fn = (target as Record)[resolved]; if (typeof fn !== 'function') return; if (param === undefined) (fn as () => void).call(target); else (fn as (p: string | number) => void).call(target, param); } callSetIsInputEnd(v: number) { this.isInputEnd = v !== 0; if (this.isInputEnd) EventManager.dispatch(EventType.InputEnd, this.gameState); } changeUIStyle(style: string) { this.uiStyle = style; VisualAssets.setTheme(style); clearUIIconCache(); void VisualAssets.preload(style); UIMain.findInstance()?.refreshStyle(style); } /** Unity GameManager.ResetLevel */ resetLevel() { void this.enqueueLoadLevel(this.curLevelID, true); } getGameSpeed(): number { return this.gameSpeed; } setGameSpeed(speed: number) { const s = speed > 0 ? speed : 1; this.gameSpeed = s; director.globalGameTimeScale = s; Movement.setSpeedMultiplier(s); } callMute() { UIMain.findInstance()?.setMuted(true); } callUnmute() { UIMain.findInstance()?.setMuted(false); } // --- 游戏逻辑(原 GameManager)--- setGameState(s: GameState) { this.gameState = s; } initData() { this.setGameState(GameState.Run); this.stepNum = 0; this.callSetIsInputEnd(0); } /** Unity MultMode:关卡 id >= 999000 */ isMultMode(): boolean { return this.curLevelID >= 999000; } jsCallCheck(n: number): boolean { if (this.isInputEnd || this.gameState !== GameState.Run) return false; this.stepNum += Math.abs(n); return true; } getRelativePosition(given: Direction, input: Direction): string { const rel = (((input - given) % 4) + 4) % 4; return ['front', 'right', 'back', 'left'][rel]; } cellKey(x: number, y: number) { return `${x},${y}`; } cellToWorld(cell: Vec3): Vec3 { return isoCellToWorld(cell); } /** 实体与砖块均对齐格子中心(tileAnchor 0.5,0.5) */ worldToCell(world: Vec3): Vec3 { return worldCenterToCell(world); } nextGridPosition(pos: Vec3, dir: Direction): Vec3 { const c = this.worldToCell(pos); switch (dir) { case Direction.North: c.x += 1; break; case Direction.South: c.x -= 1; break; case Direction.East: c.y -= 1; break; case Direction.West: c.y += 1; break; } return cellToWorldCenter(c); } calculateGridTypeAtCell(cell: Vec3): GridType { return this.calculateGridType(cellToWorldCenter(cell)); } /** 仅瓦片层格型(不含动态 Ride 覆盖),用于下车 / 载具跟随判定 */ calculateTileGridAtCell(cell: Vec3): GridType { const key = this.cellKey(cell.x, cell.y); if (this.borderCells.has(key)) return GridType.Block; const g = this.groundCells.get(key); if (g === CommonDefine.BlockBase) return GridType.Across; if (g === CommonDefine.BlockJump) return GridType.Jump; const b = this.curConfig?.boundary; if (b && (Math.abs(cell.x) >= b.x || Math.abs(cell.y) >= b.y)) return GridType.Boundary; return GridType.None; } calculateNextTileGridAtCell(cell: Vec3, dir: Direction): GridType { return this.calculateTileGridAtCell(this.offsetCell(cell, dir)); } calculateLastTileGridAtCell(cell: Vec3, dir: Direction): GridType { return this.calculateTileGridAtCell(this.offsetCell(cell, addDirection(dir, 2))); } private offsetCell(cell: Vec3, dir: Direction): Vec3 { const c = cell.clone(); switch (dir) { case Direction.North: c.x += 1; break; case Direction.South: c.x -= 1; break; case Direction.East: c.y -= 1; break; case Direction.West: c.y += 1; break; } return c; } calculateNextGridTypeAtCell(cell: Vec3, dir: Direction): GridType { return this.calculateGridTypeAtCell(this.offsetCell(cell, dir)); } calculateLastGridTypeAtCell(cell: Vec3, dir: Direction): GridType { return this.calculateGridTypeAtCell(this.offsetCell(cell, addDirection(dir, 2))); } calculateGridType(pos: Vec3): GridType { const cell = this.worldToCell(pos); const key = this.cellKey(cell.x, cell.y); const dyn = this.gridTypes.get(key); if (dyn) return dyn.type; if (this.borderCells.has(key)) return GridType.Block; const g = this.groundCells.get(key); if (g === CommonDefine.BlockBase) return GridType.Across; if (g === CommonDefine.BlockJump) return GridType.Jump; const b = this.curConfig?.boundary; if (b && (Math.abs(cell.x) >= b.x || Math.abs(cell.y) >= b.y)) return GridType.Boundary; return GridType.None; } calculateNextGridType(pos: Vec3, dir: Direction): GridType { return this.calculateGridType(this.nextGridPosition(pos, dir)); } calculateLastGridType(pos: Vec3, dir: Direction): GridType { return this.calculateGridType(this.nextGridPosition(pos, addDirection(dir, 2))); } getGameObject(pos: Vec3): Node | null { const c = this.worldToCell(pos); return this.getGameObjectAtCell(c); } getGameObjectAtCell(cell: Vec3): Node | null { return this.gridTypes.get(this.cellKey(cell.x, cell.y))?.node ?? null; } addObj(pos: Vec3, type: GridType, node: Node) { this.addObjAtCell(this.worldToCell(pos), type, node); } addObjAtCell(cell: Vec3, type: GridType, node: Node) { this.gridTypes.set(this.cellKey(cell.x, cell.y), { type, node }); } removeObj(pos: Vec3) { this.removeObjAtCell(this.worldToCell(pos)); } removeObjAtCell(cell: Vec3) { this.gridTypes.delete(this.cellKey(cell.x, cell.y)); } removeProp(pos: Vec3) { this.removePropAtCell(this.worldToCell(pos)); } removePropAtCell(cell: Vec3) { this.gridTypesForProps.delete(this.cellKey(cell.x, cell.y)); } countProp(): number { if (!this.curLevel) return 0; let n = 0; this.forEachPropInLevel(this.curLevel, (c) => { if (c?.isValid && c.active) n++; }); return n; } /** 关卡内是否已拾取全部可收集物(金币等) */ allPropsCollected(): boolean { return this.countProp() === 0; } private forEachPropInLevel(levelRoot: Node, fn: (node: Node) => void): void { if (!levelRoot?.isValid) return; const walk = (node: Node) => { if (!node?.isValid) return; if (node !== levelRoot) { const prop = node.getComponent(PropController); if (prop) { fn(node); return; } } for (const ch of node.children) walk(ch); }; walk(levelRoot); } getCurLevel() { return this.curConfig; } getEntityVisualOptions(spawnTexture?: string): EntityVisualOptions { return { theme: this.curConfig?.theme ?? this.uiStyle, entityTextures: this.curConfig?.entityTextures, spawnTexture, }; } findNodeByName(name: string): Node | null { const scene = director.getScene(); if (!scene) return null; return this.findInTree(scene, name); } private findInTree(root: Node, name: string): Node | null { if (root.name === name) return root; for (const ch of root.children) { const f = this.findInTree(ch, name); if (f) return f; } return null; } destroyCurLevel() { if (this.mainLevelEntrance?.isValid) { const grid = this.findInTree(this.mainLevelEntrance, 'LineGrid'); if (grid?.isValid) { grid.parent = this.mainLevelEntrance; grid.setPosition(0, 0, 0); } for (const ch of [...this.mainLevelEntrance.children]) { if (!ch || ch.name === 'LineGrid') continue; if (ch.isValid) ch.destroy(); } } else if (this.curLevel?.isValid) { this.curLevel.destroy(); } this.curLevel = null; this.syncCurLevelRef(); this.gridTypes.clear(); this.gridTypesForProps.clear(); this.groundCells.clear(); this.borderCells.clear(); } confirmSwitchLevel(levelID?: number) { const raw = levelID !== undefined ? String(levelID) : (typeof this.inputLevel === 'string' ? this.inputLevel : String(this.inputLevel ?? '')).trim(); if (!raw) { console.warn('[GameController] 关卡 ID 为空'); return; } const id = parseInt(raw, 10); if (Number.isNaN(id) || id <= 0) { console.warn('[GameController] 关卡 ID 无效:', raw); return; } if (!hasLevel(id)) { console.warn( `[GameController] 关卡 ${id} 不在 Cocos 关卡库 (${getMinLevelId()}–${getMaxLevelId()}),` + `仍按 Level${id}.prefab 加载`, ); } this.inputLevel = String(id); void this.loadLevel(id); } loadLevel(levelID: number) { if (!this.ready && !this.mainLevelEntrance) { console.warn('[GameController] 尚未初始化'); return; } if (!this.mainLevelEntrance) { console.error('[GameController] mainLevelEntrance 未绑定'); return; } void this.enqueueLoadLevel(levelID, false); } private enqueueLoadLevel(levelID: number, forceRestart: boolean) { this.levelLoadSeq = this.levelLoadSeq .then(() => this.loadLevelNow(levelID, forceRestart)) .catch((e) => console.error('[GameController] 关卡加载失败', e)); return this.levelLoadSeq; } private async loadLevelNow(levelID: number, forceRestart: boolean) { if (levelID === this.curLevelID && this.curLevel?.isValid && this.curConfig) { await this.restartCurrentLevel(); return; } this.destroyCurLevel(); await this.createNewLevel(levelID); } /** 同关 SwitchLevel:只补刷贴图,避免无预加载的软重置导致资源错乱 */ private async refreshLevelPresentation() { const config = this.curConfig; const levelRoot = this.curLevel; if (!config || !levelRoot?.isValid) return; await reloadThemeDatabase(); await reloadTileDisplayMeta(); const tileNames = LevelDisplay.collectTileNames(config); await VisualAssets.ensureLevelAssetsReady(config, tileNames); await LevelDisplay.refreshTiles(levelRoot, config); await VisualAssets.refreshLevelEntityVisuals(levelRoot, config); this.reapplyThemeEntityPositions(levelRoot, config); this.syncPlayerAppearanceToLevel(config); console.log(`[GameController] 已刷新关卡 ${this.curLevelID} 贴图`); } /** 重置玩家/道具,保留关卡砖块与背景,避免闪屏 */ private async restartCurrentLevel() { if (!this.curLevel?.isValid || !this.curConfig || !this.mainLevelEntrance) { await this.createNewLevel(this.curLevelID); return; } const config = this.curConfig; const levelRoot = this.curLevel; const tileNames = LevelDisplay.collectTileNames(config); await VisualAssets.ensureLevelAssetsReady(config, tileNames); await this.purgeDynamicEntities(levelRoot); const spawned = await this.spawnAllEntities(levelRoot, config); await this.finalizeEntityPresentation(levelRoot, config); this.initGridTypes(); this.initData(); EventManager.dispatch(EventType.LevelInit); this.refreshAllVehicleIcons(levelRoot); this.externalCallLevelInfo(spawned); this.syncPlayerAppearanceToLevel(config); console.log(`[GameController] 已重置关卡 ${this.curLevelID}(无重载)`); } /** 主题 entityDisplay 中 Y 偏移/站立补偿变更后,重算实体世界坐标(缩放只改贴图尺寸) */ private reapplyThemeEntityPositions(levelRoot: Node, config: LevelConfig) { const theme = config.theme || 'silu'; for (const s of config.spawns ?? []) { if (s.kind === 'player') { const pc = findLevelChildByName(levelRoot, 'Player')?.getComponent(PlayerController); pc?.reapplyCellStandPosition(); continue; } if (s.kind === 'vehicle') { const vc = findLevelChildByName(levelRoot, 'Vehicle')?.getComponent(VehicleController); vc?.reapplyCellStandPosition(); continue; } if (s.kind === 'prop') { const node = findLevelChildByName(levelRoot, `Prop_${s.x}_${s.y}`); if (!node?.isValid) continue; const cell = new Vec3(s.x, s.y, 0); const placement = resolvePropPlacement(s, config); const pos = cellToWorldCenter(cell); pos.y += propWorldYOffset(placement, theme); node.setPosition(pos); } } } private isDynamicEntity(node: Node): boolean { const n = node.name; if (n === 'Player' || /^Player[AB]\d$/.test(n)) return true; if (n === 'Vehicle' || /^Vehicle[AB]\d$/.test(n)) return true; if (n === 'PropDecor' || n.startsWith('PropDecor_')) return true; if (n === 'Prop' || n.startsWith('Prop_')) return true; return !!( node.getComponent(PlayerController) || node.getComponent(VehicleController) || node.getComponent(PropController) ); } private collectDynamicEntities(levelRoot: Node): Node[] { const found: Node[] = []; const walk = (node: Node) => { if (!node?.isValid) return; if (node !== levelRoot && this.isDynamicEntity(node)) { found.push(node); return; } for (const ch of [...node.children]) walk(ch); }; walk(levelRoot); return found; } /** destroy() 延迟到帧末;先移出场景并等一帧,避免刷新时旧道具与新道具叠在一起 */ private purgeDynamicEntities(levelRoot: Node): Promise { const toRemove = this.collectDynamicEntities(levelRoot); for (const ch of toRemove) { ch.removeFromParent(); ch.destroy(); } return new Promise((resolve) => { this.scheduleOnce(() => resolve(), 0); }); } private async spawnAllEntities(levelRoot: Node, config: LevelConfig): Promise { const spawned: Node[] = []; for (const s of config.spawns ?? []) { if (this.skipPlayerAndProps && this.isSkippedSpawn(s)) continue; const node = await this.spawnEntityAsync(levelRoot, s, config); if (node) spawned.push(node); } await this.repairMissingProps(levelRoot, config); return spawned; } /** 刷新后偶发贴图未挂上:补刷缺失或无形的可拾取物 */ private async repairMissingProps(levelRoot: Node, config: LevelConfig): Promise { const propSpawns = (config.spawns ?? []).filter((s) => s.kind === 'prop' || s.kind === 'prop_decor'); if (!propSpawns.length) return; for (const s of propSpawns) { const nodeName = s.kind === 'prop' ? `Prop_${s.x}_${s.y}` : 'PropDecor'; let node = findLevelChildByName(levelRoot, nodeName); if (!node?.isValid) { node = await this.spawnEntityAsync(levelRoot, s, config); } if (!node?.isValid) continue; const spr = node.getComponent(Sprite); if (!spr?.spriteFrame) { let propPlacement = s.propPlacement; if (s.kind === 'prop' && !propPlacement) { propPlacement = resolvePropPlacement(s, config); } await VisualAssets.setupEntityVisualAsync(node, s.kind, undefined, { theme: config.theme || 'silu', entityTextures: config.entityTextures, spawnTexture: s.texture, propPlacement, }, s.scale); } } } private async finalizeEntityPresentation(levelRoot: Node, config: LevelConfig): Promise { const player = findLevelChildByName(levelRoot, 'Player')?.getComponent(PlayerController); if (player) tryLinkPlayerVehicle(player); forEachLevelEntityNode(levelRoot, (ch) => { const vc = ch.getComponent(VehicleController); if (vc) tryLinkVehicleRider(vc); }); await VisualAssets.refreshLevelEntityVisuals(levelRoot, config); forEachLevelEntityNode(levelRoot, (node) => { const mov = node.getComponent(Movement); const spawn = mov?.getSpawnCell?.(); if (spawn) mov.shareCommittedCell(spawn); }); sortIsoTiles(levelRoot); } async createNewLevel(levelID: number) { if (this.creating) return; this.creating = true; const config = resolveLevelConfig(levelID); if (!config || !this.mainLevelEntrance) { this.creating = false; return; } this.curLevelID = levelID; this.curConfig = config; this.inputLevel = String(levelID); await reloadThemeDatabase(); await reloadTileDisplayMeta(); VisualAssets.clearNamedTileCache(); VisualAssets.clearPathFrameCache(); const path = getLevelPrefabResourcePath(levelID, config); try { const prefab = await loadLevelPrefab(path); await VisualAssets.preload(this.uiStyle); // 在 instantiate 前无法拦截;先禁用 prefab 内 GridSnapHelper 的 showGrid GridSnapHelper.stripBeforePlayFromPrefab(prefab); const levelRoot = instantiate(prefab); GridSnapHelper.purgeRuntimeGrids(levelRoot); levelRoot.parent = this.mainLevelEntrance; levelRoot.setPosition(0, 0, 0); this.curLevel = levelRoot; // 预制体 onEnable 可能已画格,挂载后再清一次 GridSnapHelper.purgeRuntimeGrids(this.mainLevelEntrance!); const runtimeConfig = mergeLevelConfigWithMapData(config, levelRoot); this.curConfig = runtimeConfig; this.applyMapDataFromConfig(runtimeConfig); await LevelDisplay.prepare(levelRoot, runtimeConfig); const mapTheme = runtimeConfig.theme || 'silu'; await ThemeBackground.apply(this.mainLevelEntrance, mapTheme); await VisualAssets.ensureLevelAssetsReady( runtimeConfig, LevelDisplay.collectTileNames(runtimeConfig), ); const spawned = await this.spawnAllEntities(levelRoot, runtimeConfig); await this.finalizeEntityPresentation(levelRoot, runtimeConfig); this.syncPlayerAppearanceToLevel(runtimeConfig); await GameAudio.playBackground(levelRoot); this.initGridTypes(); this.initData(); EventManager.dispatch(EventType.LevelInit); this.refreshAllVehicleIcons(levelRoot); if (this.mainLevelEntrance?.isValid) { LineGridRenderer.ensure(this.mainLevelEntrance); } this.externalCallLevelInfo(spawned); GridSnapHelper.purgeRuntimeGrids(levelRoot); this.scheduleOnce(() => { const scene = this.mainLevelEntrance?.scene; if (scene?.isValid) GridSnapHelper.purgeScene(scene); }, 0); console.log(`[GameController] 已加载关卡 ${levelID} (${path})`); } catch (e) { console.error(`[GameController] 预制体加载失败 (${path})`, e); console.error('[GameController] 可运行: python3 tools/bake_cocos_level_prefabs.py'); } finally { this.creating = false; this.syncCurLevelRef(); } } private applyMapDataFromConfig(config: LevelConfig) { this.groundCells.clear(); this.borderCells.clear(); if (config.ground) { for (const [k, v] of Object.entries(config.ground)) this.groundCells.set(k, v); } if (config.border) { for (const k of Object.keys(config.border)) this.borderCells.add(k); } } private initGridTypes() { this.gridTypes.clear(); this.gridTypesForProps.clear(); if (!this.curLevel?.isValid) return; const config = this.curConfig ?? undefined; const theme = config?.theme ?? this.uiStyle; const vehicles: Node[] = []; forEachLevelEntityNode(this.curLevel, (c) => { if (c.name.includes('Vehicle')) vehicles.push(c); }); for (const v of vehicles) { const vc = v.getComponent(VehicleController); const cell = vc?.getCommittedCell() ?? worldToMoverCellForRole(v.position, config, theme, 'vehicle'); this.addObjAtCell(cell, GridType.Ride, v); } for (const s of config?.spawns ?? []) { if (s.kind !== 'vehicle') continue; const key = this.cellKey(s.x, s.y); if (this.gridTypes.has(key)) continue; const node = vehicles.find((v) => { const c = v.getComponent(VehicleController)?.getCommittedCell(); return c && c.x === s.x && c.y === s.y; }); if (node) { this.addObjAtCell(new Vec3(s.x, s.y, 0), GridType.Ride, node); } } forEachLevelEntityNode(this.curLevel, (p) => { const prop = p.getComponent(PropController); if (!prop) return; const spawn = prop.getSpawnCell(); const c = spawn ?? this.worldToCell(p.position); this.gridTypesForProps.set(this.cellKey(c.x, c.y), { type: this.calculateGridTypeAtCell(c), node: p, }); }); } private isSkippedSpawn(s: SpawnConfig): boolean { return s.kind === 'player' || s.kind === 'prop' || s.kind === 'prop_decor' || s.kind === 'vehicle'; } /** LevelInit 后再刷一次载具贴图,覆盖 component.start / 异步预载的 spawn 方向 */ private refreshAllVehicleIcons(levelRoot: Node) { if (!levelRoot?.isValid) return; const refresh = () => { forEachLevelEntityNode(levelRoot, (ch) => { ch.getComponent(VehicleController)?.refreshIcon(); }); }; refresh(); this.scheduleOnce(refresh, 0); } /** 地图主题优先于主站 CallChangeSkin / uiStyle */ private syncPlayerAppearanceToLevel(config: LevelConfig) { const theme = resolveThemeId(config.theme || 'silu'); this.playerSkin = themeToSkin(theme); const player = this.findNodeByName('Player'); player?.getComponent(PlayerController)?.applyLevelTheme(theme); const vehicle = this.findNodeByName('Vehicle') ?? (() => { let found: Node | null = null; if (this.curLevel?.isValid) { forEachLevelEntityNode(this.curLevel, (c) => { if (!found && c.getComponent(VehicleController)) found = c; }); } return found; })(); vehicle?.getComponent(VehicleController)?.refreshIcon(); if (this.curLevel?.isValid) { this.reapplyThemeEntityPositions(this.curLevel, config); } UIMain.findInstance()?.refreshStyle(theme); } private resolveDirection(raw?: Direction | string): Direction | undefined { if (raw === undefined || raw === null) return undefined; if (typeof raw === 'number') return raw as Direction; const name = String(raw).replace(/^Direction\./, ''); const key = name as keyof typeof Direction; if (Object.prototype.hasOwnProperty.call(Direction, key)) { const v = Direction[key]; if (typeof v === 'number') return v as Direction; } return undefined; } private async spawnEntityAsync(parent: Node, s: SpawnConfig, config: LevelConfig): Promise { this.removeExistingSpawn(parent, s); const built = this.buildSpawnEntity(s, config); if (!built) return null; const { node, pos, visual } = built; ensureEntityUILayer(node); node.setPosition(pos); node.parent = parent; await VisualAssets.setupEntityVisualAsync( node, s.kind, visual.direction, visual.options, visual.scaleMul, ); if (s.kind === 'vehicle') { node.getComponent(VehicleController)?.refreshIcon(); } return node; } private removeExistingSpawn(levelRoot: Node, s: SpawnConfig): void { for (const n of this.collectDynamicEntities(levelRoot)) { if (s.kind === 'player' && (n.name.includes('Player') || n.getComponent(PlayerController))) { n.removeFromParent(); n.destroy(); } else if (s.kind === 'vehicle' && (n.name.includes('Vehicle') || n.getComponent(VehicleController))) { n.removeFromParent(); n.destroy(); } else if (s.kind === 'prop' && n.name === `Prop_${s.x}_${s.y}`) { n.removeFromParent(); n.destroy(); } else if (s.kind === 'prop_decor' && (n.name === 'PropDecor' || n.name.startsWith('PropDecor_'))) { n.removeFromParent(); n.destroy(); } } } private buildSpawnEntity(s: SpawnConfig, config: LevelConfig): { node: Node; pos: Vec3; visual: { direction?: Direction; options: EntityVisualOptions; scaleMul: number; }; } | null { const mapTheme = config.theme || 'silu'; const cell = new Vec3(s.x, s.y, 0); let propPlacement: PropPlacement | undefined; let pos: Vec3; if (s.kind === 'player') { pos = entityWorldPositionForRole(cell, config, mapTheme, 'player'); } else if (s.kind === 'vehicle') { pos = entityWorldPositionForRole(cell, config, mapTheme, 'vehicle'); } else { pos = cellToWorldCenter(cell); if (s.kind === 'prop') { propPlacement = resolvePropPlacement(s, config); pos.y += propWorldYOffset(propPlacement, mapTheme); } } let node: Node; let direction: Direction | undefined; if (s.kind === 'player') { node = new Node('Player'); node.addComponent(PlayerController); const pc = node.getComponent(PlayerController)!; direction = this.resolveDirection(s.playerDirection) ?? Direction.South; pc.direction = direction; pc.setSpawnCell(cell); } else if (s.kind === 'vehicle') { node = new Node('Vehicle'); const vc = node.addComponent(VehicleController); direction = this.resolveDirection(s.vehicleDirection) ?? Direction.North; vc.direction = direction; vc.setSpawnCell(cell); } else if (s.kind === 'prop') { node = new Node(`Prop_${s.x}_${s.y}`); const propCtrl = node.addComponent(PropController); propCtrl.setSpawnCell(cell); } else if (s.kind === 'prop_decor') { node = new Node('PropDecor'); } else { return null; } return { node, pos, visual: { direction, options: { theme: mapTheme, entityTextures: config.entityTextures, spawnTexture: s.texture, propPlacement, }, scaleMul: s.scale ?? 1, }, }; } /** * 关卡加载完成时通知主站(对齐 Unity GameManager.ExternalCallLevelInfo)。 * 网页侧实现全局函数 externalLevelInfo(json) 接收 JSON: * { LevelID, PlayerName, VehicleName } */ externalCallLevelInfo(objects: Node[]) { const info: ExternalLevelInfo = { LevelID: this.curLevelID, PlayerName: '', VehicleName: '', }; for (const obj of objects) { if (obj.name.includes('Player')) { info.PlayerName = obj.name; } else if (obj.name.includes('Vehicle')) { info.VehicleName = obj.name; } } console.log(`[GameController] externalLevelInfo LevelID=${info.LevelID}`); JsBridge.notifyExternalLevelInfo(info); } } /** 兼容旧代码 `GameManager.instance` */ export { GameController as GameManager };