Files
cocos_zhuzhan/assets/scripts/gameplay/Movement.ts

485 lines
17 KiB
TypeScript
Raw 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, Vec3 } from 'cc';
import {
Direction, GameState, GridType, MoveState, MoverRole, addDirection,
} from '../core/Define';
import { GameManager } from '../manager/GameManager';
import { markIsoDrawOrderDirty, resolveLevelDrawRoot, sortIsoTiles } from '../level/TileLayout';
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<void> = Promise.resolve();
private moveWaitWakeups: Array<() => void> = [];
/** resetToSpawn / 重开关时作废进行中的 moveCoroutine避免走两步被拽回出生点再走 */
private resetEpoch = 0;
/** 本步实际位移速度(可按动画一轮时长拉长,避免下一格掐断) */
private stepMoveSpeed = 0;
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);
this.markDrawOrderDirty();
}
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);
if (!this.committedCell) {
this.syncCommittedCellFromPosition();
this.markDrawOrderDirty();
}
}
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() {
if (this.moveState === MoveState.Moving) return;
this.snapMoverToCellStand();
this.markDrawOrderDirty();
}
resetMoveRuntime() {
this.resetEpoch++;
this.moveState = MoveState.Idle;
this.moveWait = false;
this.step = 0;
this.stepMoveSpeed = 0;
this.queue = Promise.resolve();
this.wakeMoveWaiters();
}
resetToSpawn() {
const moved = !!(this.spawnCell && this.committedCell
&& (this.committedCell.x !== this.spawnCell.x || this.committedCell.y !== this.spawnCell.y));
const walking = this.moveState !== MoveState.Idle || this.step > 0;
if (moved || walking) {
console.warn(
'[tfrh-reset] 把角色拉回出生点',
this.node.name,
{ from: this.committedCell, spawn: this.spawnCell, walking, step: this.step },
new Error().stack,
);
}
this.resetMoveRuntime();
this.landingCell = null;
if (this.spawnCell) {
if (!this.committedCell) this.committedCell = new Vec3();
this.committedCell.set(this.spawnCell);
}
this.snapMoverToCellStand();
this.markDrawOrderDirty();
}
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;
const speed = this.stepMoveSpeed > 0 ? this.stepMoveSpeed : this.moveSpeed;
Vec3.moveTowards(next, pos, this.targetPosition, speed * dt * speedMul);
this.node.setPosition(next);
this.onMoving();
this.syncRideAfterMoveStep();
this.markDrawOrderDirty();
if (Vec3.distance(next, this.targetPosition) < 0.005) {
this.node.setPosition(this.targetPosition);
this.moveState = MoveState.Idle;
this.moveWait = false;
this.onMoveToTarget();
this.applyDrawOrderAfterMove();
this.wakeMoveWaiters();
}
}
protected markDrawOrderDirty() {
const root = resolveLevelDrawRoot(this.node);
if (root) markIsoDrawOrderDirty(root);
}
/** 落地后按新格写 sibling */
protected applyDrawOrderAfterMove() {
const root = resolveLevelDrawRoot(this.node);
if (root) sortIsoTiles(root, true);
}
protected syncRideAfterMoveStep() {}
setDirection(dir: Direction) {
this.direction = dir;
}
protected onMoving() {}
protected onMoveToTarget() {
this.commitLandingCell();
}
/** 转向结束Unity RotateCoroutine 只改朝向,不重新 snap 落点) */
protected onRotateComplete() {}
protected onMoveNextSet(_isJump: boolean) {}
/** 本 CallMove 最后一格已起步landingCell 已确定),可提前 processData */
protected onLastMoveStepReady() {}
protected onMoveFail(_isJump: boolean) {}
protected playMoveAnim() {}
/** 本步位移至少持续该未缩放秒数(用于对齐走路/跳跃一轮) */
protected getMoveStepMinDuration(): number {
return 0;
}
/** 落地后若动画一轮未完,等播完再走下一格 */
protected waitMoveVisualReady(): Promise<void> {
return Promise.resolve();
}
private resolveStepMoveSpeed() {
const minDur = this.getMoveStepMinDuration();
const dist = Vec3.distance(this.moveStepFrom, this.targetPosition);
if (minDur <= 0 || dist < 1e-4) {
this.stepMoveSpeed = this.moveSpeed;
return;
}
this.stepMoveSpeed = Math.min(this.moveSpeed, dist / minDur);
}
/** 本步移动起点(子类可去掉骑乘视觉抬高等) */
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<void>) {
this.queue = this.queue.then(fn).catch((e) => console.error(e));
}
protected waitUntilIdle(): Promise<void> {
return this.waitUntil(() => this.moveState !== MoveState.Moving && !this.moveWait);
}
private waitUntil(cond: () => boolean): Promise<void> {
if (cond()) return Promise.resolve();
return new Promise((resolve) => {
let settled = false;
const finish = () => {
if (settled || !cond()) return;
settled = true;
resolve();
};
this.moveWaitWakeups.push(finish);
const tick = () => {
if (settled) return;
if (cond()) finish();
else requestAnimationFrame(tick);
};
tick();
});
}
/** 落地当帧唤醒队列,避免再等一帧 rAF */
protected wakeMoveWaiters() {
if (this.moveWaitWakeups.length === 0) return;
const pending = this.moveWaitWakeups;
this.moveWaitWakeups = [];
for (const fn of pending) fn();
}
private async moveCoroutine(n: number, isJump: boolean) {
const epoch = this.resetEpoch;
await this.waitUntil(() => !this.moveWait || epoch !== this.resetEpoch);
if (epoch !== this.resetEpoch) return;
const toFront = n > 0;
this.step = Math.abs(n);
const gm = GameManager.instance!;
while (this.step > 0 && gm.gameState === GameState.Run) {
if (epoch !== this.resetEpoch) return;
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);
this.resolveStepMoveSpeed();
if (this.step === 0) this.onLastMoveStepReady();
await this.waitUntil(() => !this.moveWait || epoch !== this.resetEpoch);
if (epoch !== this.resetEpoch) return;
await this.waitMoveVisualReady();
if (epoch !== this.resetEpoch) return;
} else {
this.playMoveAnim();
this.moveState = MoveState.Moving;
// 本步不可走但步数已消耗:须回传 processData / processVehicleData
if (this.step === 0) {
this.moveState = MoveState.Idle;
this.onMoveToTarget();
this.applyDrawOrderAfterMove();
}
}
}
}
private async rotateCoroutine(n: number) {
const epoch = this.resetEpoch;
await this.waitUntil(() => !this.moveWait || epoch !== this.resetEpoch);
if (epoch !== this.resetEpoch) return;
this.moveWait = true;
this.setDirection(addDirection(this.direction, n));
this.moveWait = false;
this.onRotateComplete();
// 对齐 Unity RotateCoroutine → OnMoveToTarget → ExternalCallScratch/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));
}
}