- 移除冗余的方向计算和动画名称获取方法 - 简化移动方向更新逻辑,减少方向查看步数 - 新增垂直方向改变时的动画切换机制 - 优化移动过程中的方向判断和动画更新流程 - 移除不必要的路径点移动方法,简化代码结构
1670 lines
52 KiB
TypeScript
1670 lines
52 KiB
TypeScript
import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween, Animation, Collider2D, BoxCollider2D, Contact2DType, Label, Color, Canvas, UITransform, AudioSource, director, PhysicsSystem2D, EPhysics2DDrawFlags } from 'cc';
|
||
import { TiledMapPathfinder } from './TiledMapPathfinder';
|
||
const { ccclass, property } = _decorator;
|
||
|
||
// PhysicsSystem2D.instance.debugDrawFlags =
|
||
// EPhysics2DDrawFlags.Aabb |
|
||
// EPhysics2DDrawFlags.Shape;
|
||
|
||
enum PlayerDirection {
|
||
LeftUp = 'LeftUp',
|
||
LeftDown = 'LeftDown',
|
||
RightUp = 'RightUp',
|
||
RightDown = 'RightDown',
|
||
}
|
||
|
||
@ccclass('PlayerController')
|
||
export class PlayerController extends Component {
|
||
@property(Canvas)
|
||
canvas: Canvas | null = null;
|
||
|
||
@property(Node)
|
||
player: Node | null = null; // 玩家节点
|
||
|
||
@property(Node)
|
||
bonus: Node | null = null;
|
||
|
||
@property(Node)
|
||
failedDialog: Node | null = null;
|
||
|
||
@property(Camera)
|
||
camera: Camera | null = null; // 主摄像机
|
||
|
||
@property(TiledMapPathfinder)
|
||
pathfinder: TiledMapPathfinder | null = null; // 寻路组件
|
||
|
||
@property(Node)
|
||
attackAudio: Node | null = null; // 攻击音效节点
|
||
|
||
@property({ range: [1, 300] })
|
||
moveSpeed: number = 300; // 移动速度(像素/秒)
|
||
|
||
|
||
private isMoving: boolean = false;
|
||
private isAttacking: boolean = false;
|
||
private currentPath: Vec3[] = [];
|
||
private currentPathIndex: number = 0;
|
||
private originalPosition: Vec3 = new Vec3();
|
||
private currentAnimation: string | null = null; // 当前播放的动画剪辑名称
|
||
private lastTargetPosition: Vec3 = new Vec3(); // 上一个目标位置,用于方向判断
|
||
private isUpgraded: boolean = false; // 玩家是否已升级
|
||
private isGameOver: boolean = false; // 游戏是否结束(玩家死亡)
|
||
private isWin: boolean = false; // 游戏是否胜利(到达终点)
|
||
private currentDirection: PlayerDirection = PlayerDirection.RightDown; // 当前玩家朝向:四象限(左上/左下/右上/右下)
|
||
|
||
// 平滑移动相关变量
|
||
private moveTween: any = null; // 当前移动的tween对象
|
||
private lastPosition: Vec3 = new Vec3(); // 上一帧位置
|
||
|
||
private hasWinTimes = 0
|
||
private readonly attackAlignGap = 20; // 玩家与怪物对阵时的额外左右间距,单位:像素
|
||
private readonly attackVerticalOffset = 25; // 玩家攻击时相对于怪物的垂直偏移量(玩家更高),单位:像素
|
||
|
||
// 道具列表
|
||
private props: Node[] = [];
|
||
|
||
// 行走方向相关参数
|
||
private readonly directionLookAheadSteps = 1; // 更新方向时向前查看的路径节点数
|
||
private readonly minDirectionUpdateDistance = 1; // 小于该距离时不刷新方向,避免抖动
|
||
|
||
private guideNode: Node | null = null;
|
||
private activePopup: Node | null = null;
|
||
private activePopupName: string | null = null;
|
||
private pendingPopupHide: (() => void) | null = null;
|
||
|
||
onLoad() {
|
||
this.guideNode = this.canvas.node.getChildByName('Guide');
|
||
|
||
// 注册触摸事件
|
||
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||
|
||
|
||
let collider = this.player.getComponent(Collider2D);
|
||
if (collider) {
|
||
// 监听碰撞事件
|
||
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
|
||
}
|
||
|
||
this.initProps();
|
||
// this.showBonusPopup()
|
||
// this.scheduleOnce(() => {
|
||
// this.showBonusPopup()
|
||
// }, 5);
|
||
}
|
||
|
||
onDestroy() {
|
||
// 移除触摸事件
|
||
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||
this.clearPopupHideSchedule();
|
||
}
|
||
|
||
start() {
|
||
if (this.player) {
|
||
this.originalPosition.set(this.player.position);
|
||
// 初始化时设置站立动画和正确的方向
|
||
this.switchAnimation('stand');
|
||
this.updatePlayerScale();
|
||
}
|
||
}
|
||
|
||
initProps() {
|
||
if (!this.canvas) {
|
||
console.warn('Canvas未设置,无法初始化道具');
|
||
return;
|
||
}
|
||
|
||
// 查找Canvas下的Props节点
|
||
const propsNode = this.canvas.node.getChildByName('Props');
|
||
if (!propsNode) {
|
||
console.warn('未找到Props节点,请确保Canvas下有名为"Props"的节点');
|
||
return;
|
||
}
|
||
|
||
// 清空现有的道具列表
|
||
this.props.length = 0;
|
||
|
||
// 获取Props节点下的所有子节点
|
||
for (let i = 0; i < propsNode.children.length; i++) {
|
||
const child = propsNode.children[i];
|
||
this.props.push(child);
|
||
console.log(`添加道具: ${child.name}`);
|
||
|
||
// 为每个道具添加悬浮动画
|
||
this.addFloatingAnimation(child);
|
||
}
|
||
|
||
console.log(`初始化道具完成,共找到 ${this.props.length} 个道具`);
|
||
}
|
||
|
||
/**
|
||
* 为道具添加悬浮动画
|
||
*/
|
||
private addFloatingAnimation(propNode: Node) {
|
||
if (!propNode) return;
|
||
|
||
// 保存原始位置
|
||
const originalY = propNode.position.y;
|
||
const floatHeight = 10; // 悬浮高度(像素)
|
||
const floatDuration = 2; // 悬浮周期(秒)
|
||
|
||
// 创建上下浮动的动画
|
||
tween(propNode)
|
||
.to(floatDuration / 2, { position: new Vec3(propNode.position.x, originalY + floatHeight, propNode.position.z) }, {
|
||
easing: 'sineInOut'
|
||
})
|
||
.to(floatDuration / 2, { position: new Vec3(propNode.position.x, originalY, propNode.position.z) }, {
|
||
easing: 'sineInOut'
|
||
})
|
||
.union() // 将动画串联起来
|
||
.repeatForever() // 无限重复
|
||
.start();
|
||
|
||
console.log(`为道具 ${propNode.name} 添加悬浮动画`);
|
||
}
|
||
|
||
private onTouchStart(event: EventTouch) {
|
||
if (this.activePopup) {
|
||
return;
|
||
}
|
||
|
||
if (!this.player || !this.camera || !this.pathfinder || this.isAttacking || this.isGameOver || this.isWin) return;
|
||
|
||
this.guideNode.active = false;
|
||
|
||
// 获取触摸点的UI坐标
|
||
const touchLocation = event.getUILocation();
|
||
|
||
// 将UI坐标转换为世界坐标
|
||
const worldPos = this.screenToWorldPoint(touchLocation);
|
||
|
||
console.log(`触摸UI坐标: (${touchLocation.x}, ${touchLocation.y})`);
|
||
console.log(`转换后世界坐标: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)})`);
|
||
|
||
this.moveToPositionWithPathfinding(worldPos);
|
||
}
|
||
|
||
private screenToWorldPoint(screenPos: { x: number, y: number }): Vec3 {
|
||
if (!this.camera) {
|
||
console.error('Camera未设置,无法进行坐标转换');
|
||
return new Vec3(screenPos.x, screenPos.y, 0);
|
||
}
|
||
|
||
// 获取可见区域大小
|
||
const visibleSize = view.getVisibleSize();
|
||
|
||
// 计算屏幕中心点
|
||
const centerX = visibleSize.width * 0.5;
|
||
const centerY = visibleSize.height * 0.5;
|
||
|
||
// 将屏幕坐标转换为以屏幕中心为原点的坐标
|
||
const normalizedX = screenPos.x - centerX;
|
||
const normalizedY = screenPos.y - centerY;
|
||
|
||
// 获取相机的正交高度
|
||
const orthoHeight = this.camera.orthoHeight;
|
||
|
||
// 计算屏幕坐标到世界坐标的缩放比例
|
||
// 屏幕高度的一半对应 orthoHeight 的世界单位
|
||
const scaleY = orthoHeight / (visibleSize.height * 0.5);
|
||
const scaleX = scaleY; // 保持宽高比一致
|
||
|
||
// 将屏幕坐标转换为世界坐标(相对于相机中心)
|
||
const worldOffsetX = normalizedX * scaleX;
|
||
const worldOffsetY = normalizedY * scaleY;
|
||
|
||
// 考虑相机的位置偏移
|
||
const cameraPos = this.camera.node.position;
|
||
|
||
// 计算最终的世界坐标
|
||
const worldX = worldOffsetX + cameraPos.x;
|
||
const worldY = worldOffsetY + cameraPos.y;
|
||
|
||
return new Vec3(worldX, worldY, 0);
|
||
}
|
||
|
||
|
||
private moveToPositionWithPathfinding(worldPos: Vec3) {
|
||
if (!this.player || !this.pathfinder) return;
|
||
|
||
// 停止当前移动
|
||
// this.stopMovement();
|
||
|
||
// 限制目标位置在地图边界内
|
||
const clampedPos = this.clampPlayerPosition(worldPos);
|
||
|
||
// 检查目标位置是否可行走
|
||
if (!this.pathfinder.isWorldPositionWalkable(clampedPos)) {
|
||
console.log('目标位置不可行走,寻找最近的可行走位置');
|
||
const closestWalkable = this.pathfinder.getClosestWalkablePosition(clampedPos);
|
||
if (!closestWalkable) {
|
||
console.warn('找不到可行走的位置');
|
||
return;
|
||
}
|
||
clampedPos.set(closestWalkable);
|
||
}
|
||
|
||
// 检查玩家当前位置是否可行走
|
||
let startPos = this.player.position;
|
||
if (!this.pathfinder.isWorldPositionWalkable(startPos)) {
|
||
console.log('玩家当前位置不可行走,寻找最近的可行走位置作为起点');
|
||
const closestPlayerWalkable = this.pathfinder.getClosestWalkablePosition(startPos);
|
||
if (!closestPlayerWalkable) {
|
||
console.warn('找不到玩家附近的可行走位置');
|
||
return;
|
||
}
|
||
startPos = closestPlayerWalkable;
|
||
console.log(`将玩家移动到最近的可行走位置: (${startPos.x.toFixed(2)}, ${startPos.y.toFixed(2)})`);
|
||
|
||
// 直接将玩家传送到最近的可行走位置
|
||
this.player.setPosition(startPos);
|
||
}
|
||
|
||
// 使用寻路算法计算路径
|
||
this.currentPath = this.pathfinder.findPath(startPos, clampedPos);
|
||
|
||
if (this.currentPath.length === 0) {
|
||
console.warn('无法找到路径');
|
||
return;
|
||
}
|
||
|
||
console.log(`找到路径,包含${this.currentPath.length}个点`);
|
||
|
||
// 开始沿路径移动
|
||
this.currentPathIndex = 0;
|
||
this.isMoving = true;
|
||
|
||
// 使用平滑路径移动
|
||
this.startSmoothPathMovement();
|
||
}
|
||
|
||
// 限制玩家位置在地图边界内
|
||
private clampPlayerPosition(position: Vec3): Vec3 {
|
||
return this.clampPositionWithinMap(position);
|
||
}
|
||
|
||
private clampPositionWithinMap(position: Vec3): Vec3 {
|
||
// 限制位置
|
||
const clampedPosition = position.clone();
|
||
|
||
return clampedPosition;
|
||
}
|
||
|
||
/**
|
||
* 切换动画,避免不必要的切换
|
||
* 根据 this.currentDirection 来决定 player 的 scale 是否要取反,不再需要通过动画名称进行区分
|
||
*/
|
||
private switchAnimation(actionName: string) {
|
||
if (!this.player) {
|
||
console.warn('Player节点未设置,无法切换动画');
|
||
return;
|
||
}
|
||
|
||
const animNode = this.player.getChildByName('Anim');
|
||
if (!animNode) {
|
||
console.warn('未找到Anim子节点,无法切换动画');
|
||
return;
|
||
}
|
||
|
||
const animation = animNode.getComponent(Animation);
|
||
if (!animation) {
|
||
console.warn('未找到Animation组件,无法播放动画');
|
||
return;
|
||
}
|
||
|
||
const candidates = this.getAnimationNameCandidates(actionName);
|
||
let finalAnimationName: string | null = null;
|
||
|
||
for (const candidate of candidates) {
|
||
if (!candidate) {
|
||
continue;
|
||
}
|
||
const state = animation.getState(candidate);
|
||
if (state) {
|
||
finalAnimationName = candidate;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!finalAnimationName) {
|
||
const fallback = this.isUpgraded ? 'stand_2' : 'stand5';
|
||
if (!this.currentAnimation || this.currentAnimation !== fallback) {
|
||
console.warn(`未找到可用的动画 ${actionName},使用兜底动画 ${fallback}`);
|
||
}
|
||
const fallbackState = animation.getState(fallback);
|
||
if (!fallbackState) {
|
||
console.error('无法找到兜底动画,动画切换失败');
|
||
return;
|
||
}
|
||
finalAnimationName = fallback;
|
||
}
|
||
|
||
if (this.currentAnimation === finalAnimationName) {
|
||
this.updatePlayerScale();
|
||
return;
|
||
}
|
||
|
||
animation.play(finalAnimationName);
|
||
this.currentAnimation = finalAnimationName;
|
||
console.log(`切换动画: ${finalAnimationName}`);
|
||
this.updatePlayerScale();
|
||
}
|
||
|
||
/**
|
||
* 根据当前方向更新玩家Anim子节点的scale
|
||
* 左向(LeftUp/LeftDown)时需要翻转Anim节点(scale.x 取反)
|
||
*/
|
||
private updatePlayerScale() {
|
||
if (!this.player) return;
|
||
|
||
// 获取Anim子节点
|
||
const animNode = this.player.getChildByName('Anim');
|
||
if (!animNode) {
|
||
console.warn('未找到Anim子节点,无法更新方向');
|
||
return;
|
||
}
|
||
|
||
const currentScale = animNode.scale.clone();
|
||
const faceLeft = this.currentDirection === PlayerDirection.LeftUp || this.currentDirection === PlayerDirection.LeftDown;
|
||
const desiredScaleX = faceLeft ? -Math.abs(currentScale.x) : Math.abs(currentScale.x);
|
||
|
||
if (currentScale.x !== desiredScaleX) {
|
||
animNode.setScale(desiredScaleX, currentScale.y, currentScale.z);
|
||
}
|
||
}
|
||
|
||
private normalizeAnimationAction(animationName: string): string {
|
||
if (!animationName) {
|
||
return animationName;
|
||
}
|
||
if (animationName.startsWith('walk')) {
|
||
return 'walk';
|
||
}
|
||
if (animationName.startsWith('attack')) {
|
||
return 'attack';
|
||
}
|
||
if (animationName.startsWith('stand')) {
|
||
return 'stand';
|
||
}
|
||
if (animationName.startsWith('die')) {
|
||
return 'die';
|
||
}
|
||
return animationName;
|
||
}
|
||
|
||
private resolveDirectionFromDelta(
|
||
deltaX: number,
|
||
deltaY: number,
|
||
overrides?: { horizontal?: 'Left' | 'Right', vertical?: 'Up' | 'Down' }
|
||
): PlayerDirection {
|
||
const horizontalThreshold = 0.1;
|
||
const verticalThreshold = 0.1;
|
||
|
||
let horizontal: 'Left' | 'Right' = overrides?.horizontal ?? (this.isFacingLeft() ? 'Left' : 'Right');
|
||
if (!overrides?.horizontal && Math.abs(deltaX) > horizontalThreshold) {
|
||
horizontal = deltaX < 0 ? 'Left' : 'Right';
|
||
}
|
||
|
||
let vertical: 'Up' | 'Down' = overrides?.vertical ?? (this.isFacingUp() ? 'Up' : 'Down');
|
||
if (!overrides?.vertical && Math.abs(deltaY) > verticalThreshold) {
|
||
vertical = deltaY > 0 ? 'Up' : 'Down';
|
||
}
|
||
|
||
return this.composeDirection(horizontal, vertical);
|
||
}
|
||
|
||
private composeDirection(horizontal: 'Left' | 'Right', vertical: 'Up' | 'Down'): PlayerDirection {
|
||
if (horizontal === 'Left') {
|
||
return vertical === 'Up' ? PlayerDirection.LeftUp : PlayerDirection.LeftDown;
|
||
}
|
||
return vertical === 'Up' ? PlayerDirection.RightUp : PlayerDirection.RightDown;
|
||
}
|
||
|
||
private isFacingLeft(): boolean {
|
||
return this.currentDirection === PlayerDirection.LeftUp || this.currentDirection === PlayerDirection.LeftDown;
|
||
}
|
||
|
||
private isFacingUp(): boolean {
|
||
return this.currentDirection === PlayerDirection.LeftUp || this.currentDirection === PlayerDirection.RightUp;
|
||
}
|
||
|
||
private getVerticalAnimationSuffix(): '3' | '5' {
|
||
return this.isFacingUp() ? '3' : '5';
|
||
}
|
||
|
||
private getAnimationNameCandidates(requestedName: string): string[] {
|
||
const action = this.normalizeAnimationAction(requestedName);
|
||
const suffix = this.getVerticalAnimationSuffix();
|
||
const oppositeSuffix = suffix === '3' ? '5' : '3';
|
||
const candidates: string[] = [];
|
||
|
||
const pushUnique = (name?: string) => {
|
||
if (!name) {
|
||
return;
|
||
}
|
||
if (candidates.indexOf(name) === -1) {
|
||
candidates.push(name);
|
||
}
|
||
};
|
||
|
||
if (action === 'walk' || action === 'attack') {
|
||
if (this.isUpgraded) {
|
||
pushUnique(`${action}${suffix}_2`);
|
||
pushUnique(`${action}_2`);
|
||
}
|
||
pushUnique(`${action}${suffix}`);
|
||
pushUnique(action);
|
||
pushUnique(`${action}${oppositeSuffix}`);
|
||
} else if (action === 'stand') {
|
||
if (this.isUpgraded) {
|
||
pushUnique(`stand${suffix}_2`);
|
||
pushUnique('stand_2');
|
||
}
|
||
pushUnique(`stand${suffix}`);
|
||
pushUnique(`stand${oppositeSuffix}`);
|
||
pushUnique('stand');
|
||
} else {
|
||
if (this.isUpgraded) {
|
||
pushUnique(`${action}_2`);
|
||
}
|
||
pushUnique(action);
|
||
}
|
||
|
||
return candidates;
|
||
}
|
||
|
||
/**
|
||
* 开始平滑路径移动
|
||
*/
|
||
private startSmoothPathMovement() {
|
||
if (!this.player || this.currentPath.length === 0) {
|
||
this.isMoving = false;
|
||
this.switchAnimation('stand');
|
||
return;
|
||
}
|
||
|
||
// 停止当前的移动tween
|
||
if (this.moveTween) {
|
||
this.moveTween.stop();
|
||
this.moveTween = null;
|
||
}
|
||
|
||
// 计算总路径长度
|
||
let totalDistance = 0;
|
||
for (let i = 0; i < this.currentPath.length - 1; i++) {
|
||
totalDistance += Vec3.distance(this.currentPath[i], this.currentPath[i + 1]);
|
||
}
|
||
|
||
// 计算总移动时间
|
||
const totalTime = totalDistance / this.moveSpeed;
|
||
|
||
console.log(`开始平滑路径移动,总距离: ${totalDistance.toFixed(2)}, 总时间: ${totalTime.toFixed(2)}秒`);
|
||
|
||
// 初始化时先设置第一个路径点的方向
|
||
if (this.currentPath.length > 1) {
|
||
const startPos = this.player.position.clone();
|
||
const nextPos = this.currentPath[1]; // 第二个路径点
|
||
this.updateMovementDirectionOnce(startPos, nextPos);
|
||
} else {
|
||
// 如果只有一个路径点,直接朝向它
|
||
const startPos = this.player.position.clone();
|
||
const finalTargetPos = this.currentPath[0];
|
||
this.updateMovementDirectionOnce(startPos, finalTargetPos);
|
||
}
|
||
|
||
// 创建连续的路径移动
|
||
this.moveTween = tween(this.player)
|
||
.to(totalTime, { position: this.currentPath[this.currentPath.length - 1] }, {
|
||
easing: 'linear',
|
||
onUpdate: (target: Node, ratio: number) => {
|
||
// 根据进度计算当前位置
|
||
const currentPos = this.getPositionOnPath(ratio);
|
||
if (currentPos) {
|
||
target.position = currentPos;
|
||
|
||
// 在移动过程中动态更新方向
|
||
this.updateDirectionDuringMovement(currentPos, ratio);
|
||
}
|
||
},
|
||
onComplete: () => {
|
||
this.isMoving = false;
|
||
this.switchAnimation('stand');
|
||
console.log('平滑路径移动完成');
|
||
}
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 根据路径进度获取当前位置
|
||
*/
|
||
private getPositionOnPath(ratio: number): Vec3 | null {
|
||
if (!this.player || this.currentPath.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
// 计算总路径长度
|
||
const segmentLengths: number[] = [];
|
||
let totalLength = 0;
|
||
|
||
for (let i = 0; i < this.currentPath.length - 1; i++) {
|
||
const length = Vec3.distance(this.currentPath[i], this.currentPath[i + 1]);
|
||
segmentLengths.push(length);
|
||
totalLength += length;
|
||
}
|
||
|
||
// 计算目标距离
|
||
const targetDistance = totalLength * ratio;
|
||
|
||
// 找到对应的路径段
|
||
let currentDistance = 0;
|
||
for (let i = 0; i < segmentLengths.length; i++) {
|
||
if (currentDistance + segmentLengths[i] >= targetDistance) {
|
||
// 在当前段内
|
||
const segmentRatio = (targetDistance - currentDistance) / segmentLengths[i];
|
||
const startPos = this.currentPath[i];
|
||
const endPos = this.currentPath[i + 1];
|
||
|
||
// 线性插值计算当前位置
|
||
return new Vec3(
|
||
startPos.x + (endPos.x - startPos.x) * segmentRatio,
|
||
startPos.y + (endPos.y - startPos.y) * segmentRatio,
|
||
startPos.z + (endPos.z - startPos.z) * segmentRatio
|
||
);
|
||
}
|
||
currentDistance += segmentLengths[i];
|
||
}
|
||
|
||
// 如果超出范围,返回终点
|
||
return this.currentPath[this.currentPath.length - 1];
|
||
}
|
||
|
||
/**
|
||
* 算法:优先判断水平方向(左/右),只有当水平方向不明显时才判断垂直方向
|
||
*/
|
||
private updateMovementDirectionOnce(startPos: Vec3, targetPos: Vec3) {
|
||
if (!this.player) {
|
||
return;
|
||
}
|
||
|
||
// 计算移动方向(基于起始位置和目标位置)
|
||
const deltaX = targetPos.x - startPos.x;
|
||
const deltaY = targetPos.y - startPos.y;
|
||
|
||
// 如果移动距离很小,不更新动画
|
||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||
if (distance < 1) {
|
||
return;
|
||
}
|
||
|
||
this.currentDirection = this.resolveDirectionFromDelta(deltaX, deltaY);
|
||
|
||
// 切换到对应的动画(只传递基础动画名称)
|
||
this.switchAnimation('walk');
|
||
}
|
||
|
||
/**
|
||
* 在移动过程中动态更新方向
|
||
* 根据当前位置在路径中的位置,计算朝向下一个路径点的方向
|
||
*/
|
||
private updateDirectionDuringMovement(currentPos: Vec3, ratio: number) {
|
||
if (!this.player || this.currentPath.length <= 1) {
|
||
return;
|
||
}
|
||
|
||
// 计算当前在路径中的大致位置
|
||
const pathLength = this.currentPath.length;
|
||
const approxIndex = Math.floor(ratio * (pathLength - 1));
|
||
if (approxIndex >= pathLength - 1) {
|
||
return;
|
||
}
|
||
|
||
const currentPathIndex = Math.max(0, approxIndex);
|
||
const nextIndex = currentPathIndex + 1;
|
||
const maxIndex = Math.min(pathLength - 1, nextIndex + this.directionLookAheadSteps);
|
||
|
||
let weightedDirX = 0;
|
||
let weightedDirY = 0;
|
||
let totalWeight = 0;
|
||
let farthestDistance = 0;
|
||
|
||
// 聚合未来若干路径节点的方向信息,减缓转向突变
|
||
for (let i = nextIndex; i <= maxIndex; i++) {
|
||
const samplePoint = this.currentPath[i];
|
||
const deltaX = samplePoint.x - currentPos.x;
|
||
const deltaY = samplePoint.y - currentPos.y;
|
||
const distanceToPoint = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||
|
||
farthestDistance = Math.max(farthestDistance, distanceToPoint);
|
||
if (distanceToPoint < 0.0001) {
|
||
continue;
|
||
}
|
||
|
||
const weight = i - currentPathIndex;
|
||
weightedDirX += (deltaX / distanceToPoint) * weight;
|
||
weightedDirY += (deltaY / distanceToPoint) * weight;
|
||
totalWeight += weight;
|
||
}
|
||
|
||
if (totalWeight === 0 || farthestDistance < this.minDirectionUpdateDistance) {
|
||
return;
|
||
}
|
||
|
||
const averagedDirX = weightedDirX / totalWeight;
|
||
const averagedDirY = weightedDirY / totalWeight;
|
||
const averagedMagnitude = Math.sqrt(averagedDirX * averagedDirX + averagedDirY * averagedDirY);
|
||
if (averagedMagnitude < 0.1) {
|
||
return;
|
||
}
|
||
|
||
// 计算新的方向
|
||
const newDirection = this.resolveDirectionFromDelta(averagedDirX, averagedDirY);
|
||
|
||
// 只有当方向发生显著变化时才更新
|
||
if (newDirection !== this.currentDirection) {
|
||
const previousDirection = this.currentDirection;
|
||
this.currentDirection = newDirection;
|
||
|
||
// 更新玩家缩放(翻转)
|
||
this.updatePlayerScale();
|
||
|
||
// 根据朝向改变更新移动动画
|
||
this.updateMovementAnimation(previousDirection, newDirection);
|
||
|
||
console.log(`移动过程中更新方向: ${previousDirection} -> ${newDirection}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据朝向改变更新移动动画
|
||
* @param previousDirection 之前的方向
|
||
* @param newDirection 新的方向
|
||
*/
|
||
private updateMovementAnimation(previousDirection: PlayerDirection, newDirection: PlayerDirection) {
|
||
if (!this.isMoving) {
|
||
return; // 只有在移动状态下才更新动画
|
||
}
|
||
|
||
// 检查是否需要切换动画(垂直方向改变时)
|
||
const previousVertical = this.isDirectionUp(previousDirection);
|
||
const newVertical = this.isDirectionUp(newDirection);
|
||
|
||
if (previousVertical !== newVertical) {
|
||
// 垂直方向改变,需要切换动画
|
||
this.switchAnimation('walk');
|
||
console.log(`垂直方向改变,切换移动动画: ${previousVertical ? '上' : '下'} -> ${newVertical ? '上' : '下'}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断方向是否向上
|
||
* @param direction 玩家方向
|
||
* @returns 是否向上
|
||
*/
|
||
private isDirectionUp(direction: PlayerDirection): boolean {
|
||
return direction === PlayerDirection.LeftUp || direction === PlayerDirection.RightUp;
|
||
}
|
||
|
||
/**
|
||
* 停止当前移动
|
||
*/
|
||
private stopMovement() {
|
||
// 停止当前的移动tween
|
||
if (this.moveTween) {
|
||
this.moveTween.stop();
|
||
this.moveTween = null;
|
||
}
|
||
|
||
if (this.player) {
|
||
tween(this.player).stop();
|
||
}
|
||
this.isMoving = false;
|
||
this.currentPath = [];
|
||
this.currentPathIndex = 0;
|
||
|
||
// 停止移动时播放站立动画
|
||
this.switchAnimation('stand');
|
||
}
|
||
|
||
/**
|
||
* 统一处理碰撞回调的启停,确保在逻辑失败时能恢复碰撞器状态
|
||
*/
|
||
private async processColliderCollision(otherCollider: Collider2D, handler: () => Promise<boolean | void> | boolean | void) {
|
||
if (!otherCollider || !otherCollider.node || !otherCollider.node.isValid) {
|
||
return;
|
||
}
|
||
|
||
if (!otherCollider.enabled) {
|
||
return;
|
||
}
|
||
|
||
otherCollider.enabled = false;
|
||
let shouldKeepDisabled = false;
|
||
|
||
try {
|
||
const keepDisabled = await Promise.resolve(handler());
|
||
shouldKeepDisabled = keepDisabled === true;
|
||
} finally {
|
||
if (!shouldKeepDisabled && otherCollider && otherCollider.isValid && otherCollider.node && otherCollider.node.isValid) {
|
||
otherCollider.enabled = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
update(deltaTime: number) {
|
||
// 更新逻辑现在主要由缓动系统处理
|
||
// 这里可以添加其他需要每帧更新的逻辑
|
||
|
||
|
||
}
|
||
|
||
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) {
|
||
console.log('碰撞检测', selfCollider.node.name, otherCollider.node.name);
|
||
if (!otherCollider || !otherCollider.node || !otherCollider.node.isValid) {
|
||
return;
|
||
}
|
||
|
||
if (this.isAttacking || this.isGameOver || this.isWin) {
|
||
return;
|
||
}
|
||
|
||
const nodeName = otherCollider.node.name || '';
|
||
const isMonster = nodeName.startsWith('guai_');
|
||
const isBox = nodeName.startsWith('box_');
|
||
|
||
if (!isMonster && !isBox) {
|
||
return;
|
||
}
|
||
|
||
if (isMonster) {
|
||
void this.processColliderCollision(otherCollider, () => this.handleAttack(selfCollider, otherCollider));
|
||
return;
|
||
}
|
||
|
||
void this.processColliderCollision(otherCollider, () => this.handleBoxCollision(selfCollider, otherCollider));
|
||
}
|
||
|
||
/**
|
||
* 将玩家移动到怪物正对位置,确保攻击前双方站位合理
|
||
*/
|
||
private alignPlayerForAttack(selfCollider: Collider2D, monsterCollider: Collider2D): Promise<void> {
|
||
return new Promise((resolve) => {
|
||
if (!this.player || !selfCollider || !monsterCollider || !monsterCollider.node || !monsterCollider.node.isValid) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const playerNode = this.player;
|
||
const monsterNode = monsterCollider.node;
|
||
|
||
const playerWorldPos = playerNode.worldPosition.clone();
|
||
const monsterWorldPos = monsterNode.worldPosition.clone();
|
||
|
||
const playerBox = selfCollider instanceof BoxCollider2D ? selfCollider : null;
|
||
const monsterBox = monsterCollider instanceof BoxCollider2D ? monsterCollider : null;
|
||
|
||
const playerScale = playerNode.worldScale;
|
||
const monsterScale = monsterNode.worldScale;
|
||
|
||
const playerHalfWidth = playerBox ? (playerBox.size.x * Math.abs(playerScale.x)) / 2 : 40;
|
||
const monsterHalfWidth = monsterBox ? (monsterBox.size.x * Math.abs(monsterScale.x)) / 2 : 60;
|
||
|
||
const playerOffsetX = playerBox ? playerBox.offset.x * playerScale.x : 0;
|
||
const playerOffsetY = playerBox ? playerBox.offset.y * playerScale.y : 0;
|
||
const monsterOffsetX = monsterBox ? monsterBox.offset.x * monsterScale.x : 0;
|
||
const monsterOffsetY = monsterBox ? monsterBox.offset.y * monsterScale.y : 0;
|
||
|
||
const playerCenterX = playerWorldPos.x + playerOffsetX;
|
||
const monsterCenterX = monsterWorldPos.x + monsterOffsetX;
|
||
const standOnLeft = playerCenterX <= monsterCenterX;
|
||
|
||
const totalHalfWidth = playerHalfWidth + monsterHalfWidth + this.attackAlignGap;
|
||
const directionMultiplier = standOnLeft ? -1 : 1;
|
||
|
||
const targetWorldPos = new Vec3(
|
||
monsterWorldPos.x + monsterOffsetX + directionMultiplier * totalHalfWidth - playerOffsetX,
|
||
monsterWorldPos.y + monsterOffsetY - playerOffsetY + (this.isUpgraded ? 0 : this.attackVerticalOffset),
|
||
playerWorldPos.z
|
||
);
|
||
|
||
const targetLocalPos = this.convertWorldToParentSpace(playerNode, targetWorldPos);
|
||
const currentLocalPos = playerNode.position.clone();
|
||
|
||
const distance = Vec3.distance(currentLocalPos, targetLocalPos);
|
||
|
||
const targetPlayerCenterY = targetWorldPos.y + playerOffsetY;
|
||
const monsterCenterY = monsterWorldPos.y + monsterOffsetY;
|
||
const verticalDeltaToMonster = monsterCenterY - targetPlayerCenterY;
|
||
const verticalOverride = Math.abs(verticalDeltaToMonster) <= 0.5 ? undefined : (verticalDeltaToMonster > 0 ? 'Up' : 'Down');
|
||
const desiredDirection = this.resolveDirectionFromDelta(
|
||
targetWorldPos.x - playerWorldPos.x,
|
||
targetWorldPos.y - playerWorldPos.y,
|
||
{
|
||
horizontal: standOnLeft ? 'Right' : 'Left',
|
||
vertical: verticalOverride,
|
||
}
|
||
);
|
||
this.currentDirection = desiredDirection;
|
||
|
||
if (distance < 1) {
|
||
playerNode.setPosition(targetLocalPos);
|
||
this.updatePlayerScale();
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// 只传递基础动画名称,方向由 currentDirection 控制
|
||
this.switchAnimation('walk');
|
||
|
||
const baseDuration = this.moveSpeed > 0 ? distance / this.moveSpeed : 0.2;
|
||
const duration = Math.min(Math.max(baseDuration, 0.12), 0.45);
|
||
|
||
tween(playerNode)
|
||
.to(duration, { position: targetLocalPos }, {
|
||
easing: 'smooth',
|
||
onComplete: () => {
|
||
playerNode.setPosition(targetLocalPos);
|
||
this.currentDirection = desiredDirection;
|
||
this.updatePlayerScale();
|
||
resolve();
|
||
}
|
||
})
|
||
.start();
|
||
});
|
||
}
|
||
|
||
private convertWorldToParentSpace(node: Node, worldPos: Vec3): Vec3 {
|
||
const parent = node.parent;
|
||
if (!parent) {
|
||
return worldPos.clone();
|
||
}
|
||
|
||
const parentTransform = parent.getComponent(UITransform);
|
||
if (parentTransform) {
|
||
return parentTransform.convertToNodeSpaceAR(worldPos);
|
||
}
|
||
|
||
const fallback = worldPos.clone();
|
||
fallback.subtract(parent.worldPosition);
|
||
return fallback;
|
||
}
|
||
|
||
/**
|
||
* 处理攻击逻辑
|
||
*/
|
||
private async handleAttack(selfCollider: Collider2D, otherCollider: Collider2D) {
|
||
if (this.isAttacking) {
|
||
return;
|
||
}
|
||
|
||
if (!this.player || !otherCollider || !otherCollider.node || !otherCollider.node.isValid) {
|
||
return;
|
||
}
|
||
|
||
this.isAttacking = true;
|
||
this.stopMovement();
|
||
|
||
await this.alignPlayerForAttack(selfCollider, otherCollider);
|
||
|
||
if (!this.player || !otherCollider.node || !otherCollider.node.isValid) {
|
||
this.isAttacking = false;
|
||
return;
|
||
}
|
||
|
||
console.log('开始攻击,怪物名称:', otherCollider.node.name);
|
||
|
||
// 获取玩家和怪物的生命值
|
||
const playerHpLabel = this.player.getChildByName('hp');
|
||
const monsterHpLabel = otherCollider.node.getChildByName('hp');
|
||
|
||
if (!playerHpLabel || !monsterHpLabel) {
|
||
console.warn('未找到生命值标签,玩家hp:', playerHpLabel, '怪物hp:', monsterHpLabel);
|
||
this.isAttacking = false;
|
||
return;
|
||
}
|
||
|
||
// 获取生命值数值
|
||
const playerLabel = playerHpLabel.getComponent(Label);
|
||
const monsterLabel = monsterHpLabel.getComponent(Label);
|
||
|
||
if (!playerLabel || !monsterLabel) {
|
||
console.warn('未找到Label组件');
|
||
this.isAttacking = false;
|
||
return;
|
||
}
|
||
|
||
const playerHp = parseInt(playerLabel.string) || 0;
|
||
const monsterHp = parseInt(monsterLabel.string) || 0;
|
||
console.log('玩家生命值:', playerHp, '怪物生命值:', monsterHp);
|
||
|
||
// 播放攻击音效
|
||
if (this.attackAudio) {
|
||
this.scheduleOnce(() => {
|
||
const audioSource = this.attackAudio.getComponent(AudioSource);
|
||
if (audioSource) {
|
||
audioSource.play();
|
||
console.log('播放攻击音效');
|
||
}
|
||
}, 0.5)
|
||
}
|
||
|
||
// 获取玩家动画组件
|
||
const playerAnimNode = this.player.getChildByName('Anim');
|
||
const playerAnimation = playerAnimNode ? playerAnimNode.getComponent(Animation) : null;
|
||
|
||
if (!playerAnimation) {
|
||
console.warn('未找到玩家动画组件');
|
||
this.isAttacking = false;
|
||
return;
|
||
}
|
||
|
||
// 检查是否是 guai_10,如果是则使用特殊战斗逻辑
|
||
if (otherCollider.node.name === 'guai_10') {
|
||
await this.handleGuai10SpecialAttack(selfCollider, otherCollider, playerLabel, monsterLabel, playerHp, monsterHp, playerAnimation);
|
||
} else {
|
||
// 原有的普通战斗逻辑
|
||
await this.handleNormalAttack(otherCollider, playerLabel, monsterLabel, playerHp, monsterHp, playerAnimation);
|
||
}
|
||
|
||
this.isAttacking = false;
|
||
|
||
// 停止攻击音效
|
||
// if (this.attackAudio) {
|
||
// const audioSource = this.attackAudio.getComponent(AudioSource);
|
||
// if (audioSource) {
|
||
// audioSource.stop();
|
||
// console.log('停止攻击音效');
|
||
// }
|
||
// }
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 处理普通怪物的攻击逻辑
|
||
*/
|
||
private async handleNormalAttack(otherCollider: Collider2D, playerLabel: Label, monsterLabel: Label, playerHp: number, monsterHp: number, playerAnimation: Animation) {
|
||
// 播放玩家攻击动画(只传递基础动画名称)
|
||
this.switchAnimation('attack');
|
||
|
||
// 监听玩家攻击动画结束事件
|
||
return new Promise<void>((resolve) => {
|
||
playerAnimation.once(Animation.EventType.FINISHED, async () => {
|
||
if (!this.player || !playerLabel.isValid || !monsterLabel.isValid) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// 比较生命值,判断输赢
|
||
console.log('判定攻击结果,玩家HP:', playerHp, '怪物HP:', monsterHp);
|
||
|
||
if (playerHp >= monsterHp) {
|
||
// 玩家获胜,直接执行后续流程,不需要播放怪物攻击动画
|
||
await this.handlePlayerWin(otherCollider, playerLabel, monsterHp);
|
||
} else {
|
||
// 玩家输了,需要播放怪物攻击动画
|
||
await this.handleMonsterAttack(otherCollider, playerLabel, monsterHp);
|
||
}
|
||
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理 guai_10 的特殊攻击逻辑:玩家攻击 → 怪物还击 → 玩家再攻击 → 最终判定
|
||
*/
|
||
private async handleGuai10SpecialAttack(selfCollider: Collider2D, otherCollider: Collider2D, playerLabel: Label, monsterLabel: Label, playerHp: number, monsterHp: number, playerAnimation: Animation) {
|
||
console.log('开始 guai_10 特殊战斗逻辑');
|
||
|
||
// 第一轮:玩家攻击
|
||
console.log('第一轮:玩家攻击');
|
||
this.switchAnimation('attack');
|
||
|
||
// 等待玩家攻击动画结束
|
||
await new Promise<void>((resolve) => {
|
||
playerAnimation.once(Animation.EventType.FINISHED, () => {
|
||
console.log('玩家第一轮攻击完成');
|
||
resolve();
|
||
});
|
||
});
|
||
|
||
// 检查节点是否仍然有效
|
||
if (!this.player || !otherCollider.node || !otherCollider.node.isValid || !playerLabel.isValid || !monsterLabel.isValid) {
|
||
return;
|
||
}
|
||
|
||
// 第二轮:怪物还击
|
||
console.log('第二轮:怪物还击');
|
||
await this.playMonsterAttackAnimation(otherCollider);
|
||
|
||
// 检查节点是否仍然有效
|
||
if (!this.player || !otherCollider.node || !otherCollider.node.isValid || !playerLabel.isValid || !monsterLabel.isValid) {
|
||
return;
|
||
}
|
||
|
||
// 确保玩家在怪物攻击后回到站立状态,然后再进行第三轮攻击
|
||
this.switchAnimation('stand');
|
||
|
||
// 添加短暂延迟,让玩家站立动画播放一下
|
||
await new Promise<void>((resolve) => {
|
||
this.scheduleOnce(() => {
|
||
resolve();
|
||
}, 0.2);
|
||
});
|
||
|
||
// 第三轮:玩家再次攻击
|
||
console.log('第三轮:玩家再次攻击');
|
||
this.switchAnimation('attack');
|
||
|
||
// 等待玩家攻击动画结束
|
||
await new Promise<void>((resolve) => {
|
||
playerAnimation.once(Animation.EventType.FINISHED, () => {
|
||
console.log('玩家第三轮攻击完成');
|
||
resolve();
|
||
});
|
||
});
|
||
|
||
// 检查节点是否仍然有效
|
||
if (!this.player || !otherCollider.node || !otherCollider.node.isValid || !playerLabel.isValid || !monsterLabel.isValid) {
|
||
return;
|
||
}
|
||
|
||
// 最终判定:比较生命值
|
||
console.log('最终判定,玩家HP:', playerHp, '怪物HP:', monsterHp);
|
||
|
||
if (playerHp >= monsterHp) {
|
||
// 玩家获胜
|
||
await this.handlePlayerWin(otherCollider, playerLabel, monsterHp);
|
||
} else {
|
||
// 玩家失败
|
||
this.executePlayerDefeat(otherCollider, playerLabel);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 播放怪物攻击动画
|
||
*/
|
||
private async playMonsterAttackAnimation(otherCollider: Collider2D): Promise<void> {
|
||
const animNode = otherCollider.node.getChildByName('Anim');
|
||
const monsterAnimation = animNode ? animNode.getComponent(Animation) : null;
|
||
|
||
if (monsterAnimation) {
|
||
console.log(`尝试播放怪物攻击动画: ${otherCollider.node.name}_attack`);
|
||
|
||
// 检查动画是否存在
|
||
const attackAnimState = monsterAnimation.getState(`${otherCollider.node.name}_attack`);
|
||
if (!attackAnimState) {
|
||
console.warn(`怪物攻击动画 ${otherCollider.node.name}_attack 不存在,跳过怪物攻击动画`);
|
||
return;
|
||
}
|
||
|
||
return new Promise<void>((resolve) => {
|
||
// 添加超时机制,防止动画卡住
|
||
const timeout = setTimeout(() => {
|
||
console.log('怪物攻击动画超时,强制继续');
|
||
// 确保怪物切换回站立状态
|
||
this.switchMonsterToStand(otherCollider.node.name, monsterAnimation);
|
||
resolve();
|
||
}, 3000); // 3秒超时
|
||
|
||
monsterAnimation.play(`${otherCollider.node.name}_attack`);
|
||
console.log('开始播放怪物攻击动画');
|
||
|
||
// 监听怪物攻击动画结束
|
||
monsterAnimation.once(Animation.EventType.FINISHED, () => {
|
||
clearTimeout(timeout);
|
||
console.log('怪物攻击动画完成,切换回站立状态');
|
||
// 怪物攻击完成后切换回站立动画
|
||
this.switchMonsterToStand(otherCollider.node.name, monsterAnimation);
|
||
resolve();
|
||
});
|
||
});
|
||
} else {
|
||
console.warn('未找到怪物动画组件,跳过怪物攻击动画');
|
||
// 如果没有怪物动画组件,直接返回
|
||
return;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将怪物切换到站立动画
|
||
*/
|
||
private switchMonsterToStand(monsterName: string, monsterAnimation: Animation) {
|
||
const standAnimName = `${monsterName}_stand`;
|
||
const standAnimState = monsterAnimation.getState(standAnimName);
|
||
|
||
if (standAnimState) {
|
||
monsterAnimation.play(standAnimName);
|
||
console.log(`怪物切换到站立动画: ${standAnimName}`);
|
||
} else {
|
||
console.warn(`怪物站立动画 ${standAnimName} 不存在`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理玩家获胜的情况
|
||
*/
|
||
private async handlePlayerWin(otherCollider: Collider2D, playerLabel: Label, monsterHp: number) {
|
||
const hit = otherCollider.node.getChildByName('Hit');
|
||
if (hit) {
|
||
hit.active = true;
|
||
}
|
||
|
||
this.hasWinTimes++;
|
||
// 玩家获胜
|
||
console.log('玩家获胜!更新玩家生命值为:', parseInt(playerLabel.string) + monsterHp);
|
||
|
||
// 玩家生命值增加怪物生命值
|
||
const playerHp = parseInt(playerLabel.string) || 0;
|
||
const newPlayerHp = playerHp + monsterHp;
|
||
playerLabel.string = newPlayerHp.toString();
|
||
|
||
// 播放生命值标签的强调动画
|
||
this.playLabelEmphasisAnimation(playerLabel);
|
||
|
||
// 播放怪物死亡动画
|
||
const animNode = otherCollider.node.getChildByName('Anim');
|
||
const monsterAnimation = animNode ? animNode.getComponent(Animation) : null;
|
||
if (monsterAnimation) {
|
||
monsterAnimation.play(`${otherCollider.node.name}_die`);
|
||
}
|
||
|
||
// 如果是攻击 guai_2 并且成功,创建道具飞向 player 的动画
|
||
if (otherCollider.node.name === 'guai_2') {
|
||
this.switchAnimation('stand'); // 玩家站立
|
||
await this.createPropsFlyToPlayerAnimation();
|
||
}
|
||
|
||
// 1秒后怪物消失
|
||
this.scheduleOnce(() => {
|
||
if (!otherCollider.node || !otherCollider.node.isValid) {
|
||
return;
|
||
}
|
||
|
||
console.log('怪物已消失');
|
||
otherCollider.node.destroy();
|
||
|
||
if (this.hasWinTimes === 7) {
|
||
this.isWin = true;
|
||
this.showBonusPopup();
|
||
}
|
||
}, 1);
|
||
|
||
this.switchAnimation('stand'); // 玩家站立
|
||
}
|
||
|
||
/**
|
||
* 处理怪物攻击的情况
|
||
*/
|
||
private async handleMonsterAttack(otherCollider: Collider2D, playerLabel: Label, monsterHp: number) {
|
||
// 播放怪物攻击动画
|
||
const animNode = otherCollider.node.getChildByName('Anim');
|
||
const monsterAnimation = animNode ? animNode.getComponent(Animation) : null;
|
||
|
||
if (monsterAnimation) {
|
||
// 创建Promise来监听怪物攻击动画结束
|
||
return new Promise<void>((resolve) => {
|
||
monsterAnimation.play(`${otherCollider.node.name}_attack`);
|
||
|
||
// 监听怪物攻击动画结束
|
||
monsterAnimation.once(Animation.EventType.FINISHED, () => {
|
||
// 怪物攻击动画结束后,执行玩家死亡逻辑
|
||
this.executePlayerDefeat(otherCollider, playerLabel);
|
||
resolve();
|
||
});
|
||
});
|
||
} else {
|
||
// 如果没有怪物动画组件,直接执行玩家死亡逻辑
|
||
this.executePlayerDefeat(otherCollider, playerLabel);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行玩家失败逻辑
|
||
*/
|
||
private executePlayerDefeat(otherCollider: Collider2D, playerLabel: Label) {
|
||
// 怪物获胜
|
||
console.log('怪物获胜!玩家生命值变为0');
|
||
|
||
// 玩家生命值变为0
|
||
playerLabel.string = '0';
|
||
|
||
// 播放生命值标签的失败动画
|
||
this.playLabelFailAnimation(playerLabel);
|
||
|
||
// 玩家死亡动画(只传递基础动画名称)
|
||
this.switchAnimation('die');
|
||
|
||
// 怪物站立动画
|
||
const animNode = otherCollider.node.getChildByName('Anim');
|
||
const monsterAnimation = animNode ? animNode.getComponent(Animation) : null;
|
||
if (monsterAnimation) {
|
||
monsterAnimation.play(`${otherCollider.node.name}_stand`);
|
||
}
|
||
|
||
// 设置游戏结束标志,禁止后续寻路
|
||
this.isGameOver = true;
|
||
console.log('游戏结束,禁止寻路');
|
||
|
||
// 显示失败弹窗
|
||
this.scheduleOnce(() => {
|
||
this.showFailedDialog();
|
||
}, 1); // 延迟1秒显示失败弹窗,让玩家死亡动画播放完成
|
||
}
|
||
|
||
private async handleBoxCollision(selfCollider: Collider2D, otherCollider: Collider2D) {
|
||
if (!this.player) {
|
||
return;
|
||
}
|
||
|
||
const boxNode = otherCollider.node;
|
||
if (!boxNode || !boxNode.isValid) {
|
||
return;
|
||
}
|
||
|
||
this.stopMovement();
|
||
|
||
await this.alignPlayerForAttack(selfCollider, otherCollider);
|
||
|
||
if (!this.player || !boxNode.isValid) {
|
||
return;
|
||
}
|
||
|
||
this.switchAnimation('stand');
|
||
|
||
const playerHpNode = this.player.getChildByName('hp');
|
||
const boxHpNode = boxNode.getChildByName('hp');
|
||
|
||
if (!playerHpNode || !boxHpNode) {
|
||
console.warn('未找到玩家或宝箱的hp节点', playerHpNode, boxHpNode);
|
||
return;
|
||
}
|
||
|
||
const playerLabel = playerHpNode.getComponent(Label);
|
||
const boxLabel = boxHpNode.getComponent(Label);
|
||
|
||
if (!playerLabel || !boxLabel) {
|
||
console.warn('未找到玩家或宝箱的hp标签组件');
|
||
return;
|
||
}
|
||
|
||
const rewardValue = parseInt(boxLabel.string) || 0;
|
||
const animNode = boxNode.getChildByName('Anim');
|
||
const animation = animNode ? animNode.getComponent(Animation) : null;
|
||
|
||
const finalizeBoxOpen = () => {
|
||
if (!playerLabel.isValid) {
|
||
return;
|
||
}
|
||
|
||
const currentPlayerHp = parseInt(playerLabel.string) || 0;
|
||
const updatedPlayerHp = currentPlayerHp + rewardValue;
|
||
playerLabel.string = updatedPlayerHp.toString();
|
||
|
||
this.playLabelEmphasisAnimation(playerLabel);
|
||
|
||
if (boxNode && boxNode.isValid) {
|
||
boxNode.destroy();
|
||
}
|
||
};
|
||
|
||
if (animation) {
|
||
animation.play('open');
|
||
animation.once(Animation.EventType.FINISHED, () => {
|
||
finalizeBoxOpen();
|
||
});
|
||
} else {
|
||
finalizeBoxOpen();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
/**
|
||
* 播放生命值标签强调动画(成功时)
|
||
*/
|
||
private playLabelEmphasisAnimation(label: Label) {
|
||
if (!label) return;
|
||
|
||
const originalScale = label.node.scale.clone();
|
||
const originalColor = label.color.clone();
|
||
|
||
// 创建强调动画序列
|
||
tween(label.node)
|
||
.to(0.1, { scale: new Vec3(originalScale.x * 1.2, originalScale.y * 1.2, originalScale.z) })
|
||
.to(0.1, { scale: originalScale })
|
||
.to(0.1, { scale: new Vec3(originalScale.x * 1.1, originalScale.y * 1.1, originalScale.z) })
|
||
.to(0.1, { scale: originalScale })
|
||
.start();
|
||
|
||
// 颜色闪烁效果
|
||
tween(label)
|
||
.to(0.1, { color: new Color(255, 255, 0) }) // 黄色
|
||
.to(0.1, { color: new Color(0, 255, 0) }) // 绿色
|
||
.to(0.1, { color: originalColor })
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 播放生命值标签失败动画(失败时)
|
||
*/
|
||
private playLabelFailAnimation(label: Label) {
|
||
if (!label) return;
|
||
|
||
const originalScale = label.node.scale.clone();
|
||
const originalColor = label.color.clone();
|
||
|
||
// 创建失败动画序列 - 震动效果
|
||
tween(label.node)
|
||
.to(0.05, { position: new Vec3(label.node.position.x - 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: new Vec3(label.node.position.x + 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: new Vec3(label.node.position.x - 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: new Vec3(label.node.position.x + 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: label.node.position })
|
||
.start();
|
||
|
||
// 颜色变红效果
|
||
tween(label)
|
||
.to(0.1, { color: new Color(255, 0, 0) }) // 红色
|
||
.to(0.1, { color: new Color(128, 0, 0) }) // 暗红色
|
||
.to(0.1, { color: originalColor })
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 创建道具飞向玩家的动画(同步方法)
|
||
*/
|
||
private async createPropsFlyToPlayerAnimation(): Promise<void> {
|
||
if (!this.player || this.props.length === 0) {
|
||
console.warn('玩家或道具不存在,无法创建飞行动画');
|
||
return;
|
||
}
|
||
|
||
console.log('创建道具飞向玩家的动画');
|
||
|
||
// 获取玩家节点的世界坐标(中心位置)
|
||
const playerWorldPos = this.player.worldPosition.clone();
|
||
|
||
// 创建所有道具的飞行动画承诺
|
||
const flyPromises: Promise<void>[] = [];
|
||
|
||
// 为每个道具创建飞行动画
|
||
this.props.forEach((prop, index) => {
|
||
if (!prop || !prop.isValid) return;
|
||
|
||
// 保存道具原始位置
|
||
const originalPos = prop.position.clone();
|
||
|
||
// 计算飞行时间(根据距离调整)
|
||
const distance = Vec3.distance(originalPos, playerWorldPos);
|
||
const flyDuration = Math.max(0.5, distance / 500); // 最少0.5秒,速度500像素/秒
|
||
|
||
// 添加延迟,让道具依次飞向玩家
|
||
const delay = index * 0.1;
|
||
|
||
// 停止道具的悬浮动画
|
||
tween(prop).stop();
|
||
|
||
// 创建飞行动画的承诺
|
||
const flyPromise = new Promise<void>((resolve) => {
|
||
this.scheduleOnce(() => {
|
||
// 将玩家的世界坐标转换为道具父节点的本地坐标
|
||
const propParent = prop.parent;
|
||
let targetPos: Vec3;
|
||
|
||
if (propParent) {
|
||
const parentTransform = propParent.getComponent(UITransform);
|
||
if (parentTransform) {
|
||
targetPos = parentTransform.convertToNodeSpaceAR(playerWorldPos);
|
||
} else {
|
||
// 如果没有UITransform组件,使用简单的坐标转换
|
||
targetPos = new Vec3(
|
||
playerWorldPos.x - propParent.worldPosition.x,
|
||
playerWorldPos.y - propParent.worldPosition.y,
|
||
playerWorldPos.z - propParent.worldPosition.z
|
||
);
|
||
}
|
||
} else {
|
||
// 如果道具没有父节点,直接使用世界坐标
|
||
targetPos = playerWorldPos.clone();
|
||
}
|
||
|
||
tween(prop)
|
||
.to(flyDuration, {
|
||
position: targetPos
|
||
}, {
|
||
easing: 'quadOut',
|
||
onComplete: () => {
|
||
prop.active = false;
|
||
resolve();
|
||
}
|
||
})
|
||
.start();
|
||
}, delay);
|
||
});
|
||
|
||
flyPromises.push(flyPromise);
|
||
});
|
||
|
||
// 等待所有道具飞行动画完成
|
||
await Promise.all(flyPromises);
|
||
|
||
// 所有动画完成后,播放升级动画并设置升级状态
|
||
this.playLevelUpAnimation();
|
||
this.isUpgraded = true;
|
||
this.showWeaponBonusPopup();
|
||
console.log('所有道具飞行动画完成,玩家已升级,后续动画将使用升级版本');
|
||
}
|
||
|
||
/**
|
||
* 播放玩家升级动画
|
||
*/
|
||
private playLevelUpAnimation() {
|
||
if (!this.player) {
|
||
console.warn('玩家节点不存在,无法播放升级动画');
|
||
return;
|
||
}
|
||
|
||
// 查找levelUp子节点
|
||
const levelUpNode = this.player.getChildByName('levelUp');
|
||
if (!levelUpNode) {
|
||
console.warn('未找到levelUp子节点');
|
||
return;
|
||
}
|
||
|
||
// 获取动画组件
|
||
const levelUpAnimation = levelUpNode.getComponent(Animation);
|
||
if (!levelUpAnimation) {
|
||
console.warn('levelUp节点未找到Animation组件');
|
||
return;
|
||
}
|
||
|
||
console.log('播放玩家升级动画');
|
||
|
||
// 播放升级动画
|
||
levelUpAnimation.play('levelUp');
|
||
}
|
||
|
||
/**
|
||
* 显示奖励弹窗
|
||
* 根据当前镜头位置和正交高度,将奖励节点正确缩放并移动到画面正中间
|
||
*/
|
||
public showBonusPopup() {
|
||
this.showPopupAtCameraCenter(this.bonus, '奖励弹窗');
|
||
}
|
||
|
||
/**
|
||
* 弹出武器奖励
|
||
*/
|
||
public showWeaponBonusPopup() {
|
||
// this.showPopupAtCameraCenter(this.bonusWuqi, '武器奖励');
|
||
}
|
||
|
||
private showPopupAtCameraCenter(popup: Node | null, nameForLog: string) {
|
||
if (!popup || !this.camera || !this.canvas) {
|
||
console.warn(`${nameForLog}节点、相机或画布未设置,无法显示${nameForLog}`);
|
||
return;
|
||
}
|
||
|
||
this.clearPopupHideSchedule();
|
||
|
||
popup.active = true;
|
||
|
||
const cameraPos = this.camera.node.position;
|
||
const orthoHeight = this.camera.orthoHeight;
|
||
|
||
popup.setPosition(cameraPos.x, cameraPos.y, 0);
|
||
|
||
const baseScale = 0.8;
|
||
const standardOrthoHeight = 500;
|
||
const scaleRatio = standardOrthoHeight / orthoHeight;
|
||
const finalScale = baseScale * scaleRatio;
|
||
|
||
popup.setScale(finalScale, finalScale, 1);
|
||
|
||
this.activePopup = popup;
|
||
this.activePopupName = nameForLog;
|
||
|
||
this.playPopupAppearAnimation(popup, nameForLog);
|
||
|
||
this.pendingPopupHide = () => {
|
||
this.pendingPopupHide = null;
|
||
this.hideActivePopup();
|
||
};
|
||
|
||
// this.scheduleOnce(this.pendingPopupHide, 3);
|
||
}
|
||
|
||
/**
|
||
* 播放奖励弹窗出现动画
|
||
*/
|
||
private playPopupAppearAnimation(popup: Node, nameForLog: string) {
|
||
const originalScale = popup.scale.clone();
|
||
|
||
popup.setScale(0.1, 0.1, 1);
|
||
|
||
tween(popup)
|
||
.to(0.3, {
|
||
scale: new Vec3(originalScale.x * 1.2, originalScale.y * 1.2, originalScale.z)
|
||
}, {
|
||
easing: 'backOut'
|
||
})
|
||
.to(0.1, {
|
||
scale: originalScale
|
||
}, {
|
||
easing: 'sineInOut'
|
||
})
|
||
.call(() => {
|
||
console.log(`${nameForLog}显示完成`);
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 隐藏奖励弹窗
|
||
*/
|
||
public hideBonusPopup() {
|
||
if (!this.bonus) return;
|
||
|
||
if (this.activePopup === this.bonus) {
|
||
this.hideActivePopup();
|
||
return;
|
||
}
|
||
|
||
if (!this.bonus.active) return;
|
||
|
||
this.hidePopupWithAnimation(this.bonus, '奖励弹窗');
|
||
}
|
||
|
||
private hideActivePopup() {
|
||
if (!this.activePopup) {
|
||
return;
|
||
}
|
||
|
||
const popupToHide = this.activePopup;
|
||
const nameForLog = this.activePopupName || '弹窗';
|
||
|
||
this.clearPopupHideSchedule();
|
||
|
||
this.activePopup = null;
|
||
this.activePopupName = null;
|
||
|
||
this.hidePopupWithAnimation(popupToHide, nameForLog);
|
||
}
|
||
|
||
private hidePopupWithAnimation(popup: Node, nameForLog: string) {
|
||
tween(popup).stop();
|
||
|
||
tween(popup)
|
||
.to(0.2, {
|
||
scale: new Vec3(0.1, 0.1, 1)
|
||
}, {
|
||
easing: 'backIn'
|
||
})
|
||
.call(() => {
|
||
popup.active = false;
|
||
console.log(`${nameForLog}已隐藏`);
|
||
})
|
||
.start();
|
||
}
|
||
|
||
private clearPopupHideSchedule() {
|
||
if (this.pendingPopupHide) {
|
||
this.unschedule(this.pendingPopupHide);
|
||
this.pendingPopupHide = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示失败弹窗
|
||
*/
|
||
public showFailedDialog() {
|
||
this.showPopupAtCameraCenter(this.failedDialog, '失败弹窗');
|
||
this.setupRetryButtonListener();
|
||
}
|
||
|
||
/**
|
||
* 设置重试按钮监听器
|
||
*/
|
||
private setupRetryButtonListener() {
|
||
if (!this.failedDialog) {
|
||
console.warn('失败弹窗节点未设置,无法监听重试按钮');
|
||
return;
|
||
}
|
||
|
||
// 查找Retry按钮节点
|
||
const retryButton = this.failedDialog.getChildByName('Retry');
|
||
if (!retryButton) {
|
||
console.warn('未找到Retry按钮节点');
|
||
return;
|
||
}
|
||
|
||
// 移除之前的监听器(如果存在)
|
||
retryButton.off(Node.EventType.TOUCH_END, this.onRetryButtonClick, this);
|
||
|
||
// 添加新的监听器
|
||
retryButton.on(Node.EventType.TOUCH_END, this.onRetryButtonClick, this);
|
||
console.log('已设置重试按钮监听器');
|
||
}
|
||
|
||
/**
|
||
* 重试按钮点击事件处理
|
||
*/
|
||
private onRetryButtonClick() {
|
||
console.log('重试按钮被点击,重新加载当前场景');
|
||
|
||
// 隐藏失败弹窗
|
||
if (this.failedDialog) {
|
||
this.hidePopupWithAnimation(this.failedDialog, '失败弹窗');
|
||
}
|
||
|
||
// 重新加载当前场景
|
||
this.scheduleOnce(() => {
|
||
// 使用Cocos Creator的场景管理器重新加载当前场景
|
||
const sceneName = director.getScene().name;
|
||
director.loadScene(sceneName);
|
||
}, 0.3); // 延迟0.3秒,让弹窗消失动画完成
|
||
}
|
||
}
|