import { _decorator, Node, Tween, Vec3, ProgressBar, UIOpacity, tween, Label, SkeletalAnimation, TiledUserNodeData, SphereCollider, CylinderCollider, MeshCollider} from 'cc'; import { Game } from './Game'; import { Player } from './Player'; import { Utils } from '../utils/Utils'; import { ResUtil } from '../utils/ResUtil'; import MsgHints from '../utils/MsgHints'; import { userIns } from '../data/UserData'; import { autoBind } from '../extend/AutoBind'; import { BaseExp } from '../core/base/BaseExp'; import { GunBase } from '../items/base/GunBase'; import { PoolManager } from '../core/manager/PoolManager'; import { audioMgr } from '../core/manager/AudioManager'; import { Constants } from '../data/Constants'; const { ccclass, property } = _decorator; //部位类型 export enum EPartType { head = "headCollider", //头部的碰撞体 body = "bodyCollider", //身体的碰撞体 tank = "tankCollider", //坦克的碰撞体 shield= "shieldBearer", //盾的碰撞体 } //敌人动作类型 export enum EAnimType { idle = "idle", //idle 待机状态 die = "die", //die 死亡状态 shoot = "shoot", //shoot 射击状态 walk = "walk", //shoot 行走状态 } //敌人类型枚举 export enum EnemyType { SoldierPistol = 10001, // 大兵 - 普通士兵,使用手枪 SniperSoldier = 10002, // 狙击兵 - 远程攻击单位,使用狙击枪 ShieldSoldier = 10003, // 盾牌兵 - 高生命值防御单位,使用手枪和盾牌 SnipeCaptain = 20001, // 狙击队长 - BOSS单位,使用强力狙击枪 ScatterCaptain = 20002, // 机枪队长 - BOSS单位,使用高射速机关枪 Tank = 20003, // 坦克 - BOSS单位,高生命值重型单位 GeneralPistol = 20004 // 将军 - BOSS单位,综合型强力敌人 } //武器类型枚举 export enum WeaponType { Pistol = 1001, // 大兵手枪 将军手枪 Sniper = 1002, // 狙击兵狙击枪 狙击队长狙击枪 Shield = 1003, // 盾牌兵盾牌 Scatter = 1005, // 机枪队长机关枪 Tank_Pao = 1006, // 坦克 } @ccclass('Enemy') export class Enemy extends BaseExp { @autoBind({type: ProgressBar,tooltip: "敌人血量"}) public hpBar: ProgressBar = null!; @autoBind({type: Node,tooltip: "敌人受伤节点"}) public hurt_num: Node = null!; @autoBind({type: SphereCollider,tooltip: "敌人头部"}) public headCollider: SphereCollider = null!; @autoBind({type: CylinderCollider,tooltip: "敌人身体"}) public bodyCollider: CylinderCollider = null!; @autoBind({type: MeshCollider,tooltip: "坦克身体"}) public tankCollider: MeshCollider = null!; //人物动画节点 private skeletalAnim: SkeletalAnimation = null!; //根据敌人类型动态获取 public enemyNode: Node = null!; public oTween: Tween;//敌人透明变化 public defaultSpeed: number = 0; public speed: number = 0; public isDead: boolean = false; //拥有的枪 public gun: GunBase = null; //盾的节点 public shieldNode: Node = null!; //拥有的盾可以使用的血量 private _shieldHp: number = 0; public set shieldHp(value: number) { this._shieldHp = value; if(this._shieldHp <= 0){ this.removeShield(); } } public get shieldHp(): number { return this._shieldHp; } //当前金币的值 public goldCount : number = 0; //玩家 public player: Player = null!; //运动方向 public moveDir: Vec3 = new Vec3(); //设置的血量 public totalHP: number = 0; //是否是没打死逃逸 public escape: boolean = false; //敌人数据 public data: any = null; //是否能锁定攻击对象了 public isCanLock: boolean = false; //是否是在开火 private isFire: boolean = false; //敌人行走的路径 public pathList: Array = []; //当前行走的位置 public curMoveIndex: number = 0; //枪旋转角度 public angle: number = 0; //新增警戒状态属性 public isAlert: boolean = false; start() { this.hurt_num.active = false; } /** * 初始化数据 */ public async init(data: any){ this.data = data; const eNameTypes:string[] = Object.keys(EnemyType) .filter(key => isNaN(Number(key))) .map(key => key.charAt(0).toLowerCase() + key.slice(1)); let eNodes:Node[] = this.node.children .filter(e => eNameTypes.includes(e.name)); const ids: number[] = Object.values(EnemyType).filter(v => typeof v === 'number') as number[]; ids.forEach((e_id:number,i:number) => { let active : boolean = e_id == data.id; let eTypeNode:Node = eNodes[i]; eTypeNode.active = active; if(active){ this.enemyNode = eTypeNode; this.skeletalAnim = eTypeNode.getComponent(SkeletalAnimation); } }); if(!this.enemyNode){ MsgHints.show(`不存在:${JSON.stringify(data)}`) return; } if(this.isTank()){ this.skeletalAnim?.play(EAnimType.walk); this.headCollider.enabled = false; this.bodyCollider.enabled = false; this.tankCollider.enabled = true; }else{ this.skeletalAnim?.play(EAnimType.idle); this.headCollider.enabled = true; this.bodyCollider.enabled = true; this.tankCollider.enabled = false; } this.player = Game.I.player.getComponent(Player); this.isAlert = false; this.data = this.enemyData(data); this.isDead = false; this.escape = false; //设置血量 this.totalHP = data.hp; //敌人速度 const s: number = data.speed * 3; this.speed = s; this.defaultSpeed = s; //设置枪的数据 await this.createGun(); //恢复初始雪条 this.hpBar.progress = 1; this.hpBar.node.active = false; this.endFire(); } /** * 把武器的参数带到敌人身上 */ public enemyData(data:any){ if(!data)return; //主武器id const mainWeaponID:number = data.weapon_id_1; let hp: number = Utils.clone(data).hp; let gData:any = userIns.enemyWeaponTable.find(e=>e.gun_id == mainWeaponID); //副武器 盾上的血量 const secondWeaponID:number = data.weapon_id_2; if(secondWeaponID != 0){ let sData:any = userIns.enemyWeaponTable.find(e=>e.gun_id == secondWeaponID); if(sData && sData.hp > 0){ data.hp += hp + sData.shield_hp; } } return Object.assign(data,gData); } /** * 创建英雄所拥有的枪 */ public async createGun(){ this.removeGun(); //敌人主武器 let gunPos:Node = this.enemyNode.getChildByName("gun_pos"); const mainWeaponID:number = this.data.weapon_id_1; let mData:any = userIns.enemyWeaponTable.find(e=>e.gun_id == mainWeaponID); let gunNode:Node = await ResUtil.loadGunRes(`enemy/${mData.gun_prb_name}`,this.enemyNode) as Node; this.gun = gunNode.getComponent(GunBase); gunNode.active = true; gunNode.parent = gunPos.parent; gunNode.worldPosition = gunPos.worldPosition.clone(); gunNode.eulerAngles = gunPos.eulerAngles.clone(); this.gun.init(this.data,this); //敌人附武器 const secondWeaponID:number = this.data.weapon_id_2; if(secondWeaponID != 0){ let sData:any = userIns.enemyWeaponTable.find(e=>e.gun_id == secondWeaponID); let shieldNode:Node = await ResUtil.loadGunRes(`enemy/${sData.gun_prb_name}`,this.enemyNode) as Node; let shieldPos:Node = this.enemyNode.getChildByName("shield_pos"); shieldNode.active = true; shieldNode.parent = shieldPos.parent; shieldNode.worldPosition = shieldPos.worldPosition.clone(); shieldNode.eulerAngles = shieldPos.eulerAngles.clone(); this.shieldNode = shieldNode; this.shieldHp = sData.shield_hp; } } /** * 移除枪回收武器的时候调用 */ public removeGun(){ if(this.gun){ this.gun.endFire(); PoolManager.putNode(this.gun.node); this.gun = null; } this.isFire = false; this.removeShield(); } /** * 移除盾 */ public removeShield(){ if(this.shieldNode){ PoolManager.putNode(this.shieldNode); this.shieldNode = null; } this._shieldHp = 0; } /** * 扣掉血 * @param hp 血 * @param pData 英雄数据 * @param isHeadShot 是否是爆头 */ public subHP(hp: number, pData:any,isHeadShot: boolean = false){ if(Game.I.isPause ||this.isDead ||hp == null ||this.totalHP <= 0){ return; } this.hpBar.node.active = true; this.scheduleOnce(() => { if(this.hpBar){this.hpBar.node.active = false;} }, 0.8); this.totalHP -= hp; //这种是伤害超级高直接死亡了 if(hp > this.totalHP){ this.escape = false; this.showHurt(Utils.numberToString(hp)); this.recycle() return; } //敌人死亡 if(this.totalHP <= 0 && !this.isDead){ //爆头击杀播放音效 if(isHeadShot){ audioMgr.playOneShot(Constants.audios.head_shot); } this.escape = false; this.recycle(); }else{//进度条和单独扣血 this.hpBar.progress = this.totalHP / this.data.hp; this.showHurt(Utils.numberToString(hp)); } } /** * 开始敌人移动,面向玩家行走 */ public walk(points:Vec3[]) { if(points.length <= 0)return; this.curMoveIndex = 0; this.pathList = points; this.skeletalAnim.play(EAnimType.walk); this.updateDir(points[0].clone()); } /** * 开始攻击 */ public beginFire(){ if(this.player && !this.player.isDead && this.gun){ this.isFire = true; this.gun.fire(); } } /** * 结束攻击 */ public endFire(){ this.gun.endFire(); this.isFire = false; } /** * 回收敌人节点 * @param f 是否是游戏进行中的正常回收 程序主动回收不参数个数统计和加分这些 * @returns */ public recycle(f: boolean = true){ if(!this.node ||this.isDead){ return; }; this.removeGun(); this.totalHP = 0; this.skeletalAnim.play(EAnimType.die); this.isDead = true; this.hpBar.node.active = false; let death: Function = function(inite: boolean){ this.node.getComponent(UIOpacity).opacity = 255; PoolManager.putNode(this.node); if(inite){ Game.I.buildEnemys.subtractEnemy(this); } }.bind(this,f); if(this.oTween){this.oTween?.stop()}; if(f){//修改透明度 let op: UIOpacity = this.node.getComponent(UIOpacity); this.oTween = tween(op) .delay(0.4) .to(0.15,{ opacity: 0}) .call(()=>{death();}) .start(); }else{ death(); } } /** * 展示敌人受到的伤害 * @param hpStr */ public showHurt(hpStr: string) { if(Game.I.isGameOver || this.isDead) return; //创建3D伤害数字 const n = PoolManager.getNode(this.hurt_num, this.hurt_num.parent); n.getComponent(Label).string = hpStr; const oPos: Vec3 = this.hpBar.node.position.clone(); n.position = oPos; const oScale: Vec3 = this.hurt_num.scale.clone(); n.scale = oScale; n.getComponent(UIOpacity).opacity = 255; let bezier: Vec3[] = []; let num: number = 100; let forward = Utils.getRandomFloat(0, 1) > 0.5 ; if (forward) { bezier = [new Vec3(-(10/num)+oPos.x, (30/num)+oPos.y), new Vec3((-40/num)+oPos.x, (40/num)+oPos.y), new Vec3((-60/num)+oPos.x, oPos.y)]; } else { bezier = [new Vec3((10/num)+oPos.x, (30/num)+oPos.y), new Vec3((40/num)+oPos.x, (40/num)+oPos.y), new Vec3((60/num)+oPos.x, oPos.y)]; } n.position = new Vec3(bezier[2].x,bezier[2].y - (80/num),oPos.z); //贝塞尔曲线坐标函数 let twoBezier = (t: number, p1: Vec3, cp: Vec3, p2: Vec3) => { let x = (1 - t) * (1 - t) * p1.x + 2 * t * (1 - t) * cp.x + t * t * p2.x; let y = (1 - t) * (1 - t) * p1.y + 2 * t * (1 - t) * cp.y + t * t * p2.y; return new Vec3(x, y, oPos.z); }; let time: number = 0.65; tween(n) .parallel( tween() .to(time,{ scale: new Vec3(oScale.x * 0.4,oScale.y * 0.4,oScale.z * 0.4)}), tween() .to(time, bezier[2], { onUpdate: (target: Vec3, ratio: number) => { if(n.parent){ const pos: Vec3 = twoBezier(ratio, bezier[0], bezier[1], bezier[2]); n.position = new Vec3(pos.x,pos.y - (80/num),oPos.z) } } }) ).call(function(){ tween(this.getComponent(UIOpacity)) .to(0.15,{opacity: 0}) .call(()=>{ PoolManager.putNode(this); }) .start(); }.bind(n)).start(); } /** * 计算是否命中玩家 * @returns 返回命中状态 */ public calculateIsHit():boolean{ let isGuaranteedHit: boolean = false; //获取射击的精准度 const data:any = this.getDifficultyData(); //生成0-1随机数用于命中判定 根据precision范围确定命中逻辑 const random = Math.random(); if(data.precision < 0){//负值100%不命中 isGuaranteedHit = false; }else if(data.precision > 1) {//超过1 100%命中 isGuaranteedHit = true; }else {//正常范围:随机判定 isGuaranteedHit = random <= data.precision; } return isGuaranteedHit; } /** * 轻松杀敌增加游戏难度 只有单其他附加因素 例如:连杀等叠加状态 给敌人增加血 */ public addDiffHP(){ if(!this.node ||this.isDead ||Game.I.isGameOver ||this.isDead){ return; }; const data:any = this.getDifficultyData(); if(data.hp_ratio > 0){ this.totalHP = this.data.hp * (1 + data.hp_ratio); } } /** * 敌人的射击的精准度 获得难度数据 */ public getDifficultyData(): any{ //获取基础精准度(玩家预设值) const precision: number = userIns.getCurLevelData().precision; let diff:any = { precision: precision,//增加或减少的精准度 0-100% hp_ratio: 0,//增加或减少的血量 0-100% reaction_time: 0,//增加或减少的反应时间 0-100% } const each: number = Math.floor(Game.I.buildEnemys.getCurKillNum() / 3); //连杀3个敌人的次数 AI精准度增加1% diff.precision += each * 0.01; if(userIns.isContinuePass){//1、连续通关 AI生命值增加15% 精准度增加1% diff.hp_ratio += 0.15; diff.precision += 0.01; }else{//2、通关失败 下一局 AI精准度下降5% AI反应时间增加0.5s diff.precision -= 0.05; diff.reaction_time += 0.5; } return diff; } /** * 更新敌人行走和变化方向 */ protected update(dt: number): void { if (Game.I.isGameOver || Game.I.isPause || !this.data || this.isDead) return; if (this.curMoveIndex >= this.pathList.length) { return; } dt = dt / Game.I.map.multiplySpeed(); const targetPos = this.pathList[this.curMoveIndex]; const currentPos = this.node.worldPosition.clone(); //计算移动方向和剩余距离 const moveDirection = new Vec3(); Vec3.subtract(moveDirection, targetPos, currentPos); const distanceToTarget = moveDirection.length(); //计算本帧移动量 const moveDistance = this.speed * dt; const normalizedDir = moveDirection.normalize(); if (distanceToTarget > 0.1) {//添加小阈值防止抖动 线性插值平滑移动 const newPos = currentPos.add(normalizedDir.multiplyScalar(moveDistance)); this.node.worldPosition = newPos; //更新面向方向(仅在需要时更新) if(moveDirection.lengthSqr() > 0) { this.node.forward = normalizedDir.negative(); } } else { //到达当前路径点后指向下一个点 this.curMoveIndex++; if (this.curMoveIndex < this.pathList.length) { const nextTarget = this.pathList[this.curMoveIndex]; Vec3.subtract(moveDirection, nextTarget, currentPos); this.node.forward = moveDirection.normalize().negative(); } } //到达路径终点处理 if (this.curMoveIndex >= this.pathList.length) { this.beginFire(); this.skeletalAnim.play(EAnimType.shoot); this.updateDir(this.node.worldPosition.clone()); } } /** * 更新敌人方向 * @param curPos 位置 */ public updateDir(curPos: Vec3){ const direction = Vec3.subtract(new Vec3(), curPos, Game.I.player.node.worldPosition); direction.normalize(); this.node.forward = direction; } /** * 是否是坦克 */ public isTank():boolean{ return this.data.id == EnemyType.Tank; } } /** * if (this.isDead || !this.player) return; if (!this.isAlert) { this.skeletalAnim.play(EAnimType.walk); const playerPos = this.player.node.getWorldPosition(); const enemyPos = this.node.getWorldPosition(); //创建射线对象修复类型错误 const ray = new geometry.Ray(); ray.o = enemyPos; ray.d = playerPos.subtract(enemyPos).normalize(); //修正物理检测参数 if (PhysicsSystem.instance.raycast(ray, 1 << 0, 2)) { const randomDir = Math.random() > 0.5 ? 1 : -1; //修改为正确的参数格式 const quat = new Quat(); Quat.fromAxisAngle(quat, Vec3.UP, 45 * randomDir * Math.PI / 180); this.node.rotate(quat, NodeSpace.WORLD); } //this.skeletalAnim.play("walk"); //使用定时器持续移动 this.schedule(() => { const distance: number = Vec3.distance(this.node.worldPosition,Game.I.buildEnemys.ambush.worldPosition); if(distance < 10) { this.isAlert = true; this.walk(); return; } const moveVec = this.node.forward.negative().multiplyScalar(this.speed * 0.016); this.node.position = this.node.position.add(moveVec); }, 0.1); } else if (!this.currentCover) { const nearestCover = this.findNearestCover(); if (nearestCover) { this.currentCover = nearestCover; //生成Z轴随机偏移(-18到+18) const targetPos = nearestCover.worldPosition.clone(); targetPos.z += Math.random() * 36 - 18; //移动到掩体位置 直接使用缓动动画移动到掩体 tween(this.node) .to(2, { worldPosition: targetPos }, { easing: easing.quadOut, onStart: () => { this.speed = this.defaultSpeed * 1.5; //加速移动 }, onComplete: () => { this.skeletalAnim.play(EAnimType.shoot); this.beginFire(); //到达后开始攻击 this.currentCover = nearestCover; } }) .start(); } } */