import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween, Animation, Collider2D, BoxCollider2D, Contact2DType, Label, Color, Canvas, UITransform, AudioSource, director, PhysicsSystem2D, EPhysics2DDrawFlags, sys } 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) { 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 { 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((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((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((resolve) => { this.scheduleOnce(() => { resolve(); }, 0.2); }); // 第三轮:玩家再次攻击 console.log('第三轮:玩家再次攻击'); this.switchAnimation('attack'); // 等待玩家攻击动画结束 await new Promise((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 { 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((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((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 { if (!this.player || this.props.length === 0) { console.warn('玩家或道具不存在,无法创建飞行动画'); return; } console.log('创建道具飞向玩家的动画'); // 获取玩家节点的世界坐标(中心位置) const playerWorldPos = this.player.worldPosition.clone(); // 创建所有道具的飞行动画承诺 const flyPromises: Promise[] = []; // 为每个道具创建飞行动画 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((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, '奖励弹窗'); this.setupEnterButtonListener(); } /** * 弹出武器奖励 */ 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秒,让弹窗消失动画完成 } /** * 设置Enter按钮监听器 */ private setupEnterButtonListener() { if (!this.bonus) { console.warn('奖励弹窗节点未设置,无法监听Enter按钮'); return; } // 查找Enter按钮节点 const enterButton = this.bonus.getChildByName('Enter'); if (!enterButton) { console.warn('未找到Enter按钮节点'); return; } // 移除之前的监听器(如果存在) enterButton.off(Node.EventType.TOUCH_END, this.onEnterButtonClick, this); // 添加新的监听器 enterButton.on(Node.EventType.TOUCH_END, this.onEnterButtonClick, this); console.log('已设置Enter按钮监听器'); } /** * Enter按钮点击事件处理 */ private onEnterButtonClick() { console.log('Enter按钮被点击,执行微信小游戏状态通知'); // 隐藏奖励弹窗 if (this.bonus) { this.hidePopupWithAnimation(this.bonus, '奖励弹窗'); } // 执行微信小游戏状态通知代码 if (sys.platform === sys.Platform.WECHAT_GAME && (window as any).wx?.notifyMiniProgramPlayableStatus) { (window as any).wx.notifyMiniProgramPlayableStatus({ isEnd: true }); console.log('已发送微信小游戏结束状态通知'); } } }