Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
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 → 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));
|
||
}
|
||
}
|