Files
climb/assets/scripts/PlayerController.ts
2025-10-20 09:22:47 +08:00

1456 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween, Animation, Collider2D, BoxCollider2D, Contact2DType, Label, Color, Canvas, UITransform, AudioSource, director } 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 = 10; // 玩家与怪物对阵时的额外左右间距,单位:像素
// 道具列表
private props: Node[] = [];
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);
}
// 使用寻路算法计算路径
const startPos = this.player.position;
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;
// 根据移动方向选择动画
const animationName = this.getAnimationNameByDirection(startPos, clampedPos);
// 切换到对应的动画
this.switchAnimation(animationName);
// 使用平滑路径移动
this.startSmoothPathMovement();
}
// 限制玩家位置在地图边界内
private clampPlayerPosition(position: Vec3): Vec3 {
return this.clampPositionWithinMap(position);
}
private clampPositionWithinMap(position: Vec3): Vec3 {
// 限制位置
const clampedPosition = position.clone();
return clampedPosition;
}
/**
* 根据移动方向获取对应的动画名称
* 现在只返回基础动画名称,不包含方向信息
*/
private getAnimationNameByDirection(currentPos: Vec3, targetPos: Vec3): string {
const deltaX = targetPos.x - currentPos.x;
const deltaY = targetPos.y - currentPos.y;
// 如果移动距离很小,保持当前动画
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < 1) {
if (this.isCurrentAction('walk')) {
return 'walk';
}
return 'stand';
}
this.currentDirection = this.resolveDirectionFromDelta(deltaX, deltaY);
// 只返回基础动画名称,不包含方向信息
return 'walk';
}
/**
* 切换动画,避免不必要的切换
* 根据 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 isCurrentAction(action: string): boolean {
if (!this.currentAnimation) {
return false;
}
return this.normalizeAnimationAction(this.currentAnimation) === action;
}
private resolveDirectionFromDelta(
deltaX: number,
deltaY: number,
overrides?: { horizontal?: 'Left' | 'Right', vertical?: 'Up' | 'Down' }
): PlayerDirection {
const horizontalThreshold = 0.5;
const verticalThreshold = 0.5;
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 moveToNextWaypoint() {
if (this.isCurrentAction('attack') || this.isAttacking) {
return;
}
if (!this.player || this.currentPath.length === 0 || this.currentPathIndex >= this.currentPath.length) {
this.isMoving = false;
this.switchAnimation('stand');
console.log('路径移动完成');
return;
}
// 停止当前的移动tween
if (this.moveTween) {
this.moveTween.stop();
this.moveTween = null;
}
const targetPos = this.currentPath[this.currentPathIndex];
const currentPos = this.player.position;
// 计算移动距离和时间
const distance = Vec3.distance(currentPos, targetPos);
const moveTime = distance / this.moveSpeed;
console.log(`移动到路径点${this.currentPathIndex}: (${targetPos.x.toFixed(2)}, ${targetPos.y.toFixed(2)})`);
// 记录目标位置用于方向判断
this.lastTargetPosition.set(targetPos);
this.lastPosition.set(currentPos);
// 在移动前计算并设置方向(只计算一次)
this.updateMovementDirectionOnce(currentPos, targetPos);
// 使用缓动移动到目标位置
this.moveTween = tween(this.player)
.to(moveTime, { position: targetPos }, {
easing: 'linear', // 使用线性插值,保持匀速移动
onComplete: () => {
this.currentPathIndex++;
this.moveToNextWaypoint();
}
})
.start();
}
/**
* 开始平滑路径移动
*/
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)}`);
// 在移动前计算并设置方向(只计算一次)
const startPos = this.player.position.clone();
const finalTargetPos = this.currentPath[this.currentPath.length - 1];
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;
}
},
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 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');
}
update(deltaTime: number) {
// 更新逻辑现在主要由缓动系统处理
// 这里可以添加其他需要每帧更新的逻辑
}
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) {
console.log('碰撞检测', selfCollider.node.name, otherCollider.node.name);
// 禁用碰撞器,防止重复触发
otherCollider.enabled = false;
if (otherCollider.node.name.startsWith('guai_')) {
void this.handleAttack(selfCollider, otherCollider);
} else if (otherCollider.node.name.startsWith('box_')) {
void 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,
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) {
const audioSource = this.attackAudio.getComponent(AudioSource);
if (audioSource) {
audioSource.play();
console.log('播放攻击音效');
}
}
// 获取玩家动画组件
const playerAnimNode = this.player.getChildByName('Anim');
const playerAnimation = playerAnimNode ? playerAnimNode.getComponent(Animation) : null;
if (!playerAnimation) {
console.warn('未找到玩家动画组件');
this.isAttacking = false;
return;
}
// 播放玩家攻击动画(只传递基础动画名称)
this.switchAnimation('attack');
// 监听玩家攻击动画结束事件
playerAnimation.once(Animation.EventType.FINISHED, async () => {
if (!this.player || !playerLabel.isValid || !monsterLabel.isValid) {
this.isAttacking = false;
if (this.attackAudio) {
const audioSource = this.attackAudio.getComponent(AudioSource);
if (audioSource) {
audioSource.stop();
}
}
return;
}
// 比较生命值,判断输赢
console.log('判定攻击结果玩家HP:', playerHp, '怪物HP:', monsterHp);
if (playerHp >= monsterHp) {
// 玩家获胜,直接执行后续流程,不需要播放怪物攻击动画
await this.handlePlayerWin(otherCollider, playerLabel, monsterHp);
} else {
// 玩家输了,需要播放怪物攻击动画
await this.handleMonsterAttack(otherCollider, playerLabel, monsterHp);
}
this.isAttacking = false;
// 停止攻击音效
if (this.attackAudio) {
const audioSource = this.attackAudio.getComponent(AudioSource);
if (audioSource) {
audioSource.stop();
console.log('停止攻击音效');
}
}
});
}
/**
* 处理玩家获胜的情况
*/
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;
}
// 防止重复触发
otherCollider.enabled = false;
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();
}
}
private setNodeWorldPosition(node: Node, worldPos: Vec3) {
const parent = node.parent;
if (!parent) {
node.setWorldPosition(worldPos);
return;
}
const parentTransform = parent.getComponent(UITransform);
if (parentTransform) {
const localResult = parentTransform.convertToNodeSpaceAR(new Vec3(worldPos.x, worldPos.y, worldPos.z));
node.setPosition(localResult);
return;
}
node.setWorldPosition(worldPos);
}
/**
* 播放生命值标签强调动画(成功时)
*/
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秒,让弹窗消失动画完成
}
}