Files
cocos/assets/scripts/gameplay/Movement.ts
刘宇飞 d393302388 Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:30:58 +08:00

381 lines
13 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 { 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();
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<void>) {
this.queue = this.queue.then(fn).catch((e) => console.error(e));
}
private waitUntil(cond: () => boolean): Promise<void> {
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 → 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));
}
}