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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
157
assets/scripts/controller/RideLink.ts
Normal file
157
assets/scripts/controller/RideLink.ts
Normal 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);
|
||||
}
|
||||
9
assets/scripts/controller/RideLink.ts.meta
Normal file
9
assets/scripts/controller/RideLink.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7152704a-c09b-4ea6-a8e5-291537bc7984",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
136
assets/scripts/controller/ViewController.ts
Normal file
136
assets/scripts/controller/ViewController.ts
Normal 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 ViewController:Orthographic 缩放与拖拽 */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
9
assets/scripts/controller/ViewController.ts.meta
Normal file
9
assets/scripts/controller/ViewController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e7bb6bb7-3908-413f-9664-06267b8e39fd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user