import { _decorator, EventTouch, Node, Quat, math, Camera, sys, Vec3, Label, geometry, ProgressBar, tween, easing, Tween, UIOpacity, game, PhysicsSystem, PhysicsRayResult } from 'cc'; import { Game } from '../game/Game'; import { BaseExp } from '../core/base/BaseExp'; import { autoBind } from '../extend/AutoBind'; import { Constants } from '../data/Constants'; import { uiMgr } from '../core/manager/UIManager'; import { userIns } from '../data/UserData'; import MsgHints from '../utils/MsgHints'; import List from '../third/List'; import { Utils } from '../utils/Utils'; import { TaskEnemyItem } from '../items/item/TaskEnemyItem'; import { BulletMagazine } from '../game/BulletMagazine'; import { audioMgr } from '../core/manager/AudioManager'; const { ccclass, property } = _decorator; const { clamp, toRadian } = math; @ccclass('GunfightShootUI') export class GunfightShootUI extends BaseExp { @autoBind({ type: Node, tooltip: "轮盘节点" }) public wheel: Node; @autoBind({ type: Node, tooltip: "标准贴图" }) public scopeOverlay: Node; @autoBind({ type: Node, tooltip: "准心" }) public crossHair: Node; @autoBind({ type: ProgressBar, tooltip: "步枪子弹进度条" }) public rifle_bullet_progressBar: ProgressBar; @autoBind({ type: Label, tooltip: "步枪子弹数文本" }) public rifle_bullet_num_label: Label; @autoBind({ type: BulletMagazine, tooltip: "步枪子弹列表" }) public rifle_bullets_scrollView: BulletMagazine; @autoBind({ type: BulletMagazine, tooltip: "狙击枪子弹列表" }) public snipe_bullets_scrollView: BulletMagazine; @autoBind({ type: Node, tooltip: "步枪显示子弹的总的节点" }) public rifle_bullets_bg: Node; @autoBind({ type: List, tooltip: "敌人任务列表" }) public task_scrollView: List; @autoBind({ type: ProgressBar, tooltip: "玩家生命进度条" }) public hpProgressBar: ProgressBar; @property({type: [Node], tooltip: "要隐藏的所有按钮"}) public hiddeNodes: Array = []; @autoBind({ type: ProgressBar, tooltip: "换弹夹时间进度条" }) public reloadProgressBar: ProgressBar; @autoBind({ type: Node, tooltip: "玩家受到伤害呼吸闪烁" }) public injury_blood: Node; @autoBind({ type: Label, tooltip: "玩家枪的名字" }) public gun_name_label: Label; /** 子弹飞行距离 */ public bulletDistance: number = 800; /** 当前相机绕 X 轴的旋转角度 */ private currentXRotation: number = 0; /** 当前相机绕 Y 轴的旋转角度 */ private currentYRotation: number = 0; /** 左右旋转角度的最大限制值 */ private maxHorizontalAngle: number = 70; /** 上下旋转角度的最大限制值 */ private maxVerticalAngle: number = 40; /** 相机的原始视野值 */ private originalFov: number; /** 玩家节点的初始旋转四元数 */ private initialPlayerRotation: Quat = new Quat(); /** 相机节点的初始旋转四元数 */ private initialCameraRotation: Quat = new Quat(); /**相机旋转的灵敏度 值越大移动越快 灵敏度越高*/ private sensitivity: number = 0.5; /** 相机放大后的目标视野值 */ private targetFov: number; /** 标记相机是否正在进行放大操作 */ private isZoomingIn: boolean = false; /** 标记相机是否正在进行缩小操作 */ private isZoomingOut: boolean = false; /** 相机视野放大的速度 */ private zoomSpeed: number = 0; /** 期望的缩放时间,单位为秒 */ private zoomDuration: number = 0.2; /** 记录镜头开始放大的时间 */ private zoomStartTime: number = 0; /** 标记是否在2秒内完成放大 */ private zoomValid: boolean = false; /** 标记是否开镜 */ private _isScopeOpen: boolean = false; get isScopeOpen() { return this._isScopeOpen; } set isScopeOpen(value: boolean) { this.gunDataUI(); if (this._isScopeOpen !== value) { this._isScopeOpen = value; if (value) { this.openScope(); } else { this.closeScope(); } } } /**镜头的位置*/ private wheelPos: Vec3 = Vec3.ZERO; //任务数据 private taskDatas: Array = []; //是否正在被击中 private isHurtRun: boolean = false; start() { this.hasAnim = false; this.closeOnBlank = false; this.injury_blood.active = false; this.reloadProgressBar.node.active = false; if(!Constants.isDebug){ this.hiddeNodes.forEach(e => e.active = false); } this.wheelPos = this.wheel.position.clone(); //记录摄像机原始视野 this.originalFov = Game.I.camera.getComponent(Camera).fov; //录初始旋转角度 this.initialPlayerRotation = Game.I.player.node.rotation.clone(); this.initialCameraRotation = Game.I.camera.node.rotation.clone(); //监听当前节点触摸事件 this.node.on(Node.EventType.TOUCH_START, this.onNodeTouchMove, this); this.node.on(Node.EventType.TOUCH_MOVE, this.onNodeTouchMove, this); this.node.on(Node.EventType.TOUCH_END, this.onCustomNodeTouchEnd, this); this.node.on(Node.EventType.TOUCH_CANCEL, this.onCustomNodeTouchEnd, this); //监听轮盘点击事件 监听轮盘的触摸开始事件,当触摸开始时调用 onWheelClick 方法 this.wheel.on(Node.EventType.TOUCH_START, this.onWheelClick, this); this.wheel.on(Node.EventType.TOUCH_END, this.onWheelRelease, this); this.wheel.on(Node.EventType.TOUCH_CANCEL, this.onWheelRelease, this); //敌人数量改变 this.register(Constants.eventName.enemy_num_change, this.loadTaskData.bind(this)); } public show(...args: any[]){ this.loadTaskData(); } /** * enemyId total count enemyData * 加载数据 */ public loadTaskData() { this.taskDatas = Game.I.buildEnemys.enemyTypeRecords; this.task_scrollView.numItems = this.taskDatas.length; //加载子弹的数据 this.gunDataUI(); } /** * 任务数据 * @param item item节点 * @param idx 数据下标 */ public setTaskItemData(item: Node, idx: number) { item.getComponent(TaskEnemyItem).init(this.taskDatas[idx]); } /** * 设置枪的数据 */ public gunDataUI() { const gData:any = Game.I.player.pData; if(!gData)return; this.crossHair.active = true;//准心 //步枪没有开镜贴图 this.scopeOverlay.active = !this.isRifleGun(); this.gun_name_label.string = gData.name_lang; const isSnipeGun: boolean = gData.type == 1; //换弹夹进度条 const isMagazine: boolean = Game.I.player.isReloadMagazine; if(isMagazine){ this.rifle_bullets_bg.active = false; this.snipe_bullets_scrollView.node.active = false; }else{ //步枪子弹视图 this.rifle_bullets_bg.active = !isSnipeGun; //狙击子弹视图列表 this.snipe_bullets_scrollView.node.active = isSnipeGun; if(isSnipeGun){ this.snipe_bullets_scrollView.magazineNum = gData.magazine; }else{ this.rifle_bullets_scrollView.magazineNum = gData.magazine; //子弹进度条 let s_bullet: number = gData.magazine - Game.I.player.gun.shotBullets; this.rifle_bullet_progressBar.progress = s_bullet / gData.magazine; this.rifle_bullet_num_label.string = `${s_bullet}/${gData.magazine}`; } } } /** * 更换弹夹动画 * @param time 换弹时间 单位秒 * @param complete 完成回调 */ public reloadMagazineing(time: number,complete?: Function) { if(time <= 0){ complete?.(); }else{ this.reloadProgressBar.node.active = true; this.reloadProgressBar.progress = 1; this.rifle_bullets_bg.active = false; this.snipe_bullets_scrollView.node.active = false; //创建定时器动画 tween(this.reloadProgressBar) .to(time, { progress: 0 }, { easing: easing.linear, onStart: () => { tween(this.reloadProgressBar).stop(); }, onComplete: () => { this.reloadProgressBar.node.active = false; complete?.(); } }) .start(); } } /** * 根据切换枪的stability设置镜头的稳定性 */ public crossHairStability() { const gData:any = Game.I.player.pData; if(!gData)return; this.gunDataUI(); this.wheel.position = this.wheelPos; //参数映射优化(假设stability范围0-1) 振幅稳定性越低振幅越大 频率稳定性越低抖动越快 const amplitude = 20 * (1 - gData.stability / 1000); let elapsed = 0; //基础抖动持续时间 const duration = 0.4; const updateShake = (deltaTime: number) => { elapsed += deltaTime; //双轴随机偏移 const offsetX = amplitude * (Math.random() - 0.5) * Math.max(1 - elapsed/duration, 0); const offsetY = amplitude * (Math.random() - 0.5) * Math.max(1 - elapsed/duration, 0); this.wheel.position = new Vec3( this.wheelPos.x + offsetX, this.wheelPos.y + offsetY, this.wheelPos.z ); //自动结束 if(elapsed >= duration) { this.wheel.position = this.wheelPos; this.unschedule(updateShake); } }; this.unschedule(updateShake); // 使用游戏时间而非系统时间 this.schedule(updateShake, 0.02); // 每0.02秒更新(约50FPS) } /** * 屏幕触摸事件 * 触摸移动时,根据触摸的位置计算旋转角度,并应用到节点上 * @param event */ private onNodeTouchMove(event: EventTouch) { const delta = event.getDelta(); //计算旋转角度 this.currentYRotation -= delta.x * this.sensitivity; this.currentXRotation -= delta.y * this.sensitivity; //分别限制左右和上下旋转角度 this.currentYRotation = clamp(this.currentYRotation, -this.maxHorizontalAngle, this.maxHorizontalAngle); this.currentXRotation = clamp(this.currentXRotation, -this.maxVerticalAngle, this.maxVerticalAngle); //计算水平和垂直旋转的四元数 const yQuat = new Quat(); Quat.fromEuler(yQuat, 0, this.currentYRotation, 0); const xQuat = new Quat(); Quat.fromEuler(xQuat, +this.currentXRotation, 0, 0); //合并旋转 let finalQuat = new Quat(); Quat.multiply(finalQuat, yQuat, xQuat); //将最终旋转与初始摄像机旋转合并 Quat.multiply(finalQuat, this.initialPlayerRotation, finalQuat); //应用旋转到摄像机 Game.I.player.node.rotation = finalQuat; } /** * 触摸结束 * 触摸结束时 触摸结束处理方法 */ private onCustomNodeTouchEnd() { //触摸结束,可添加其他逻辑 } /** * 开始慢慢放大视野 */ private onWheelClick() { this.isScopeOpen = true; } /** * 恢复原始视野 */ private onWheelRelease() { if(!this.zoomValid){ this.isScopeOpen = false; return } this.scheduleOnce(()=>{ //检查是否在秒内完成放大 已经在update里开镜完成时调用 Game.I.player.shoot(); this.isScopeOpen = false; },0.2) } /** * 开始开镜 */ private openScope() { //从枪械数据获取参数 const gData = Game.I.player.pData; if(!gData)return; const isRifle: boolean = this.isRifleGun(); //是步枪直接连续开火 if(isRifle){ Game.I.player.shoot(); } this.isZoomingIn = true; //将 zoomingSpeed 转换为持续时间(450对应1.2秒) this.zoomDuration = 72 / gData.zoomingSpeed; //使用枪械类型决定视口倍数 步枪使用 rifleZoom 狙击枪使用 scopeZoom const zoomMultiplier = isRifle ? 1 / gData.rifleZoom : 1 / gData.scopeZoom; this.targetFov = this.originalFov * zoomMultiplier; //保持原有速度计算逻辑 const fovDifference = this.originalFov - this.targetFov; this.zoomSpeed = fovDifference / this.zoomDuration; this.zoomStartTime = sys.now(); } // 在 closeScope 方法中保持相同持续时间 private closeScope() { this.isZoomingIn = false; this.isZoomingOut = true; const fovDifference = this.originalFov - this.targetFov; //使用相同 zoomDuration 保证缩镜速度一致 this.zoomSpeed = fovDifference / this.zoomDuration; } /** * 是否是步枪 */ public isRifleGun(){ const gData:any = Game.I.player.pData; if(!gData)return false; return gData.id == '100014' || gData.id == '100015'; } /** * 计算从枪口出发,沿准心射线方向的点 * @param isBulletEndPos 是否获得子弹的终点 */ public getCrossHairPos(){ //获取屏幕准心位置(世界坐标) const worldPos = this.crossHair.worldPosition.clone(); const camera2D: Camera = Game.I.canvas.getChildByName('Camera2D').getComponent(Camera); let screenPos = camera2D.worldToScreen(worldPos); //校准补偿准心的偏移量 screenPos.x -= 0; screenPos.y += this._isScopeOpen ? 80 : 20; //从摄像机发射通过准心的射线 const ray = new geometry.Ray(); Game.I.camera.screenPointToRay(screenPos.x, screenPos.y, ray); //获取枪口位置 const muzzlePos = Game.I.player.gun.muzzleNode.worldPosition; return new Vec3( muzzlePos.x + ray.d.x * this.bulletDistance, muzzlePos.y + ray.d.y * this.bulletDistance, muzzlePos.z + ray.d.z * this.bulletDistance); } /** * 玩家血量低于30%一直闪烁、打一枪的时候闪一下 * @param progress 血量进度 * @returns */ public playerHurtTwinkle(progress: number){ this.hpProgressBar.progress = progress; let isRepeat: boolean = progress <= 0.3; if(this.isHurtRun)return; this.isHurtRun = true; this.injury_blood.active = true; //audioMgr.play(Constants.audios.pop_up_sound); Tween.stopAllByTarget(this.injury_blood); let op: UIOpacity = this.injury_blood.getComponent(UIOpacity); op.opacity = 255; tween(op) .to(0.2,{opacity: 0}) .call(() => { this.isHurtRun = false; this.injury_blood.active = false; }) .repeat(isRepeat ? 100000 : 1) .start(); } /** * 点击事件 * @param event 事件 * @param customEventData 数据 */ public onBtnClicked(event: EventTouch, customEventData: any) { super.onBtnClicked(event,customEventData); let btnName = event.target.name; if (btnName === 'pause_btn') { // 暂停页面 uiMgr.show(Constants.popUIs.pauseUI); }else if (btnName === 'shot_btn') {//射击 if(this.isRifleGun){ //模拟按下射击 this._isScopeOpen = true; Game.I.player.shoot(); this.scheduleOnce(()=>{ this._isScopeOpen = false; },1.5) }else{ Game.I.player.shoot(); } }else if (btnName === 'cut_gun_btn') {//切枪 Game.I.player.randomCutGun() }else if(btnName === 'clean_btn'){//重新加载数据 userIns.removeData(); Game.I.player.pData = userIns.getCurUseGun(); MsgHints.show('重新加载数据完成'); }else if(btnName === 'adds_btn'){//解锁全部枪数据 userIns.unlockAllGuns(); MsgHints.show('已经解锁全部枪数据'); } } /** * 每帧更新,处理视野渐变和检查 2 秒时间限制 * @param deltaTime - 上一帧到当前帧的时间间隔 */ update(deltaTime: number) { if (this.isZoomingIn) { const currentFov = Game.I.camera.fov; if (currentFov > this.targetFov) { const newFov = currentFov - this.zoomSpeed * deltaTime; Game.I.camera.fov = Math.max(newFov, this.targetFov); } else { //检查是否在2秒内完成放大 const elapsedTime = (sys.now() - this.zoomStartTime) / 1000; if (elapsedTime <= 1.5) { this.zoomValid = true; } this.isZoomingIn = false; } } else if (this.isZoomingOut) { const currentFov = Game.I.camera.fov; if (currentFov < this.originalFov) { const newFov = currentFov + this.zoomSpeed * deltaTime; Game.I.camera.fov = Math.min(newFov, this.originalFov); } else { this.isZoomingOut = false; this.zoomValid = false; //非步枪开镜结束开火 if(!this.isRifleGun()){ Game.I.player.shoot(); } } } } }