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>
This commit is contained in:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

View File

@@ -1,55 +1,223 @@
import { _decorator, Component, Vec3 } from 'cc';
import {
Direction, GameState, GridType, MoveState, MoverRole,
addDirection, getMoveCondition, lookupMove,
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 = 4;
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;
private moveWait = false;
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 {
return GameManager.instance!.calculateGridType(this.node.position);
const gm = GameManager.instance!;
if (this.committedCell) return gm.calculateGridTypeAtCell(this.committedCell);
return gm.calculateGridType(this.getGridSamplePosition());
}
get nextGrid(): GridType {
return GameManager.instance!.calculateNextGridType(this.node.position, this.direction);
const gm = GameManager.instance!;
if (this.committedCell) return gm.calculateNextGridTypeAtCell(this.committedCell, this.direction);
return gm.calculateNextGridType(this.getGridSamplePosition(), this.direction);
}
get lastGrid(): GridType {
return GameManager.instance!.calculateLastGridType(this.node.position, this.direction);
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();
Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt);
const speedMul = GameManager.instance?.getGameSpeed() ?? Movement.speedMultiplier;
Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt * speedMul);
this.node.setPosition(next);
this.onMoving();
if (Vec3.distance(next, this.targetPosition) < 0.01) {
this.syncRideAfterMoveStep();
if (Vec3.distance(next, this.targetPosition) < 0.005) {
this.node.setPosition(this.targetPosition);
this.moveState = MoveState.Idle;
this.moveWait = false;
@@ -57,47 +225,70 @@ export class Movement extends Component {
}
}
protected syncRideAfterMoveStep() {}
setDirection(dir: Direction) {
this.direction = dir;
}
protected onMoving() {}
protected onMoveToTarget() {}
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 targetTmp = gm.nextGridPosition(this.node.position, dir);
const targetType = gm.calculateGridType(targetTmp);
const table = getMoveCondition(gm.multMode);
const nextG = toFront ? this.nextGrid : this.lastGrid;
const v = lookupMove(table, this.moverRole, this.curGrid, nextG, isJump);
if (v !== undefined) {
if (v === 1) {
if (gm.multMode) {
const nextCell = gm.worldToCell(targetTmp);
for (const n of ['PlayerA1', 'PlayerA2', 'PlayerA3', 'PlayerB1', 'PlayerB2', 'PlayerB3']) {
const p = gm.findNodeByName(n);
if (p) {
const c = gm.worldToCell(p.worldPosition);
if (c.x === nextCell.x && c.y === nextCell.y) return 0;
}
}
if (nextG === GridType.Ride) {
const ride = gm.getGameObject(targetTmp);
const expect = this.node.name.replace('Player', 'Vehicle');
if (ride && ride.name !== expect) return 0;
}
}
this.targetPosition.set(targetTmp);
this.targetGridType = targetType;
}
return v;
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;
}
return gm.multMode ? 0 : -1;
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>) {
@@ -128,13 +319,18 @@ export class Movement extends Component {
}
if (r === 1) {
this.moveWait = true;
this.lastPosition.set(this.node.position);
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();
}
}
}
}
@@ -144,14 +340,20 @@ export class Movement extends Component {
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') return gm.jsCallCheck(n);
if (gm.multMode) return gm.jsCallCheckMultMode(n, name);
if (
name === 'Player' || name === 'Vehicle'
|| /^Player[AB]\d$/.test(name) || /^Vehicle[AB]\d$/.test(name)
) {
return gm.jsCallCheck(n);
}
return false;
}