import { _decorator, Node, Tween, Vec3, ProgressBar, UIOpacity, tween, Label, SkeletalAnimation, Animation, SphereCollider, CylinderCollider, MeshCollider, Collider} 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 = "shieldCollider", //盾的碰撞体 sundries = "sundriesCollider", //杂物的碰撞体 } //敌人动作类型 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: Collider,tooltip: "敌人头部"}) public headCollider: Collider = null!; @autoBind({type: Collider,tooltip: "敌人身体"}) public bodyCollider: Collider = null!; @autoBind({type: Collider,tooltip: "坦克身体"}) public tankCollider: Collider = null!; @autoBind({type: Node,tooltip: "坦克行走"}) public tank_walk: Node = null!; @autoBind({type: Node,tooltip: "坦克射击"}) public tank_shoot: Node = null!; //浓烟节点 只有军车的时候血量低于30%才会有 public heavySmoke: Node = null; //敌人动画节点 private skeletalAnim: SkeletalAnimation = null!; //根据敌人类型动态获取 public enemyNode: Node = null!; 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 curHP: number = 0; //敌人的原始血量 public originHP: number = 0; //是否是没打死逃逸 public escape: boolean = false; //敌人数据 public data: any = null; //是否能锁定攻击对象了 public isCanLock: boolean = false; //是否是在开火 private isFire: boolean = false; //减血的时候是否是打到头 public isShotHead: 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; this.player = Game.I.player.getComponent(Player); const isTank:boolean = this.isTank(); this.enemyNode = this.node.getChildByName(data.prb_name); if(isTank){ this.tankCollider["args"] = [EPartType.tank,this]; }else{ this.headCollider["args"] = [EPartType.head,this]; this.bodyCollider["args"] = [EPartType.body,this]; this.skeletalAnim = this.enemyNode.getComponent(SkeletalAnimation); if(this.skeletalAnim){ this.skeletalAnim?.play(EAnimType.idle); } } if(!this.enemyNode){ MsgHints.show(`不存在:${JSON.stringify(data)}`) return; } this.isShotHead = false; this.isAlert = false; this.data = this.enemyData(data); this.isDead = false; this.escape = false; //设置血量 this.curHP = this.originHP = 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.loadRes(`enemy/gun/${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.loadRes(`enemy/gun/${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(); shieldNode.children[0].getComponent(Collider)["args"] = [EPartType.shield,this]; 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; } /** * 坦克冒浓烟 */ public tankHeavySmoke(){ if(!this.isTank())return; if(this.heavySmoke)return; //血量过低30%冒浓烟 if(this.curHP < this.data.hp * 0.3){ ResUtil.playParticle( `effects/Prefabs/HeavySmoke`, 0, new Vec3(0.3,0.3,0.3), (heavySmoke) => { heavySmoke.parent = this.node.parent; heavySmoke.worldPosition = this.node.worldPosition.clone(); heavySmoke.active = true; this.heavySmoke = heavySmoke; } ); } } /** * 扣掉血 * @param hp 血 * @param pData 英雄数据 * @param isHeadShot 是否是爆头 */ public subHP(hp: number, pData:any){ if(Game.I.isPause ||this.isDead ||hp == null ||this.curHP <= 0){ return; } this.hpBar.node.active = true; this.scheduleOnce(() => { if(this.hpBar){this.hpBar.node.active = false;} }, 0.8); this.curHP -= hp; this.tankHeavySmoke(); //这种是伤害超级高直接死亡了 if(hp > this.originHP){ this.escape = false; this.showHurt(Utils.numberToString(hp)); this.recycle() return; } //敌人死亡 if(this.curHP <= 0 && !this.isDead){ this.escape = false; this.recycle(); }else{//进度条和单独扣血 this.hpBar.progress = this.curHP / this.originHP; this.showHurt(Utils.numberToString(hp)); } } /** * 开始敌人移动,面向玩家行走 */ public walk(points:Vec3[]) { if(points.length <= 0)return; this.curMoveIndex = 0; this.pathList = points; const time: number = 1 / this.data.speed; if(this.isTank()){ this.tank_walk.active = true; this.tank_shoot.active = false; }else{ ResUtil.playSkeletalAnim(this.skeletalAnim,EAnimType.walk,time); } 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; }; if(f){//爆头击杀播放音效 audioMgr.playOneShot(this.isShotHead ? Constants.audios.head_shot : Constants.audios.enemy_die); } this.removeGun(); this.curHP = 0; //动画和回收时间 let recycleTime: number = 2; if(!this.isTank()){ ResUtil.playSkeletalAnim(this.skeletalAnim,EAnimType.die); }else{ //坦克爆炸后生成爆炸特效 ResUtil.playParticle( `effects/Prefabs/TankBoom`, 3, new Vec3(0.1,0.1,0.1), (particle) => { particle.parent = this.node.parent; particle.worldPosition = this.node.worldPosition.clone(); particle.active = true; }, () => {//爆炸完成后 回收浓烟 if(this.heavySmoke && this.heavySmoke.parent){ PoolManager.putNode(this.heavySmoke); this.heavySmoke = null; } } ); recycleTime = 3; } 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); this.unschedule(death) this.scheduleOnce(death,recycleTime); } /** * 展示敌人受到的伤害 * @param hpStr */ public showHurt(hpStr: string) { if(Game.I.isGameOver || this.isDead) return; //敌人流血特效 ResUtil.playParticle( `effects/Prefabs/blood`, 1, new Vec3(0.2,0.2,0.2), (blood) => { blood.active = true; blood.parent = this.enemyNode.parent; const targetPos: Vec3 = this.enemyNode.worldPosition.clone(); blood.worldPosition = new Vec3(targetPos.x,targetPos.y - 0.2,targetPos.z); } ); //创建3D伤害数字 const n = PoolManager.getNode(this.hurt_num, this.hurt_num.parent); let label:Label = n.getComponent(Label); label.color = Utils.hexColor(this.isShotHead ? "#F51414" : "#FFFFFF"); 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.curHP += this.originHP * data.hp_ratio; } } /** * 敌人的射击的精准度 获得难度数据 */ public getDifficultyData(): any{ //获取基础精准度(玩家预设值) const precision: number = userIns.getCurLevelData().precision; let basePrecision: number = 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; //连续通关3次以上才有的难度 const lx_num: number = 3; if(userIns.passNum > lx_num){//1、连续通关 AI生命值增加15% 精准度增加5% diff.precision += userIns.passNum * 0.05; diff.precision = Math.min(basePrecision, 0.9); diff.hp_ratio += userIns.passNum * 0.15; diff.hp_ratio = Math.min(diff.hp_ratio, 1); }else{//2、通关失败 下一局 AI精准度下降5% AI反应时间增加0.5s diff.precision -= userIns.passNum * 0.05; diff.precision = Math.max(diff.precision, 0.1); diff.reaction_time += 0.5; } return diff; } /** * 更新敌人行走和变化方向 */ protected update(dt: number): void { if(Game.I.isGameOver || Game.I.isPause || !this.data || this.isDead) return; const targetPos = this.pathList[this.curMoveIndex]; if(!targetPos)return; //保持速度调节逻辑 dt = dt / Game.I.map.multiplySpeed(); const currentPos = this.node.worldPosition.clone(); const toTarget = targetPos.clone().subtract(currentPos); //敌人移动 const moveDir = new Vec3(); Vec3.subtract(moveDir, targetPos, currentPos); let distance:number = moveDir.length(); //计算实际应移动距离 const moveDistance = this.speed * dt; if(moveDistance > 0) { //使用标准化方向向量 + 实际移动距离 const newPos = currentPos.add(toTarget.normalize().multiplyScalar(moveDistance)); //添加移动平滑过渡 平滑系数 10-15比较好 值越大越平滑 const smoothFactor = 20; this.node.worldPosition = Vec3.lerp( new Vec3(), currentPos, newPos, Math.min(1, dt * smoothFactor) ); //更新方向 const isLastPathPoint = this.curMoveIndex == this.pathList.length - 1; const dirTarget = isLastPathPoint ? Game.I.player.node.worldPosition : this.pathList[this.curMoveIndex + 1]; this.updateDir(dirTarget); } //到达判断增加缓冲范围 if(moveDistance > distance) { //从出生点走到第二个点就是外门第一个点就开门 并且门没有被打开 if(this.curMoveIndex == 1 && !Game.I.buildEnemys.isOpenDoor){ Game.I.map.openDoor(); Game.I.buildEnemys.isOpenDoor = true; } this.curMoveIndex++; if (this.curMoveIndex >= this.pathList.length) { this.beginFire(); if(this.isTank()){ this.tank_walk.active = false; this.tank_shoot.active = true; } else { ResUtil.playSkeletalAnim(this.skeletalAnim, EAnimType.shoot, this.data.atk_speed); } this.updateDir(Game.I.player.node.worldPosition); } } } /** * 更新敌人方向(根据目标坐标转向) * @param targetPos 目标坐标(路径点或玩家坐标) */ public updateDir(targetPos: Vec3){ const curPos = this.node.worldPosition.clone(); //从目标位置指向当前位置 const targetDir = Vec3.subtract(new Vec3(), curPos, targetPos); targetDir.normalize(); //平滑插值转向 避免瞬间转向 const currentDir = this.node.forward.clone(); Vec3.slerp(currentDir, currentDir, targetDir, 0.15); this.node.forward = currentDir; } /** * 是否是坦克 */ public isTank():boolean{ return this.data.id == EnemyType.Tank; } }