import { _decorator, Component, Vec3 } from 'cc'; import { Direction, GameState, GridType, MoveState, MoverRole, addDirection, } from '../core/Define'; import { GameManager } from '../manager/GameManager'; import { scaledMoveSpeed, UNITY_VEHICLE_MOVE_SPEED } from '../core/GridConstants'; import { cellToWorldCenter } from '../core/GridCoords'; import { entityWorldPositionForRole, moverLogicalGridPositionForRole, roleUsesPlayerStandOffset, worldToMoverCellForRole, } from '../level/EntitySpawnPlacement'; import { checkMoveStep, MoveCheckResult } from './MoveRules'; const { ccclass, property } = _decorator; /** * 离散格子移动基类(对齐 Unity Movement) * - 查表判定能否进入下一格 * - FixedUpdate 式插值到 targetPosition * - 骑乘联动由 Player/Vehicle 子类覆写 */ @ccclass('Movement') export class Movement extends Component { private static speedMultiplier = 1; static setSpeedMultiplier(m: number) { Movement.speedMultiplier = m > 0 ? m : 1; if (typeof globalThis !== 'undefined') { (globalThis as { __tfrhGameSpeed?: number }).__tfrhGameSpeed = Movement.speedMultiplier; } } static getSpeedMultiplier(): number { return Movement.speedMultiplier; } @property moveSpeed = scaledMoveSpeed(UNITY_VEHICLE_MOVE_SPEED); direction: Direction = Direction.North; moveState: MoveState = MoveState.Idle; moverRole: MoverRole = 'player'; protected targetPosition = new Vec3(); /** 目标格类型(含 Ride 动态覆盖),对齐 Unity Movement.targetGridType */ protected targetGridType: GridType = GridType.None; protected lastPosition = new Vec3(); protected step = 0; protected moveWait = false; protected committedCell: Vec3 | null = null; /** 关卡 spawn 逻辑格(优先于从世界坐标反推,避免站立 Y 微调导致格偏移) */ protected spawnCell: Vec3 | null = null; protected landingCell: Vec3 | null = null; private moveStepFrom = new Vec3(); private queue: Promise = Promise.resolve(); static callEach = false; protected getMoverLocalPosition(): Vec3 { return this.node.position.clone(); } setSpawnCell(cell: Vec3) { this.spawnCell = cell.clone(); if (!this.committedCell) this.committedCell = new Vec3(); this.committedCell.set(cell); } getSpawnCell(): Vec3 | null { return this.spawnCell ? this.spawnCell.clone() : null; } protected syncCommittedCellFromPosition() { const gm = GameManager.instance; if (!gm) return; if (this.spawnCell) { if (!this.committedCell) this.committedCell = new Vec3(); this.committedCell.set(this.spawnCell); this.spawnCell = null; return; } const config = gm.getCurLevel(); const theme = config?.theme ?? gm.uiStyle; if (!this.committedCell) this.committedCell = new Vec3(); const cell = worldToMoverCellForRole( this.getMoverLocalPosition(), config ?? undefined, theme, this.moverRole, ); this.committedCell.set(cell); } public shareCommittedCell(cell: Vec3) { if (!this.committedCell) this.committedCell = new Vec3(); this.committedCell.set(cell); } getCommittedCell(): Vec3 | null { return this.committedCell ? this.committedCell.clone() : null; } getLandingCell(): Vec3 | null { return this.landingCell ? this.landingCell.clone() : null; } isMoving(): boolean { return this.moveState === MoveState.Moving; } /** 当前移动步进度 0(起步)→ 1(落点) */ getMoveStepProgress(): number { if (this.moveState !== MoveState.Moving) return 1; const total = Vec3.distance(this.moveStepFrom, this.targetPosition); if (total < 1e-4) return 1; const remain = Vec3.distance(this.node.position, this.targetPosition); return Math.max(0, Math.min(1, 1 - remain / total)); } /** 逻辑格采样(含动态 Ride);骑乘时由 PlayerController 覆写 */ protected getGridSamplePosition(): Vec3 { if (this.committedCell) { return cellToWorldCenter(this.committedCell); } const gm = GameManager.instance; const local = this.getMoverLocalPosition(); if (!gm) return local; const config = gm.getCurLevel(); const theme = config?.theme ?? gm.uiStyle; if (this.moverRole === 'player' || this.moverRole === 'vehicle') { return moverLogicalGridPositionForRole( local, config ?? undefined, theme, this.moverRole, ); } return local; } /** 当前格(含 Ride 动态格) */ get curGrid(): GridType { const gm = GameManager.instance!; if (this.committedCell) return gm.calculateGridTypeAtCell(this.committedCell); return gm.calculateGridType(this.getGridSamplePosition()); } get nextGrid(): GridType { const gm = GameManager.instance!; if (this.committedCell) return gm.calculateNextGridTypeAtCell(this.committedCell, this.direction); return gm.calculateNextGridType(this.getGridSamplePosition(), this.direction); } get lastGrid(): GridType { const gm = GameManager.instance!; if (this.committedCell) return gm.calculateLastGridTypeAtCell(this.committedCell, this.direction); return gm.calculateLastGridType(this.getGridSamplePosition(), this.direction); } get isFront(): boolean { return this.direction === Direction.South || this.direction === Direction.East; } start() { this.setDirection(this.direction); this.syncCommittedCellFromPosition(); } protected commitLandingCell() { if (this.landingCell) { if (!this.committedCell) this.committedCell = new Vec3(); this.committedCell.set(this.landingCell); this.landingCell = null; } this.snapMoverToCellStand(); } protected shouldApplyPlayerStandOffset(): boolean { return roleUsesPlayerStandOffset(this.moverRole); } protected snapMoverToCellStand() { const gm = GameManager.instance; if (!gm || !this.committedCell) return; const config = gm.getCurLevel(); const theme = config?.theme ?? gm.uiStyle; const pos = entityWorldPositionForRole( this.committedCell, config ?? undefined, theme, this.moverRole, ); this.node.setPosition(pos); this.targetPosition.set(pos); } /** 主题 entityDisplay 变更后重算站立 Y(缩放刷新不会自动更新节点坐标) */ reapplyCellStandPosition() { this.snapMoverToCellStand(); } resetMoveRuntime() { this.moveState = MoveState.Idle; this.moveWait = false; this.step = 0; this.queue = Promise.resolve(); } update(dt: number) { if (this.moveState !== MoveState.Moving) return; const pos = this.node.position; const next = new Vec3(); const speedMul = GameManager.instance?.getGameSpeed() ?? Movement.speedMultiplier; Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt * speedMul); this.node.setPosition(next); this.onMoving(); this.syncRideAfterMoveStep(); if (Vec3.distance(next, this.targetPosition) < 0.005) { this.node.setPosition(this.targetPosition); this.moveState = MoveState.Idle; this.moveWait = false; this.onMoveToTarget(); } } protected syncRideAfterMoveStep() {} setDirection(dir: Direction) { this.direction = dir; } protected onMoving() {} protected onMoveToTarget() { this.commitLandingCell(); } /** 转向结束(Unity RotateCoroutine 只改朝向,不重新 snap 落点) */ protected onRotateComplete() {} protected onMoveNextSet(_isJump: boolean) {} protected onMoveFail(_isJump: boolean) {} protected playMoveAnim() {} /** 本步移动起点(子类可去掉骑乘视觉抬高等) */ protected resolveMoveStepOrigin(_targetCell: Vec3, _landingGrid: GridType): Vec3 { return this.node.position.clone(); } /** 本步移动落点世界坐标 */ protected resolveTargetWorldPosition(targetCell: Vec3, landingGrid: GridType): Vec3 { const gm = GameManager.instance!; const config = gm.getCurLevel(); const theme = config?.theme ?? gm.uiStyle; return entityWorldPositionForRole( targetCell, config ?? undefined, theme, this.moverRole, ); } protected extraMoveStepCheck(_isJump: boolean, _toFront: boolean): MoveCheckResult | null { return null; } private moveNextCheck(isJump: boolean, toFront: boolean): number { const gm = GameManager.instance!; const mult = gm.isMultMode(); const dir = toFront ? this.direction : addDirection(this.direction, 2); const nextG = toFront ? this.nextGrid : this.lastGrid; const blocked = this.extraMoveStepCheck(isJump, toFront); if (blocked !== null) return blocked; const r = checkMoveStep(this.moverRole, this.curGrid, nextG, isJump, mult); if (r !== 1) return r; const targetTmp = gm.nextGridPosition(this.getGridSamplePosition(), dir); const targetCell = gm.worldToCell(targetTmp); if (mult && nextG === GridType.Ride) { const rideNode = gm.getGameObjectAtCell(targetCell); const expectedVehicle = this.node.name.replace('Player', 'Vehicle'); if (rideNode && rideNode.name !== expectedVehicle) return 0; } if (!this.landingCell) this.landingCell = new Vec3(); this.landingCell.set(targetCell); const landingGrid = gm.calculateGridTypeAtCell(targetCell); const targetPos = this.resolveTargetWorldPosition(targetCell, landingGrid); this.targetGridType = landingGrid; this.moveStepFrom.set(this.resolveMoveStepOrigin(targetCell, landingGrid)); this.targetPosition.set(targetPos); return 1; } private enqueue(fn: () => Promise) { this.queue = this.queue.then(fn).catch((e) => console.error(e)); } private waitUntil(cond: () => boolean): Promise { return new Promise((resolve) => { const tick = () => { if (cond()) resolve(); else requestAnimationFrame(tick); }; tick(); }); } private async moveCoroutine(n: number, isJump: boolean) { await this.waitUntil(() => !this.moveWait); const toFront = n > 0; this.step = Math.abs(n); const gm = GameManager.instance!; while (this.step > 0 && gm.gameState === GameState.Run) { this.step--; const r = this.moveNextCheck(isJump, toFront); if (r === -1) { this.onMoveFail(isJump); return; } if (r === 1) { this.moveWait = true; this.lastPosition.set(this.getGridSamplePosition()); this.moveState = MoveState.Moving; this.onMoveNextSet(isJump); await this.waitUntil(() => !this.moveWait); } else { this.playMoveAnim(); this.moveState = MoveState.Moving; // 本步不可走但步数已消耗:须回传 processData / processVehicleData if (this.step === 0) { this.moveState = MoveState.Idle; this.onMoveToTarget(); } } } } private async rotateCoroutine(n: number) { await this.waitUntil(() => !this.moveWait); this.moveWait = true; this.setDirection(addDirection(this.direction, n)); this.moveWait = false; this.onRotateComplete(); // 对齐 Unity RotateCoroutine → OnMoveToTarget → ExternalCall(Scratch/Python 等待 processData) this.onMoveToTarget(); } protected jsCallCheck(n: number): boolean { const gm = GameManager.instance!; const name = this.node.name; if ( name === 'Player' || name === 'Vehicle' || /^Player[AB]\d$/.test(name) || /^Vehicle[AB]\d$/.test(name) ) { return gm.jsCallCheck(n); } return false; } callMove(n: number) { if (!this.jsCallCheck(n)) return; this.enqueue(() => this.moveCoroutine(n, false)); } callRotateLeft(n: number) { if (!this.jsCallCheck(n)) return; this.enqueue(() => this.rotateCoroutine(-n)); } callRotateRight(n: number) { if (!this.jsCallCheck(n)) return; this.enqueue(() => this.rotateCoroutine(n)); } callJump() { if (this.moverRole !== 'player') return; if (!this.jsCallCheck(1)) return; this.enqueue(() => this.moveCoroutine(1, true)); } }