Files
cocos_zhuzhan/assets/scripts/GameController.ts

1382 lines
52 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '180';
@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<void> = 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;
/** 已显示在场上、可以同关原地重置的关卡 IDadopt 之后才写入) */
private playableLevelId = 0;
private curConfig: LevelConfig | null = null;
private gridTypes = new Map<string, GridEntry>();
private gridTypesForProps = new Map<string, GridEntry>();
private groundCells = new Map<string, string>();
private borderCells = new Set<string>();
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 使用 PascalCaseCocos 组件多为 camelCase */
private resolveMethod(target: object, methodName: string): string | null {
const rec = target as Record<string, unknown>;
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<string, unknown>)[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<void> {
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<void> {
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<Node[]> {
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<void> {
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<void> {
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<Node | null> {
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 };