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,11 +1,25 @@
import { _decorator, Vec3 } from 'cc';
import { Direction, GameState, GridType, Skin } from '../core/Define';
import { _decorator, AudioSource, Vec3 } from 'cc';
import { Direction, GameState, GridType, MoveState, MoverRole, Skin, skinToTheme, themeToSkin } from '../core/Define';
import { EventManager, EventType } from '../core/EventManager';
import { JsBridge } from '../bridge/JsBridge';
import { GameAudio } from '../audio/GameAudio';
import { GameManager } from '../manager/GameManager';
import { Movement } from '../gameplay/Movement';
import { vehicleFollowsLandingTile, canStayMountedAfterLanding, moverRoleForLandingCell, shouldDismountOnTile } from '../gameplay/MoveRules';
import { VehicleController } from './VehicleController';
import { VisualAssets } from '../visual/VisualAssets';
import { PropController } from './PropController';
import { findVehicleAtCell, tryLinkPlayerVehicle } from './RideLink';
import { PlayerAction } from '../visual/PlayerAnimPaths';
import { PlayerActionAnimator } from '../visual/PlayerActionAnimator';
import { scaledJumpArcOffset, scaledMoveSpeed, UNITY_PLAYER_MOVE_SPEED, CELL_PIXEL, PROP_COLLECT_MOVE_PROGRESS, PROP_COLLECT_TOUCH_RADIUS } from '../core/GridConstants';
import { cellToWorldCenter } from '../core/GridCoords';
import { getThemePlayerRideYOffset } from '../theme/ThemeDatabase';
import {
entityWorldPositionForRole,
moverLogicalGridPositionForRole,
worldToMoverCellForRole,
} from '../level/EntitySpawnPlacement';
import { forEachLevelEntityNode } from '../level/TileLayout';
const { ccclass } = _decorator;
@@ -27,14 +41,26 @@ export interface ExternalResult {
isInputEnd: boolean;
}
/**
* 玩家移动(对齐 Unity PlayerController
* - 玩家节点为位移主体
* - 骑乘:仅目标空地联动载具;落砖下车;视觉 Y 抬高
* - 跳跃:只能落到 Jump 砖MoveRules + moveCondition
*/
@ccclass('PlayerController')
export class PlayerController extends Movement {
coins = 0;
private vehicle: VehicleController | null = null;
private sendFinally = false;
private animator: PlayerActionAnimator | null = null;
private sfxSource: AudioSource | null = null;
onLoad() {
this.moverRole = 'player';
this.moveSpeed = scaledMoveSpeed(UNITY_PLAYER_MOVE_SPEED);
this.animator = this.node.getComponent(PlayerActionAnimator)
?? this.node.addComponent(PlayerActionAnimator);
this.sfxSource = this.node.getComponent(AudioSource) ?? this.node.addComponent(AudioSource);
EventManager.register(EventType.LevelInit, this.onLevelInit);
EventManager.register(EventType.InputEnd, this.onInputEnd);
this.coins = 0;
@@ -47,72 +73,452 @@ export class PlayerController extends Movement {
start() {
super.start();
VisualAssets.applyPlayerSprite(this.node, this.direction);
if (this.node.name === 'Player' && GameManager.instance) {
this.callChangeSkin(GameManager.instance.playerSkin);
} else {
this.animator?.setAction(this.animator?.getAction() ?? PlayerAction.Idle);
}
}
override setDirection(dir: Direction) {
super.setDirection(dir);
VisualAssets.applyPlayerSprite(this.node, dir);
// —— 骑乘视觉(逻辑坐标在载具甲板,显示抬高) ——
private rideYOffset(): number {
const gm = GameManager.instance;
const theme = gm?.getCurLevel()?.theme ?? gm?.uiStyle;
return getThemePlayerRideYOffset(theme);
}
private rideDeckAtCell(cell: Vec3): Vec3 {
const gm = GameManager.instance!;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
return entityWorldPositionForRole(cell, config ?? undefined, theme, 'vehicle');
}
/** 步行上载具时的落点(甲板 + 骑乘视觉 Y避免到格后再闪一下 */
private rideMountWorldPosition(cell: Vec3): Vec3 {
const deck = this.rideDeckAtCell(cell);
return new Vec3(deck.x, deck.y + this.rideYOffset(), deck.z);
}
private applyMountVisualAtDeck(deck: Vec3) {
this.node.setPosition(deck.x, deck.y + this.rideYOffset(), deck.z);
}
/** 骑乘时逻辑甲板世界坐标(无视觉抬高) */
getRideDeckWorldPosition(): Vec3 {
if (this.vehicle) return this.vehicle.node.position.clone();
const p = this.node.position;
return new Vec3(p.x, p.y, p.z);
}
/** 是否处于「载具跟随」状态(仅 Idle 时锁定到甲板;移动中由 OnMoving 驱动载具) */
private isVehicleLinked(): boolean {
return !!this.vehicle && this.moveState !== MoveState.Moving;
}
snapToRideBase() {
if (!this.vehicle) return;
this.applyMountVisualAtDeck(this.vehicle.node.position);
}
applyRideVisual() {
if (!this.vehicle) return;
this.applyMountVisualAtDeck(this.vehicle.node.position);
}
/** 载具换向/flip 后重对齐骑乘高度 */
syncRideMountVisual() {
this.applyRideVisual();
}
setRideBaseFromVehicle(base: Vec3) {
this.node.setPosition(base);
this.applyRideVisual();
}
private syncVehicleToPlayerBase() {
if (!this.vehicle) return;
const p = this.node.position;
const off = this.rideYOffset();
this.vehicle.node.setPosition(p.x, p.y - off, p.z);
}
// —— 格子采样:骑乘期间始终以载具甲板为逻辑位置 ——
protected override getGridSamplePosition(): Vec3 {
// 优先 committedCell空地格 Y 补偿会让 worldToMoverCell 误判邻格(如 (-1,-1)→(0,0)
if (this.committedCell) {
return cellToWorldCenter(this.committedCell);
}
const gm = GameManager.instance!;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
if (this.vehicle) {
return moverLogicalGridPositionForRole(
this.vehicle.node.position,
config ?? undefined,
theme,
'vehicle',
);
}
return moverLogicalGridPositionForRole(
this.node.position,
config ?? undefined,
theme,
'player',
);
}
update(dt: number) {
if (this.isVehicleLinked()) {
this.applyMountVisualAtDeck(this.vehicle!.node.position);
}
super.update(dt);
}
protected override shouldApplyPlayerStandOffset(): boolean {
if (!this.vehicle) return true;
if (this.moveState !== MoveState.Moving) return false;
return !vehicleFollowsLandingTile(this.targetGridType);
}
setRideVehicle(v: VehicleController | null) {
if (this.vehicle && this.vehicle !== v) {
this.vehicle.setPlayer(null);
}
this.vehicle = v;
if (v) {
v.setPlayer(this);
Movement.callEach = true;
v.setDirection(this.direction);
Movement.callEach = false;
}
}
getRideVehicle(): VehicleController | null {
return this.vehicle;
}
// —— 上下车 ——
private unbindVehicle() {
if (!this.vehicle) return;
this.vehicle.setPlayer(null);
this.vehicle = null;
}
private alignWithVehicleBase() {
if (!this.vehicle || !this.committedCell) return;
const deck = this.rideDeckAtCell(this.committedCell);
this.vehicle.node.setPosition(deck);
this.vehicle.shareCommittedCell(this.committedCell);
this.node.setPosition(deck);
this.applyRideVisual();
}
/** Unity CheckIfCurIsRide仅脚下为 Ride 动态格时绑定载具 */
private checkIfCurIsRide() {
if (this.curGrid !== GridType.Ride || !this.committedCell) return;
const gm = GameManager.instance!;
const vc = findVehicleAtCell(this.committedCell)
?? gm.getGameObjectAtCell(this.committedCell)?.getComponent(VehicleController);
if (!vc) return;
this.setRideVehicle(vc);
vc.setPlayer(this);
vc.setDirection(this.direction);
this.alignWithVehicleBase();
}
private onLevelInit = () => {
this.checkRide();
this.sendFinally = false;
this.coins = 0;
this.resetMoveRuntime();
this.unbindVehicle();
this.refreshVisual(PlayerAction.Idle);
this.syncCommittedCellFromPosition();
this.snapMoverToCellStand();
this.checkIfCurIsRide();
tryLinkPlayerVehicle(this);
};
private onInputEnd = () => {
this.externalCallResult(false);
const gm = GameManager.instance!;
this.externalCallResult(gm.allPropsCollected());
};
protected override snapMoverToCellStand() {
const gm = GameManager.instance;
if (!gm || !this.committedCell) return;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
const landingLive = gm.calculateGridTypeAtCell(this.committedCell);
if (canStayMountedAfterLanding(landingLive)) {
const deck = this.rideDeckAtCell(this.committedCell);
if (this.vehicle) {
this.vehicle.node.setPosition(deck);
this.vehicle.shareCommittedCell(this.committedCell);
const mount = this.rideMountWorldPosition(this.committedCell);
this.node.setPosition(mount);
this.targetPosition.set(mount);
} else {
const mount = this.rideMountWorldPosition(this.committedCell);
this.node.setPosition(mount);
this.targetPosition.set(mount);
}
return;
}
const follow = !!this.vehicle && vehicleFollowsLandingTile(landingLive);
const deck = entityWorldPositionForRole(this.committedCell, config ?? undefined, theme, 'vehicle');
if (follow) {
this.vehicle!.node.setPosition(deck);
this.vehicle!.shareCommittedCell(this.committedCell);
const mount = this.rideMountWorldPosition(this.committedCell);
this.node.setPosition(mount);
this.targetPosition.set(mount);
return;
}
const pos = entityWorldPositionForRole(this.committedCell, config ?? undefined, theme, 'player');
this.node.setPosition(pos);
this.targetPosition.set(pos);
}
protected override resolveMoveStepOrigin(_targetCell: Vec3, landingGrid: GridType): Vec3 {
if (this.vehicle && vehicleFollowsLandingTile(landingGrid) && this.committedCell) {
return this.rideMountWorldPosition(this.committedCell);
}
// 下车:从骑乘视觉高度平滑移到砖面,勿先 snap 到甲板
if (this.vehicle && shouldDismountOnTile(landingGrid) && this.committedCell) {
return this.rideMountWorldPosition(this.committedCell);
}
return this.node.position.clone();
}
protected override resolveTargetWorldPosition(targetCell: Vec3, landingGrid: GridType): Vec3 {
if (this.vehicle && vehicleFollowsLandingTile(landingGrid)) {
return this.rideMountWorldPosition(targetCell);
}
const gm = GameManager.instance!;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
const role: MoverRole = moverRoleForLandingCell('player', landingGrid, !!this.vehicle);
if (!this.vehicle && landingGrid === GridType.Ride) {
return this.rideMountWorldPosition(targetCell);
}
return entityWorldPositionForRole(targetCell, config ?? undefined, theme, role);
}
protected override onRotateComplete() {
if (this.vehicle) {
this.vehicle.setDirection(this.direction);
if (this.committedCell) this.alignWithVehicleBase();
}
}
/** 是否应拾取该道具(移动中需进入格子且接近触碰,避免起步即消失) */
canCollectProp(prop: PropController): boolean {
const propCell = prop.getSpawnCell();
if (!propCell) return false;
const committed = this.getCommittedCell();
if (!this.isMoving()) {
return !!committed && prop.matchesCell(committed);
}
const landing = this.getLandingCell();
if (!landing || !prop.matchesCell(landing)) return false;
const gm = GameManager.instance!;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
const sample = worldToMoverCellForRole(this.node.position, config ?? undefined, theme, 'player');
if (!prop.matchesCell(sample)) return false;
if (this.getMoveStepProgress() < PROP_COLLECT_MOVE_PROGRESS) return false;
const touchR = CELL_PIXEL * PROP_COLLECT_TOUCH_RADIUS;
return Vec3.distance(this.node.position, prop.node.position) <= touchR;
}
private collectPropsOnApproach() {
const gm = GameManager.instance;
const level = gm?.curLevel;
if (!level) return;
forEachLevelEntityNode(level, (ch) => {
const prop = ch.getComponent(PropController);
if (!prop || !this.canCollectProp(prop)) return;
prop.collect(this);
});
}
protected onMoveNextSet(isJump: boolean) {
if (isJump && this.targetGridType === GridType.Jump && this.vehicle) {
this.unbindVehicle();
}
this.animator?.setAction(isJump ? PlayerAction.Jump : PlayerAction.Move);
if (isJump && this.targetGridType === GridType.Jump) {
const p = this.node.worldPosition.clone();
p.y += 0.15;
this.targetPosition.set(p);
this.targetPosition.y += scaledJumpArcOffset();
}
}
protected override playMoveAnim() {
this.animator?.setAction(PlayerAction.Move);
}
protected override syncRideAfterMoveStep() {
if (this.vehicle && vehicleFollowsLandingTile(this.targetGridType)) {
this.syncVehicleToPlayerBase();
}
}
onMoving() {
if (this.targetGridType === GridType.None && this.vehicle && !Movement.callEach) {
this.vehicle.setPosition(this.node.worldPosition);
if (this.vehicle && vehicleFollowsLandingTile(this.targetGridType) && !Movement.callEach) {
this.syncVehicleToPlayerBase();
}
this.collectPropsOnApproach();
if (!this.sfxSource) return;
const action = this.animator?.getAction() ?? PlayerAction.Idle;
if (action === PlayerAction.Move) {
const key = vehicleFollowsLandingTile(this.targetGridType) && this.vehicle
? 'vehicleMove' : 'move';
void GameAudio.playSfxOnSource(this.sfxSource, key);
} else if (action === PlayerAction.Jump) {
void GameAudio.playSfxOnSource(this.sfxSource, 'jump');
}
}
protected onMoveFail(isJump: boolean) {
this.externalCallResult(false);
const gm = GameManager.instance!;
if (gm.multMode) {
const other = gm.findNodeByName(this.node.name === 'Player' ? 'Enemy' : 'Player');
other?.getComponent(PlayerController)?.externalCallResult(true);
if (isJump) {
console.log(
`${this.node.name} 无法跳跃:跳跃只能落到 Jump 砖块(当前=${GridType[this.curGrid]} 前方=${GridType[this.nextGrid]}`,
);
} else if (this.curGrid === GridType.Jump) {
console.log(`${this.node.name} 在跳跃砖块上只能跳跃或转向,不能普通前后移动`);
} else {
console.log(
`${this.node.name} 无法移动(当前=${GridType[this.curGrid]} 前方=${GridType[this.nextGrid]} 后方=${GridType[this.lastGrid]}`,
);
}
console.log(`${this.node.name} 无法移动`, isJump);
}
protected onMoveToTarget() {
this.commitLandingCell();
this.finishMoveToTarget(true);
}
/** Unity OnMoveToTarget(bool) — 上下车 / 载具 Ride 注册 */
finishMoveToTarget(sendMsgOnStep0 = true) {
const gm = GameManager.instance!;
this.applyResultAction();
const cell = this.committedCell
?? worldToMoverCellForRole(
this.node.position,
gm.getCurLevel() ?? undefined,
gm.getCurLevel()?.theme ?? gm.uiStyle,
'player',
);
if (this.curGrid === GridType.Ride) {
const obj = GameManager.instance!.getGameObject(this.node.worldPosition);
if (obj) {
this.vehicle = obj.getComponent(VehicleController);
this.vehicle?.setPlayer(this);
if (this.vehicle) this.vehicle.setDirection(this.direction);
const vc = findVehicleAtCell(cell)
?? gm.getGameObjectAtCell(cell)?.getComponent(VehicleController);
if (vc) {
this.setRideVehicle(vc);
vc.setPlayer(this);
vc.setDirection(this.direction);
}
} else if (this.curGrid !== GridType.None && this.vehicle) {
this.vehicle.setPlayer(null);
this.vehicle = null;
this.unbindVehicle();
}
PropController.tryCollectAtCell(cell, this);
if (this.vehicle) {
this.vehicle.setPosition(this.node.worldPosition);
GameManager.instance!.removeObj(this.lastPosition);
GameManager.instance!.addObj(this.node.worldPosition, GridType.Ride, this.vehicle.node);
this.alignWithVehicleBase();
gm.removeObj(this.lastPosition);
const rideCell = this.committedCell ?? cell;
gm.addObjAtCell(rideCell, GridType.Ride, this.vehicle.node);
}
this.externalCall();
if (this.committedCell) {
this.targetGridType = gm.calculateGridTypeAtCell(this.committedCell);
}
if (this.step === 0 && sendMsgOnStep0) {
this.externalCall();
}
}
// —— 外观 / 外部接口 ——
private activeVisualTheme(): string {
const gm = GameManager.instance;
const levelTheme = gm?.getCurLevel()?.theme?.trim();
if (levelTheme) return levelTheme;
if (gm) return skinToTheme(gm.playerSkin);
return 'silu';
}
private visualOpts() {
const gm = GameManager.instance;
const theme = this.activeVisualTheme();
return { ...(gm?.getEntityVisualOptions() ?? {}), theme };
}
private refreshVisual(action = PlayerAction.Idle) {
this.animator?.configure(this.activeVisualTheme(), this.direction);
this.animator?.setAction(action);
}
/** 关卡加载后强制用地图主题(主站 CallChangeSkin 不得覆盖) */
applyLevelTheme(theme: string) {
const t = theme?.trim();
if (!t) return;
const gm = GameManager.instance;
if (gm) gm.playerSkin = themeToSkin(t);
this.animator?.configure(t, this.direction);
this.animator?.setAction(this.animator?.getAction() ?? PlayerAction.Idle, true);
this.reapplyCellStandPosition();
this.vehicle?.refreshIcon();
}
override setDirection(dir: Direction) {
super.setDirection(dir);
this.animator?.setDirection(dir, this.visualOpts());
if (Movement.callEach) return;
Movement.callEach = true;
this.vehicle?.setDirection(dir);
Movement.callEach = false;
}
callChangeSkin(n: number) {
if (n < 0 || n > Skin.sanxing) return;
if (GameManager.instance) GameManager.instance.playerSkin = n as Skin;
const levelTheme = GameManager.instance?.getCurLevel()?.theme?.trim();
if (levelTheme) {
this.applyLevelTheme(levelTheme);
return;
}
this.refreshVisual(this.animator?.getAction() ?? PlayerAction.Idle);
}
playCoinsAudio() {
GameAudio.playSfx('coins', this.node);
}
private applyResultAction() {
const gm = GameManager.instance;
if (!gm) return;
if (gm.gameState === GameState.ResultWin) {
this.animator?.setAction(PlayerAction.Win);
} else if (gm.gameState === GameState.ResultFail) {
this.animator?.setAction(PlayerAction.Fail);
} else {
this.animator?.setAction(PlayerAction.Idle);
}
}
callPlayerInfo() {
@@ -129,85 +535,73 @@ export class PlayerController extends Movement {
addCoins() {
this.coins++;
this.playCoinsAudio();
}
setPosition(pos: Vec3) {
this.node.setPosition(pos);
if (this.vehicle) {
this.setRideBaseFromVehicle(pos);
} else {
this.node.setPosition(pos);
}
}
setTargetGridType(t: GridType) {
this.targetGridType = t;
}
/** 供 VehicleController 同步 */
syncFromVehicle(targetGridType: GridType, isJump: boolean) {
this.targetGridType = targetGridType;
this.onMoveNextSet(isJump);
}
syncMoveToTargetFromVehicle() {
this.onMoveToTarget();
this.finishMoveToTarget(false);
}
externalCall() {
const gm = GameManager.instance!;
const list: ExternalDataList = { direction: this.direction, externalDatas: [] };
const self = gm.worldToCell(this.node.worldPosition);
const sample = this.getGridSamplePosition();
const self = gm.worldToCell(sample);
list.externalDatas.push({
position: { x: self.x, y: self.y, z: 0 },
gridType: this.curGrid,
direction: 'self',
});
for (let d = Direction.North; d <= Direction.West; d++) {
const wp = gm.nextGridPosition(this.node.worldPosition, d);
const wp = gm.nextGridPosition(sample, d);
const cell = gm.worldToCell(wp);
list.externalDatas.push({
position: { x: cell.x, y: cell.y, z: 0 },
gridType: gm.calculateNextGridType(this.node.worldPosition, d),
gridType: gm.calculateNextGridType(sample, d),
direction: gm.getRelativePosition(this.direction, d),
});
}
const json = JSON.stringify(list);
if (gm.multMode) JsBridge.call(`process${this.node.name}`, json);
else JsBridge.call('processData', json);
JsBridge.call('processData', JSON.stringify(list));
}
externalCallResult(isWin: boolean) {
const gm = GameManager.instance!;
if (isWin && (this.node.name === 'Player' || this.node.name === gm.multPlayerRole)) {
/* success audio */
} else if (!isWin && (this.node.name === 'Player' || this.node.name === gm.multPlayerRole)) {
/* fail audio */
}
let myStep = gm.stepNum;
if (gm.multMode) {
switch (gm.multPlayerRole) {
case 'PlayerA1': myStep = gm.stepA1Num; break;
case 'PlayerA2': myStep = gm.stepA2Num; break;
case 'PlayerA3': myStep = gm.stepA3Num; break;
case 'PlayerB1': myStep = gm.stepB1Num; break;
case 'PlayerB2': myStep = gm.stepB2Num; break;
case 'PlayerB3': myStep = gm.stepB3Num; break;
if (this.node.name === 'Player') {
if (isWin) {
GameAudio.playSfx('success', this.node);
this.animator?.setAction(PlayerAction.Win);
} else {
GameAudio.playSfx('fail', this.node);
this.animator?.setAction(PlayerAction.Fail);
}
}
if ((this.node.name === 'Player' || this.node.name === gm.multPlayerRole) && !this.sendFinally) {
const payload: ExternalResult = {
if (this.node.name === 'Player' && !this.sendFinally) {
JsBridge.call('externalResult', JSON.stringify({
isWin,
stepNum: myStep,
stepNum: gm.stepNum,
direction: this.direction,
isInputEnd: gm.isInputEnd,
};
JsBridge.call('externalResult', JSON.stringify(payload));
} satisfies ExternalResult));
this.sendFinally = true;
}
if (gm.gameState !== GameState.Run) return;
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail);
}
private checkRide() {
if (this.curGrid === GridType.Ride) {
const obj = GameManager.instance!.getGameObject(this.node.worldPosition);
if (obj) {
this.vehicle = obj.getComponent(VehicleController);
this.vehicle?.setDirection(this.direction);
this.vehicle?.setPlayer(this);
}
}
}
}

View File

@@ -1,59 +1,75 @@
import { _decorator, Component, Vec3 } from 'cc';
import { GameManager } from '../manager/GameManager';
import { forEachLevelEntityNode } from '../level/TileLayout';
import { PlayerController } from './PlayerController';
const { ccclass } = _decorator;
/**
* 可拾取物(对齐 Unity PropController OnTriggerEnter2D
* 玩家进入道具格且靠近时拾取(由 PlayerController 判定时机)
*/
@ccclass('PropController')
export class PropController extends Component {
private collected = false;
private spawnCell: Vec3 | null = null;
setSpawnCell(cell: Vec3) {
this.spawnCell = cell.clone();
}
getSpawnCell(): Vec3 | null {
return this.spawnCell ? this.spawnCell.clone() : null;
}
matchesCell(cell: Vec3): boolean {
const propCell = this.getLogicCell();
return propCell.x === cell.x && propCell.y === cell.y;
}
private getLogicCell(): Vec3 {
if (this.spawnCell) return this.spawnCell;
const gm = GameManager.instance!;
return gm.worldToCell(this.node.position);
}
/** 玩家落格后尝试拾取(由 PlayerController 调用) */
static tryCollectAtCell(cell: Vec3, player: PlayerController) {
const gm = GameManager.instance;
const level = gm?.curLevel;
if (!level) return;
forEachLevelEntityNode(level, (ch) => {
const prop = ch.getComponent(PropController);
if (!prop || prop.collected || !prop.matchesCell(cell)) return;
prop.collect(player);
});
}
update() {
if (this.collected || !GameManager.instance) return;
const gm = GameManager.instance;
const players = gm.curLevel?.children.filter((c) => c.name.includes('Player')) ?? [];
const propCell = gm.worldToCell(this.node.worldPosition);
for (const p of players) {
const pc = p.getComponent(PlayerController);
if (!pc) continue;
const pcCell = gm.worldToCell(p.worldPosition);
if (pcCell.x !== propCell.x || pcCell.y !== propCell.y) continue;
this.onCollected(pc);
break;
}
const player = GameManager.instance.findNodeByName('Player');
const pc = player?.getComponent(PlayerController);
if (!pc?.canCollectProp(this)) return;
this.collect(pc);
}
collect(player: PlayerController) {
if (this.collected) return;
this.onCollected(player);
}
private onCollected(player: PlayerController) {
if (this.collected) return;
this.collected = true;
this.node.active = false;
const gm = GameManager.instance!;
player.addCoins();
gm.removeProp(this.node.worldPosition);
const remaining = (gm.curLevel?.children.filter((c) => c.name.includes('Prop') && c !== this.node).length ?? 0);
const propCell = this.getLogicCell();
gm.removePropAtCell(propCell);
this.node.removeFromParent();
this.node.destroy();
if (remaining === 0) {
if (gm.multMode) this.resolveMultWin(gm);
else player.externalCallResult(true);
}
}
private resolveMultWin(gm: GameManager) {
const sum = (names: string[]) =>
names.reduce((t, n) => t + (gm.findNodeByName(n)?.getComponent(PlayerController)?.coins ?? 0), 0);
const totalA = sum(['PlayerA1', 'PlayerA2', 'PlayerA3']);
const totalB = sum(['PlayerB1', 'PlayerB2', 'PlayerB3']);
const winA = totalA > totalB || (totalA === totalB && gm.stepA1Num + gm.stepA2Num + gm.stepA3Num <
gm.stepB1Num + gm.stepB2Num + gm.stepB3Num);
const set = (names: string[], win: boolean) => {
for (const n of names) gm.findNodeByName(n)?.getComponent(PlayerController)?.externalCallResult(win);
};
if (winA) {
set(['PlayerA1', 'PlayerA2', 'PlayerA3'], true);
set(['PlayerB1', 'PlayerB2', 'PlayerB3'], false);
} else {
set(['PlayerA1', 'PlayerA2', 'PlayerA3'], false);
set(['PlayerB1', 'PlayerB2', 'PlayerB3'], true);
if (gm.allPropsCollected()) {
player.externalCallResult(true);
}
}
}

View File

@@ -0,0 +1,157 @@
import { Vec3 } from 'cc';
import { GridType } from '../core/Define';
import { GameManager } from '../manager/GameManager';
import { forEachLevelEntityNode } from '../level/TileLayout';
import { entityWorldPositionForRole, worldToMoverCellForRole } from '../level/EntitySpawnPlacement';
import { PlayerController } from './PlayerController';
import { VehicleController } from './VehicleController';
/** 按逻辑格查找载具gridTypes + 当前 committedCell */
export function findVehicleAtCell(cell: Vec3): VehicleController | null {
const gm = GameManager.instance;
if (!gm) return null;
const fromGrid = gm.getGameObjectAtCell(cell)?.getComponent(VehicleController);
if (fromGrid) return fromGrid;
const level = gm.curLevel;
if (!level) return null;
let found: VehicleController | null = null;
forEachLevelEntityNode(level, (ch) => {
if (found || !ch.name.includes('Vehicle')) return;
const vc = ch.getComponent(VehicleController);
if (!vc || !vehicleMatchesCell(vc, cell)) return;
found = vc;
});
return found;
}
/** 双向绑定玩家与载具,并确保 Ride 格注册 */
export function linkRidePair(player: PlayerController, vehicle: VehicleController, cell: Vec3) {
const gm = GameManager.instance;
if (!gm) return;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
const deck = entityWorldPositionForRole(cell, config ?? undefined, theme, 'vehicle');
player.setRideVehicle(vehicle);
vehicle.setPlayer(player);
vehicle.setDirection(player.direction);
player.shareCommittedCell(cell);
vehicle.shareCommittedCell(cell);
vehicle.node.setPosition(deck);
gm.addObjAtCell(cell, GridType.Ride, vehicle.node);
player.setRideBaseFromVehicle(deck);
}
function playerLogicCell(player: PlayerController): Vec3 {
const committed = player.getCommittedCell();
if (committed) return committed.clone();
const gm = GameManager.instance!;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
return worldToMoverCellForRole(
player.node.position,
config ?? undefined,
theme,
'player',
);
}
function vehicleLogicCell(vehicle: VehicleController): Vec3 | null {
const committed = vehicle.getCommittedCell();
if (committed) return committed.clone();
const spawn = vehicle.getSpawnCell() ?? vehicle.getCommittedCell();
if (spawn) return spawn.clone();
const gm = GameManager.instance;
if (!gm) return null;
const config = gm.getCurLevel();
const theme = config?.theme ?? gm.uiStyle;
return worldToMoverCellForRole(vehicle.node.position, config ?? undefined, theme, 'vehicle');
}
function vehicleMatchesCell(vehicle: VehicleController, cell: Vec3): boolean {
const vCell = vehicleLogicCell(vehicle);
return !!vCell && vCell.x === cell.x && vCell.y === cell.y;
}
/** 玩家与载具占同一逻辑格时绑定(不依赖 Ride 格是否已注册) */
export function tryLinkPlayerVehicle(player: PlayerController): boolean {
const gm = GameManager.instance;
const level = gm?.curLevel;
if (!level) return false;
const pCell = playerLogicCell(player);
if (!pCell) return false;
const existing = player.getRideVehicle();
if (existing) {
const vCell = vehicleLogicCell(existing);
if (!vCell || vCell.x !== pCell.x || vCell.y !== pCell.y) {
existing.setPlayer(null);
player.setRideVehicle(null);
return false;
}
linkRidePair(player, existing, pCell);
return true;
}
let linked = false;
forEachLevelEntityNode(level, (ch) => {
if (linked || !ch.name.includes('Vehicle')) return;
const vc = ch.getComponent(VehicleController);
if (!vc || !vehicleMatchesCell(vc, pCell)) return;
linkRidePair(player, vc, pCell);
linked = true;
});
if (linked) return true;
const rideVc = findVehicleAtCell(pCell);
if (rideVc) {
linkRidePair(player, rideVc, pCell);
return true;
}
return false;
}
/** 载具侧:同格玩家自动设为骑手 */
export function tryLinkVehicleRider(vehicle: VehicleController): boolean {
const gm = GameManager.instance;
const level = gm?.curLevel;
if (!level) return false;
const vCell = vehicleLogicCell(vehicle);
if (!vCell) return false;
const rider = vehicle.getPlayer();
if (rider) {
const pCell = playerLogicCell(rider);
if (pCell && pCell.x === vCell.x && pCell.y === vCell.y) {
linkRidePair(rider, vehicle, vCell);
return true;
}
}
let matched = false;
forEachLevelEntityNode(level, (ch) => {
if (matched || !ch.name.includes('Player')) return;
const pc = ch.getComponent(PlayerController);
if (!pc) return;
const pCell = playerLogicCell(pc);
if (!pCell || pCell.x !== vCell.x || pCell.y !== vCell.y) return;
linkRidePair(pc, vehicle, vCell);
matched = true;
});
return matched;
}
/** 移动中强制位置联动Unity OnMoving 语义) */
export function syncRidePositions(driver: 'player' | 'vehicle', player: PlayerController, vehicle: VehicleController) {
if (driver === 'player') {
const p = player.node.position;
vehicle.node.setPosition(p.x, p.y, p.z);
} else {
player.setRideBaseFromVehicle(vehicle.node.position);
}
}
export function resolveRideBind(player: PlayerController, cell: Vec3 | null): VehicleController | null {
if (!cell) return null;
return findVehicleAtCell(cell);
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7152704a-c09b-4ea6-a8e5-291537bc7984",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -3,36 +3,57 @@ import { Direction, GridType } from '../core/Define';
import { EventManager, EventType } from '../core/EventManager';
import { JsBridge } from '../bridge/JsBridge';
import { GameManager } from '../manager/GameManager';
import { scaledMoveSpeed, UNITY_VEHICLE_MOVE_SPEED } from '../core/GridConstants';
import { Movement } from '../gameplay/Movement';
import { PlayerController, ExternalDataList } from './PlayerController';
import { syncRidePositions } from './RideLink';
import { VisualAssets } from '../visual/VisualAssets';
const { ccclass } = _decorator;
/**
* 载具移动(对齐 Unity VehicleController
* - 仅能在空地移动None↔None、Ride→None
* - 有骑手时每帧同步玩家;落格更新 Ride 注册
*/
@ccclass('VehicleController')
export class VehicleController extends Movement {
private player: PlayerController | null = null;
onLoad() {
this.moverRole = 'vehicle';
this.moveSpeed = scaledMoveSpeed(UNITY_VEHICLE_MOVE_SPEED);
this.moveState = 0;
}
/** 不调用 super.start(),避免 component.start 晚于 LevelInit 绑定后再次用 spawn 方向刷贴图 */
start() {
super.start();
this.syncCommittedCellFromPosition();
if (this.player) {
this.syncFacingFromRider();
}
this.setIcon();
}
setPlayer(p: PlayerController | null) {
this.player = p;
if (!p) return;
if (p.direction !== this.direction) {
super.setDirection(p.direction);
}
this.setIcon();
}
getPlayer(): PlayerController | null {
return this.player;
}
setPosition(pos: Vec3) {
this.node.setPosition(pos);
}
setName(name: string) {
/* 可挂 Label 显示名称 */
setName(_name: string) {
/* 可挂 Label */
}
override setDirection(dir: Direction) {
@@ -44,45 +65,95 @@ export class VehicleController extends Movement {
Movement.callEach = false;
}
private entityVisualOptions() {
const gm = GameManager.instance;
const levelTheme = gm?.getCurLevel()?.theme?.trim();
return {
...(gm?.getEntityVisualOptions() ?? {}),
theme: levelTheme ?? gm?.uiStyle ?? 'default',
};
}
/** 载具四向贴图:有骑手时跟随骑手 direction转向时 setIcon 直接换图 */
private iconDirection(): Direction {
return this.player?.direction ?? this.direction;
}
setIcon() {
const style = GameManager.instance?.uiStyle ?? 'default';
VisualAssets.applyVehicleSprite(this.node, this.direction, style);
const dir = this.iconDirection();
if (this.player && dir !== this.direction) {
super.setDirection(dir);
}
VisualAssets.applyVehicleIconForDirection(
this.node,
dir,
this.entityVisualOptions(),
);
this.player?.syncRideMountVisual();
}
/** 骑乘时逻辑朝向与骑手保持一致Scratch 回传等) */
private syncFacingFromRider() {
if (!this.player || this.player.direction === this.direction) return;
super.setDirection(this.player.direction);
}
/** 主题 / 软重置后按当前逻辑朝向重刷贴图 */
refreshIcon() {
this.syncFacingFromRider();
this.setIcon();
}
override callJump() {
/* 载具不可跳跃 */
}
protected override syncRideAfterMoveStep() {
if (this.player) syncRidePositions('vehicle', this.player, this);
}
protected onMoveNextSet(isJump: boolean) {
if (Movement.callEach) return;
Movement.callEach = true;
this.player?.syncFromVehicle(this.targetGridType, isJump);
if (this.player) {
this.player.setTargetGridType(this.targetGridType);
this.player.syncFromVehicle(this.targetGridType, isJump);
}
Movement.callEach = false;
}
protected onMoving() {
if (this.player) syncRidePositions('vehicle', this.player, this);
if (Movement.callEach) return;
Movement.callEach = true;
if (this.player) {
this.player.onMoving();
this.player.setPosition(this.node.worldPosition);
}
this.player?.onMoving();
Movement.callEach = false;
}
protected onMoveToTarget() {
GameManager.instance!.removeObj(this.lastPosition);
GameManager.instance!.addObj(this.node.worldPosition, GridType.Ride, this.node);
if (Movement.callEach) return;
Movement.callEach = true;
if (this.player) {
this.player.setPosition(this.node.worldPosition);
this.player.syncMoveToTargetFromVehicle();
this.commitLandingCell();
const gm = GameManager.instance!;
gm.removeObj(this.lastPosition);
if (this.committedCell) {
gm.addObjAtCell(this.committedCell, GridType.Ride, this.node);
} else {
gm.addObj(this.node.position, GridType.Ride, this.node);
}
Movement.callEach = false;
this.externalCall();
if (!Movement.callEach && this.player) {
Movement.callEach = true;
this.player.setRideBaseFromVehicle(this.node.position);
if (this.committedCell) this.player.shareCommittedCell(this.committedCell);
this.player.syncMoveToTargetFromVehicle();
Movement.callEach = false;
}
if (this.step === 0) this.externalCall();
}
protected onMoveFail(isJump: boolean) {
if (!GameManager.instance!.multMode) {
EventManager.dispatch(EventType.InputEnd, GameManager.instance!.gameState);
}
protected onMoveFail(_isJump: boolean) {
this.externalCall();
EventManager.dispatch(EventType.InputEnd, GameManager.instance!.gameState);
}
callVehicleInfo() {
@@ -92,23 +163,22 @@ export class VehicleController extends Movement {
externalCall() {
const gm = GameManager.instance!;
const list: ExternalDataList = { direction: this.direction, externalDatas: [] };
const self = gm.worldToCell(this.node.worldPosition);
const sample = this.getGridSamplePosition();
const self = gm.worldToCell(sample);
list.externalDatas.push({
position: { x: self.x, y: self.y, z: 0 },
gridType: this.curGrid,
direction: 'self',
});
for (let d = Direction.North; d <= Direction.West; d++) {
const wp = gm.nextGridPosition(this.node.worldPosition, d);
const wp = gm.nextGridPosition(sample, d);
const cell = gm.worldToCell(wp);
list.externalDatas.push({
position: { x: cell.x, y: cell.y, z: 0 },
gridType: gm.calculateNextGridType(this.node.worldPosition, d),
gridType: gm.calculateNextGridType(sample, d),
direction: gm.getRelativePosition(this.direction, d),
});
}
const json = JSON.stringify(list);
if (gm.multMode) JsBridge.call(`process${this.node.name}`, json);
else JsBridge.call('processVehicleData', json);
JsBridge.call('processVehicleData', JSON.stringify(list));
}
}

View File

@@ -0,0 +1,136 @@
import {
_decorator, Camera, Component, EventMouse, EventTouch, Input, input, Vec2, view,
} from 'cc';
import { CAMERA_ORTHO_HALF, DESIGN_WIDTH } from '../core/GridConstants';
import { getEmbeddedOrthoHalf } from '../core/EmbeddedView';
const { ccclass } = _decorator;
/** 对齐 Unity ViewControllerOrthographic 缩放与拖拽 */
@ccclass('ViewController')
export class ViewController extends Component {
static instance: ViewController | null = null;
/** Unity zoomSpeed=2 → 世界半高步进 200 */
zoomSpeed = 200;
/** Unity minZoom=3, maxZoom=10×100 世界单位) */
minOrtho = 300;
maxOrtho = 1000;
private camera: Camera | null = null;
private dragOrigin = new Vec2();
private dragging = false;
onLoad() {
if (ViewController.instance && ViewController.instance !== this) {
this.destroy();
return;
}
ViewController.instance = this;
this.camera = this.getComponent(Camera);
if (this.camera && this.camera.orthoHeight <= 0) {
this.camera.orthoHeight = CAMERA_ORTHO_HALF;
}
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this);
input.on(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
input.on(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
}
onDestroy() {
if (ViewController.instance === this) ViewController.instance = null;
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
input.off(Input.EventType.TOUCH_END, this.onTouchEnd, this);
input.off(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
input.off(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this);
}
zoomIn() {
const cam = this.camera;
if (!cam || cam.orthoHeight <= this.minOrtho) return;
cam.orthoHeight = Math.max(this.minOrtho, cam.orthoHeight - this.zoomSpeed);
}
zoomOut() {
const cam = this.camera;
if (!cam || cam.orthoHeight >= this.maxOrtho) return;
cam.orthoHeight = Math.min(this.maxOrtho, cam.orthoHeight + this.zoomSpeed);
}
resetZoom() {
const cam = this.camera;
if (!cam) return;
cam.orthoHeight = getEmbeddedOrthoHalf();
cam.node.setPosition(0, 0, cam.node.position.z);
}
private onTouchStart(e: EventTouch) {
if (this.isPointerOnUI(e.getLocation())) return;
this.dragging = true;
e.getLocation(this.dragOrigin);
}
private onTouchEnd() {
this.dragging = false;
}
private onTouchMove(e: EventTouch) {
if (!this.dragging) return;
const cur = new Vec2();
e.getLocation(cur);
this.applyDrag(cur);
e.getLocation(this.dragOrigin);
}
private onMouseDown(e: EventMouse) {
if (this.isPointerOnUI(new Vec2(e.getLocationX(), e.getLocationY()))) return;
this.dragging = true;
this.dragOrigin.set(e.getLocationX(), e.getLocationY());
}
private onMouseUp() {
this.dragging = false;
}
private onMouseMove(e: EventMouse) {
if (!this.dragging) return;
this.applyDrag(new Vec2(e.getLocationX(), e.getLocationY()));
this.dragOrigin.set(e.getLocationX(), e.getLocationY());
}
private applyDrag(cur: Vec2) {
if (!this.camera) return;
const delta = cur.subtract(this.dragOrigin);
const ortho = this.camera.orthoHeight;
const { width, height } = view.getVisibleSize();
const worldPerPixelX = (2 * ortho * (width / height)) / width;
const worldPerPixelY = (2 * ortho) / height;
const pos = this.camera.node.position;
let nx = pos.x - delta.x * worldPerPixelX;
let ny = pos.y - delta.y * worldPerPixelY;
const limit = ortho - 20;
if (Math.abs(nx) > limit) nx = pos.x;
if (Math.abs(ny) > limit) ny = pos.y;
this.camera.node.setPosition(nx, ny, pos.z);
}
/** 右侧 UIMain 区域不拖拽镜头(与 UIMain 边距一致) */
private isPointerOnUI(loc: Vec2): boolean {
const vis = view.getVisibleSize();
const margin = Math.max(96, (DESIGN_WIDTH * 0.5) * 0.14);
const right = vis.width > DESIGN_WIDTH
? DESIGN_WIDTH * 0.5
: vis.width * 0.5;
return loc.x >= right - margin;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e7bb6bb7-3908-413f-9664-06267b8e39fd",
"files": [],
"subMetas": {},
"userData": {}
}