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 { ensureRuntimeAssetsForLevel } from './core/RuntimePack'; 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 as loadLevelPrefabImpl } 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, primeIsoDrawOrder, 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; } /** Web 主站:优先走 loader 注入的 loadLevelPrefab(按关下载 + 内存 bundle) */ function loadLevelPrefabForRuntime(path: string) { const hook = (globalThis as { __tfrhLoadLevelPrefab?: typeof loadLevelPrefabImpl }).__tfrhLoadLevelPrefab; if (typeof hook === 'function') return hook(path); return loadLevelPrefabImpl(path); } /** * 主站唯一入口组件(原 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; /** 编辑器最近一次要求加载的关;过期回传不再通知,避免慢关握手错切 */ private requestedLevelID = 0; @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; //终于完成了********PlayerLTH17/2026/9/12 private creating = false; /** SwitchLevel 排队未完成:拦截并暂存 Player/Vehicle 指令,避免执行按钮 200ms 抢跑 */ private levelBusy = false; private pendingLevelOps = 0; private pendingActorMsgs: Array<{ objectName: string; methodName: string; param?: string | number }> = []; /** 进关完成后的干净场景树,同关 SwitchLevel 按 Unity:销毁当前实例再 instantiate 这份缓存 */ private levelSnapshot: Node | null = null; private levelSnapshotId = 0; /** 加载中只保留最后一次 SwitchLevel,避免连点排队重建 */ private queuedLevelId: number | null = null; private instantResetGuard = false; private lastInstantResetAt = 0; private lastInstantResetId = 0; /** 已显示在场上、可以同关原地重置的关卡 ID(adopt 之后才写入) */ private playableLevelId = 0; 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() { this.disposeLevelSnapshot(); 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; this.publishLevelReady(true); } } 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; } if (this.shouldDeferActorMessage(objectName)) { if (methodName === 'CallMove' || methodName === 'callMove') { console.warn('[tfrh-reset] CallMove 被关卡重置拦住,稍后发出', { creating: this.creating, levelBusy: this.levelBusy }); } this.pendingActorMsgs.push({ objectName, methodName, param }); return; } if (methodName === 'CallMove' || methodName === 'callMove') { console.warn('[tfrh-reset] CallMove 立刻执行', { param, playableLevelId: this.playableLevelId, busy: this.levelBusy }); } const node = this.findNodeByName(objectName); if (!node) { if (objectName === 'Player' && (this.creating || this.levelBusy)) 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) continue; ch.active = false; ch.removeFromParent(); ch.destroy(); } } else if (this.curLevel?.isValid) { this.curLevel.active = false; this.curLevel.removeFromParent(); this.curLevel.destroy(); } this.curLevel = null; this.playableLevelId = 0; this.syncCurLevelRef(); this.gridTypes.clear(); this.gridTypesForProps.clear(); this.groundCells.clear(); this.borderCells.clear(); this.disposeLevelSnapshot(); } /** 新关就绪后再卸旧关,避免执行/取消时闪出另一张地图 */ private adoptLevelRoot(levelRoot: Node) { this.curLevel = levelRoot; this.playableLevelId = Number(this.curLevelID); if (!this.mainLevelEntrance?.isValid) return; for (const ch of [...this.mainLevelEntrance.children]) { if (!ch?.isValid || ch === levelRoot || ch.name === 'LineGrid') continue; ch.active = false; ch.removeFromParent(); ch.destroy(); } } 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); this.requestedLevelID = id; this.pendingActorMsgs = []; this.setLevelBusy(true); const inPlace = this.tryResetCurrentLevelInPlace(id); console.warn('[tfrh-reset] SwitchLevel', { id, playableLevelId: this.playableLevelId, curLevelID: this.curLevelID, inPlace, creating: this.creating, pendingLevelOps: this.pendingLevelOps, hasLevel: !!this.curLevel?.isValid, }); if (inPlace) { this.setLevelBusy(false); return; } void this.loadLevel(id); } loadLevel(levelID: number) { if (!this.ready && !this.mainLevelEntrance) { console.warn('[GameController] 尚未初始化'); if (this.pendingLevelOps <= 0) this.setLevelBusy(false); return; } if (!this.mainLevelEntrance) { console.error('[GameController] mainLevelEntrance 未绑定'); if (this.pendingLevelOps <= 0) this.setLevelBusy(false); return; } void this.enqueueLoadLevel(levelID, false); } private enqueueLoadLevel(levelID: number, forceRestart: boolean) { this.queuedLevelId = levelID; if (this.tryResetCurrentLevelInPlace(levelID) && this.pendingLevelOps <= 0 && !this.creating) { this.queuedLevelId = null; this.setLevelBusy(false); return this.levelLoadSeq; } this.setLevelBusy(true); if (this.pendingLevelOps > 0) return this.levelLoadSeq; this.pendingLevelOps++; this.levelLoadSeq = this.levelLoadSeq .then(async () => { while (this.queuedLevelId != null) { const id = this.queuedLevelId; this.queuedLevelId = null; await this.loadLevelNow(id, forceRestart); } }) .catch((e) => console.error('[GameController] 关卡加载失败', e)) .finally(() => { this.pendingLevelOps = Math.max(0, this.pendingLevelOps - 1); if (this.queuedLevelId != null && this.pendingLevelOps <= 0) { void this.enqueueLoadLevel(this.queuedLevelId, forceRestart); return; } if (this.pendingLevelOps <= 0) this.setLevelBusy(false); }); return this.levelLoadSeq; } private shouldDeferActorMessage(objectName: string) { if (!this.levelBusy && !this.creating) return false; return objectName === 'Player' || objectName === 'Vehicle' || /^Player[AB]\d$/.test(objectName) || /^Vehicle[AB]\d$/.test(objectName); } private setLevelBusy(busy: boolean) { this.levelBusy = busy; this.publishLevelReady(!busy); if (!busy) this.flushPendingActorMsgs(); } private publishLevelReady(ready: boolean) { const apply = (target: { __tfrhLevelReady?: boolean; __tfrhLevelBusy?: boolean } | null) => { if (!target) return; target.__tfrhLevelBusy = !ready; target.__tfrhLevelReady = ready; }; if (typeof window === 'undefined') return; apply(window as unknown as { __tfrhLevelReady?: boolean; __tfrhLevelBusy?: boolean }); try { if (window.parent && window.parent !== window) { apply(window.parent as unknown as { __tfrhLevelReady?: boolean; __tfrhLevelBusy?: boolean }); } } catch { /* 跨域 iframe 无法写父页,编辑器仍靠游戏内排队挡住抢跑 */ } } private flushPendingActorMsgs() { if (!this.pendingActorMsgs.length) return; const queued = this.pendingActorMsgs; this.pendingActorMsgs = []; for (const msg of queued) { this.sendMessage(msg.objectName, msg.methodName, msg.param); } } private async loadLevelNow(levelID: number, _forceRestart: boolean) { if (this.tryResetCurrentLevelInPlace(levelID)) return; 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} 贴图`); } /** * 同关执行:只当场拉回出生点,不拆地图。不同 ID 就是不同地图,绝不以偏移折成同一关。 */ private tryResetCurrentLevelInPlace(levelID: number): boolean { if (this.creating) return false; if (Number(levelID) !== Number(this.playableLevelId)) return false; if (!this.curLevel?.isValid || !this.curConfig) return false; this.queuedLevelId = null; this.resetCurrentLevelInPlace(); return true; } private resetCurrentLevelInPlace() { if (this.instantResetGuard) return; const id = Number(this.playableLevelId); const now = Date.now(); if (id === this.lastInstantResetId && now - this.lastInstantResetAt < 50) { this.pendingActorMsgs = []; this.initData(); return; } this.instantResetGuard = true; try { this.restartCurrentLevelSync(); this.lastInstantResetAt = Date.now(); this.lastInstantResetId = id; } finally { this.instantResetGuard = false; } } private captureLevelSnapshot(levelRoot: Node) { this.disposeLevelSnapshot(); if (!levelRoot?.isValid) return; const snap = instantiate(levelRoot); snap.active = false; snap.removeFromParent(); this.levelSnapshot = snap; this.levelSnapshotId = Number(this.curLevelID); } private disposeLevelSnapshot() { if (this.levelSnapshot?.isValid) this.levelSnapshot.destroy(); this.levelSnapshot = null; this.levelSnapshotId = 0; } /** 对齐 Unity Addressables 二次 Instantiate:旧关卸掉,缓存树当场克隆出来。 */ private replayFromSnapshot(): boolean { const snap = this.levelSnapshot; if (!snap?.isValid || this.levelSnapshotId !== Number(this.curLevelID) || !this.mainLevelEntrance) { return false; } const fresh = instantiate(snap); if (!fresh?.isValid) return false; fresh.active = false; GridSnapHelper.purgeRuntimeGrids(fresh); fresh.parent = this.mainLevelEntrance; fresh.setPosition(0, 0, 0); this.adoptLevelRoot(fresh); if (this.curConfig) this.applyMapDataFromConfig(this.curConfig); this.initGridTypes(); this.initData(); EventManager.dispatch(EventType.LevelInit); this.refreshAllVehicleIcons(fresh); this.finishLevelDrawOrder(fresh); this.revealLevelEntities(fresh); fresh.active = true; void GameAudio.playBackground(fresh); GridSnapHelper.purgeRuntimeGrids(fresh); console.log(`[GameController] 已按快照重建关卡 ${this.curLevelID}`); return true; } private restartCurrentLevelSync() { const config = this.curConfig; const levelRoot = this.curLevel; if (!config || !levelRoot?.isValid) return; this.resetExistingMoversToSpawn(levelRoot, config); this.restoreCollectedProps(levelRoot); this.initData(); EventManager.dispatch(EventType.LevelInit); this.applyMapDataFromConfig(config); this.initGridTypes(); this.refreshAllVehicleIcons(levelRoot); this.finishLevelDrawOrder(levelRoot); this.revealLevelEntities(levelRoot); console.log(`[GameController] 已重置关卡 ${this.curLevelID}`); } private restoreCollectedProps(levelRoot: Node) { const walk = (node: Node) => { if (!node?.isValid) return; node.getComponent(PropController)?.restoreForRestart(); for (const ch of node.children) walk(ch); }; walk(levelRoot); } private resetExistingMoversToSpawn(levelRoot: Node, config: LevelConfig) { for (const s of config.spawns ?? []) { if (s.kind === 'player') { const pc = findLevelChildByName(levelRoot, 'Player')?.getComponent(PlayerController); if (!pc) continue; pc.direction = this.resolveDirection(s.playerDirection) ?? Direction.South; pc.setSpawnCell(new Vec3(s.x, s.y, 0)); pc.resetToSpawn(); continue; } if (s.kind === 'vehicle') { const vc = findLevelChildByName(levelRoot, 'Vehicle')?.getComponent(VehicleController); if (!vc) continue; vc.direction = this.resolveDirection(s.vehicleDirection) ?? Direction.North; vc.setSpawnCell(new Vec3(s.x, s.y, 0)); vc.resetToSpawn(); } } } private purgePropEntities(levelRoot: Node): Promise { const toRemove = this.collectDynamicEntities(levelRoot).filter((n) => { const name = n.name; return name === 'Prop' || name.startsWith('Prop_') || name === 'PropDecor' || name.startsWith('PropDecor_') || !!n.getComponent(PropController); }); for (const ch of toRemove) { ch.removeFromParent(); ch.destroy(); } return new Promise((resolve) => { this.scheduleOnce(() => resolve(), 0); }); } /** 主题 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 { await VisualAssets.refreshLevelEntityVisuals(levelRoot, config); // 先按各自 spawn 对齐格子,再绑乘:避免「先同格绑乘 → 再写回各自 spawn」把骑乘格拆散 forEachLevelEntityNode(levelRoot, (node) => { const mov = node.getComponent(Movement); const spawn = mov?.getSpawnCell?.(); if (spawn) mov.shareCommittedCell(spawn); }); const player = findLevelChildByName(levelRoot, 'Player')?.getComponent(PlayerController); if (player) tryLinkPlayerVehicle(player); forEachLevelEntityNode(levelRoot, (ch) => { const vc = ch.getComponent(VehicleController); if (vc) tryLinkVehicleRider(vc); }); // 先排图层再显示,避免载具带着错误 sibling 露出来 sortIsoTiles(levelRoot, true); } private revealLevelEntities(levelRoot: Node) { forEachLevelEntityNode(levelRoot, (node) => { if (node?.isValid && !node.active) node.active = true; }); } private finishLevelDrawOrder(levelRoot: Node | null) { if (!levelRoot?.isValid) return; sortIsoTiles(levelRoot, true); primeIsoDrawOrder(levelRoot, 2); } async createNewLevel(levelID: number) { console.warn('[tfrh-reset] createNewLevel 开始(整关重建)', { levelID, playableLevelId: this.playableLevelId, curLevelID: this.curLevelID, creating: this.creating, }); if (this.creating) { console.warn('[tfrh-reset] createNewLevel 被 creating 挡住,同关执行会走异步队列'); return; } this.creating = true; try { await ensureRuntimeAssetsForLevel(levelID); } catch (e) { console.error('[GameController] 运行时资源加载失败', e); this.creating = false; return; } 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 loadLevelPrefabForRuntime(path); await VisualAssets.preload(this.uiStyle); // 在 instantiate 前无法拦截;先禁用 prefab 内 GridSnapHelper 的 showGrid GridSnapHelper.stripBeforePlayFromPrefab(prefab); const levelRoot = instantiate(prefab); levelRoot.active = false; GridSnapHelper.purgeRuntimeGrids(levelRoot); levelRoot.parent = this.mainLevelEntrance; levelRoot.setPosition(0, 0, 0); // 预制体 onEnable 可能已画格,挂载后再清一次 GridSnapHelper.purgeRuntimeGrids(this.mainLevelEntrance!); const runtimeConfig = mergeLevelConfigWithMapData(config, levelRoot); await LevelDisplay.prepare(levelRoot, runtimeConfig); await VisualAssets.ensureLevelAssetsReady( runtimeConfig, LevelDisplay.collectTileNames(runtimeConfig), ); const spawned = await this.spawnAllEntities(levelRoot, runtimeConfig); await this.finalizeEntityPresentation(levelRoot, runtimeConfig); this.adoptLevelRoot(levelRoot); this.curConfig = runtimeConfig; this.applyMapDataFromConfig(runtimeConfig); levelRoot.active = true; const mapTheme = runtimeConfig.theme || 'silu'; await ThemeBackground.apply(this.mainLevelEntrance, mapTheme); this.syncPlayerAppearanceToLevel(runtimeConfig); await GameAudio.playBackground(levelRoot); this.initGridTypes(); this.initData(); EventManager.dispatch(EventType.LevelInit); this.refreshAllVehicleIcons(levelRoot); this.finishLevelDrawOrder(levelRoot); this.revealLevelEntities(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.active = false; 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[]) { if (this.requestedLevelID > 0 && Number(this.curLevelID) !== Number(this.requestedLevelID)) { console.warn( `[GameController] 跳过过期 externalLevelInfo cur=${this.curLevelID} want=${this.requestedLevelID}`, ); return; } 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 };