import { _decorator, Collider, Collider2D, Component, Contact2DType, ERigidBody2DType, find, instantiate, IPhysics2DContact, isValid, Node, ParticleSystem2D, Prefab, RigidBody2D, sp, Tween, tween, TweenSystem, UITransform, v2, v3, Vec2, Vec3 } from 'cc'; import { SuperTrail } from 'db://assets/mx/components/SuperTrail'; import { ColaMucusAreaSchedule } from './ColaMucusAreaSchedule'; import { ColaVortexAreaSchedule } from './ColaVortexAreaSchedule'; import { GBundle } from '../../../game/ConfigRes'; import { WeaponType } from '../../../manager/WeaponDataManager'; import { IWeaponProperty, BattleDataManager, SpecialSkillType, SpecialSkillAddBuffType, BezierFallEffectType } from '../BattleDataManager'; import BombSpine from './BombSpine'; import { ConstantSpeedParabola } from './ConstantSpeedParabola'; import { MagmaBurnSchedule } from './MagmaBurnSchedule'; import { SustainSchedule } from './SustainSchedule'; import { SkillType } from './SkillTypes'; import { Monster, MonsterState } from '../Monster'; import MTools from 'db://assets/mx/tools/MTools'; import { MapAgent } from '../MapAgent'; import { BattlePerformance } from '../BattlePerformance'; const { ccclass, property } = _decorator; @ccclass export default class SkillBase extends Component { /**武器的唯一实例ID */ instanceId: number; /**武器的基础配置数据 */ weaponData: ITableWeapon2; /**发射该子弹的方块等级(用于伤害倍率等) */ public tetriLevel: number = 1; /**武器的伤害属性 */ weaponPropertyData: IWeaponProperty; Sk: sp.Skeleton = null; isTweenPaused: boolean = false private _lastCanUpdateFrame: boolean = true; targetPos: Vec3 = null //targetPosArray posBezierEndTime: number = 0 //弹幕武器穿透数量 piercingCount: number = 0 //是否碰撞过怪物 isColliderMonster: boolean = false /**是否是分裂的母体*/ isSplitParent: boolean = false /**是否是分裂的子弹*/ isSplitBullet: boolean = false /**是否是无人机载体 */ isDroneCarrier: boolean = false /**是否是陀螺子弹 */ isGyroBullet: boolean = false //分裂子弹的索引 splitIndex: number = 0 //是否是燃油弹 isFuel: boolean = false //存活时间 aliveTime: number = 0 //是否死亡 isDead: boolean = false /** 本次发射时起点(父节点本地坐标),用于异常飞出兜底回收 */ private _bulletSpawnLocal: Vec3 | null = null /**上一次锁定的目标怪物 uuid(用于弹射/连续寻敌避免一直打同一只) */ private _lastTargetMonsterUuid: string = ''; /** * 弹射刚离开的那只怪:选下一目标时从候选里排除,且在仍与其碰撞体重叠时忽略碰撞, * 否则 colliderMonster 一清就会立刻再次 onBeginContact 同一只怪(血厚时必现)。 */ private _catapultLastHitMonsterUuid: string = ''; /** 武器20 弧刃脉冲炮:本颗子弹已结算过伤害的怪物 uuid(同怪只打一次) */ static readonly WEAPON20_PIERCE_ID = 20; private _pierceHitMonsterUuids = new Set(); /**每个技能实例单独随机到的殊技能buff */ specialSkillAddBuffInfoForEntity: string[] = []; /**当前切牌的类型索引 */ currentSplitTypeIndex: number = 0; splitAngleRandom: number = 0 private _currentPath: Vec2[] = []; // 榴莲武器当前路径点数组 private _targetIndex: number = 0; // 榴莲武器当前目标路径点索引 _isSplitBulletInvincible: boolean = false //再次分裂次数 _splitContinueNumbers: number = 0 //弹射次数 catapultCount: number = 0 bulletLevel: number = 1; //分裂 protected onLoad(): void { this.Sk = this.node.getComponentInChildren(sp.Skeleton); } /**美术前向轴偏移(角度制)。运行时可通过 globalThis.__skillFacingOffsetDeg 调整 */ private getFacingOffsetDeg() { const v = (globalThis as any).__skillFacingOffsetDeg; return Number.isFinite(v) ? Number(v) : -90; } /**角度方向符号:atan2 为逆时针正;若项目角度为顺时针正,则设为 -1。运行时用 globalThis.__skillFacingSign 设置 */ private getFacingSign() { const v = (globalThis as any).__skillFacingSign; return v === -1 ? -1 : 1; } /**同步设置 2D 角度(避免 angle/rotation 不一致导致不转向) */ private setFacingAngleDeg(deg: number) { this.node.angle = deg; // 强制同步 rotation(部分节点在 tween/暂停恢复后只写 angle 可能不刷新渲染) (this.node as any).setRotationFromEuler?.(0, 0, deg); } protected onDestroy(): void { this.unscheduleAllCallbacks(); // 使用组件自己的方法取消定时器 } protected update(dt: number): void { if (!gg.game.isBattleContextActive()) return; if(gg.game.IsPlayingGuideStory){ return } const canUpdateFrame = gg.game.CurentBattle.canUpdateFrame(); //IsWarnTimeActive预计倒计时也不要pauseTween if (canUpdateFrame !== this._lastCanUpdateFrame) { if (!canUpdateFrame){ if(gg.game.CurentBattle.IsWarnTimeActive){ return } this.pauseTween() }else{ this.resumeTween() } this._lastCanUpdateFrame = canUpdateFrame; } this._checkBulletOutOfRange(dt); // if (!this.isTweenPaused) { // const scaledDt = dt * gg.game.CurentBattle.TimeScale; // this.aliveTime += scaledDt; // this.followPath(scaledDt); // } } /** 子弹异常飞出或战斗已结束时的兜底回收(不依赖 tween 回调 / 碰撞里 isValid) */ private _checkBulletOutOfRange(_dt: number): void { if (this.isDead || !this.node?.isValid || !this.weaponData) return; if (gg.game?.CurentBattle?.IsGameOver) { this.removeBullet(); return; } const pos = this.node.position; const wp = this.node.worldPosition; if ( Math.abs(pos.x) > 8000 || Math.abs(pos.y) > 8000 || Math.abs(wp.x) > 20000 || Math.abs(wp.y) > 20000 ) { this.removeBullet(); return; } if (this._bulletSpawnLocal) { const d = Vec3.distance(pos, this._bulletSpawnLocal); if (d > 10000) { this.removeBullet(); } } } /** 带 RigidBody2D 的子弹改为 Kinematic,避免 tween 改坐标与 Dynamic 刚体冲突导致坐标飞出 */ private _prepareBulletRigidBody(): void { const rb2d = this.node.getComponent(RigidBody2D); if (!rb2d) return; rb2d.fixedRotation = true; rb2d.linearVelocity = v2(0, 0); rb2d.angularVelocity = 0; rb2d.type = ERigidBody2DType.Kinematic; } /**武器存活时间 */ getAliveTime() { if (this.aliveTime > 10) { this.aliveTime = 10 } return this.aliveTime } setData( weaponData: ITableWeapon2, targetPos: Vec3, isSplitBullet: boolean = false, isFuel: boolean = false, splitIndex: number = 0, beziertime: number = 0, splitAngleRandom: number = 0, splitContinueNumbers = 0, tetriLevel: number = 1) { this._prepareBulletRigidBody(); // console.log('setData ++++ SkillBase++++++++++===', splitIndex) this.isDead = false this.aliveTime = 0 this._bulletSpawnLocal = this.node.position.clone() this.isSplitBullet = isSplitBullet this.splitIndex = splitIndex this.isTweenPaused = false // 池化复用:上次回收前若 pauseTween 过,ActionManager 上该节点仍为暂停态; // 仅清 isTweenPaused 不会恢复 tween,新 straightAtkTo 的 tween 永远不推进 → 无到达回调 TweenSystem.instance.ActionManager.resumeTarget(this.node) this.node.angle = 0 this.weaponData = weaponData; this.tetriLevel = Number.isFinite(tetriLevel as any) && (tetriLevel as any) > 0 ? (tetriLevel as any) : 1; this.weaponPropertyData = BattleDataManager.getWeaponAddProperty(weaponData.weaponid) // 生成唯一实例ID this.catapultCount = this.weaponPropertyData.catapultCount //this.instanceId = Date.now() + Math.floor(Math.random() * 1000); this.piercingCount = this.weaponPropertyData.piercingCount //由于分裂的子弹会立即打到怪物身上,所以需要增加一次穿透 this._isSplitBulletInvincible = true; this.splitContinueNumbers = 0 if (isSplitBullet) { this.piercingCount++ if (this.weaponData.flight == SkillType.直线分裂) { this.piercingCount = 100 } } if(this.weaponData.flight == SkillType.直线穿透){ this.piercingCount = 100 } this.splitAngleRandom = Math.floor(splitAngleRandom) this.isFuel = isFuel this.isColliderMonster = false this.isSplitParent = false this._catapultLastHitMonsterUuid = '' this._pierceHitMonsterUuids.clear() this._lastTargetMonsterUuid = '' this.targetPos = targetPos.clone() this.posBezierEndTime = beziertime this.specialSkillAddBuffInfoForEntity = []; this.setSpecialSkillEffect() this.startAtk() } /**是否是分裂的子弹 */ getIsSplitBullet() { return this.isSplitBullet } setInstanceId(instanceId) { this.instanceId = instanceId } /**获取武器实例的唯一ID */ getInstanceId() { return this.instanceId } /** 直线弹目标点:Boss 用碰撞盒中心,避免缩小包围盒后仍飞向节点原点导致打不中 */ protected getMonsterAimWorldPos(monsterNode: Node): Vec3 { if (!monsterNode?.isValid) { return v3(0, 0, 0); } const m = monsterNode.getComponent(Monster); return m ? m.getColliderAimWorldPos() : monsterNode.worldPosition.clone(); } /** 将世界坐标转换到子弹父节点(ui_skilllayer)本地坐标,供直线 tween 使用 */ protected worldToSkillParentLocal(worldPos: Vec3): Vec3 { const parentUi = this.node.parent?.getComponent(UITransform); if (parentUi) { return parentUi.convertToNodeSpaceAR(worldPos); } const skillRoot = gg.game?.CurentBattle?.SkillController?.node?.getComponent(UITransform); if (skillRoot) { return skillRoot.convertToNodeSpaceAR(worldPos); } return worldPos.clone(); } /**开始扔武器 */ startAtk() { let originPos = this.node.position.clone() //获取离当前武器最近的怪物 if (this.weaponData.flight == SkillType.直线弹幕 || this.weaponData.flight == SkillType.直线弹射 || this.weaponData.flight == SkillType.直线数量 || this.weaponData.flight == SkillType.直线分裂 || this.weaponData.flight == SkillType.直线穿透 ) { let nearestMonster = null; //终点的怪物 if(this.weaponData.flight == SkillType.直线数量 || this.weaponData.flight == SkillType.直线分裂 || this.weaponData.flight == SkillType.直线穿透){ nearestMonster = this.pickNearestMonsterByIndex(this.splitIndex); }else{ nearestMonster = this.pickNearestMonsterAvoidLast(); } if(!nearestMonster){ console.log('没有怪物不在发射武器 skillBase') this.removeBullet() return } //朝着怪物方向旋转角度,武器的初始角度y轴正方向朝向 let targetPos = v3(0, 0, 0) let targetScale = v3(1, 1, 1) //分裂的子弹 if (this.isSplitBullet && this.weaponData.flight != SkillType.直线分裂) { // 分裂子弹统一改为“向下半区”发散(与飞剑分裂方向一致) // 以 180°(向下) 为中心,半角 67.5°,范围 [112.5°, 247.5°] const halfDeg = 67.5; const angle = 180 + (Math.random() * 2 - 1) * halfDeg; const distance = 1000; // 足够飞出屏幕 const radian = angle * Math.PI / 180; const dx = distance * Math.sin(radian); const dy = distance * Math.cos(radian); targetPos = v3(originPos.x + dx, originPos.y + dy, 0); // 与抛物线分裂保持同一套角度约定(顺时针角度取负给 Cocos) this.node.angle = -angle; } else if (this.isSplitBullet && this.weaponData.flight == SkillType.直线分裂) { //分裂子弹 左右分裂 if (this.splitIndex % 2 == 0) { targetPos = v3(600, originPos.y, 0) this.node.angle = -90 } else if (this.splitIndex % 2 == 1) { targetPos = v3(-600, originPos.y, 0) this.node.angle = 90 } } else { //正常子弹:直线 tween 使用父节点本地坐标 targetPos = this.worldToSkillParentLocal(this.getMonsterAimWorldPos(nearestMonster)); } if (this.weaponData.flight == SkillType.直线穿透) { // 穿透弹:过怪物点后沿飞行方向继续飞出屏幕,不在怪物处停住 const dx = targetPos.x - originPos.x; const dy = targetPos.y - originPos.y; const len = Math.hypot(dx, dy); const extendDist = 1200; if (len > 1e-6) { targetPos = v3( targetPos.x + (dx / len) * extendDist, targetPos.y + (dy / len) * extendDist, 0, ); } else { targetPos = v3(targetPos.x, targetPos.y - extendDist, 0); } } // 朝着目标方向旋转角度(该武器美术默认朝向为 +X) if (!this.isSplitBullet) { const dx2 = targetPos.x - originPos.x; const dy2 = targetPos.y - originPos.y; if (dx2 * dx2 + dy2 * dy2 > 1e-6) { const angleRad = Math.atan2(dy2, dx2); const angleDeg = angleRad * 180 / Math.PI; const offsetDeg = this.getFacingOffsetDeg(); const sign = this.getFacingSign(); const appliedDeg = angleDeg * sign + offsetDeg; this.setFacingAngleDeg(appliedDeg); } } targetScale = this.node.scale.clone(); let isBulletTurnBack = false let isExtraExplode = false let isBulletMoreLarge = false let moreLargeScale = 1 let explodeRadius = 0 let explodeTimes = 0 let specialSkillInfo = this.weaponPropertyData.specialSkillInfo for (let i = 0; i < specialSkillInfo.length; i++) { let buffInfo = specialSkillInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //特殊技能,折返 if (buffType == SpecialSkillType.bulletTurnBack) { isBulletTurnBack = true //越来越大 } else if (buffType == SpecialSkillType.moreLarge) { isBulletMoreLarge = true moreLargeScale = Number(arr[1]) targetScale = v3(moreLargeScale, moreLargeScale, 1) }else if (buffType == SpecialSkillType.extraExplode) { isExtraExplode = true explodeRadius = Number(arr[1]) explodeTimes = Number(arr[2]) } } this.straightAtkTo(this.node, targetPos.clone(), targetScale, () => { // this.node.destroy() // console.log('到达目标位置!') if (isBulletTurnBack) { //往返子弹 fix me } else { // 直线段结束必须回收:若命中过但因 Monster 里 isValid(other) 为 false 漏了 removeBullet, // isColliderMonster 会一直为 true,这里又判 !collider 不删 → 子弹永久残留(飞到极大坐标)。 this.removeBullet(); } }) }else if (this.weaponData.flight == SkillType.抛物线碰撞) { //如果是分裂子弹 if(this.weaponData.weaponid == WeaponType.FeiJian && this.isSplitBullet){ //随机角度 let targetPos = v3(0, 0, 0) // 以“Y 轴正方向(向上)”为 0°,顺时针为正; // 分裂子弹需要往下飞:以 180°(向下)为中心,在扇形内随机,避免 360° 乱飞 // splitAngleRandom > 0 时表示半角(度),例如 50 则随机范围 [-50, 50];为 0 时用默认半角 const halfDeg = this.splitAngleRandom > 0 ? this.splitAngleRandom : 55 const offset = Math.floor(Math.random() * (halfDeg * 2 + 1)) - halfDeg const angle = 180 + offset // 根据随机角度计算飞向屏幕四周的目标位置 // 使用固定的距离值确保子弹能飞出屏幕 let distance = 1000 // 足够飞出屏幕的距离 let radian = angle * Math.PI / 180 // 角度转弧度 // 计算目标位置 // angle=0 => 向上 (0, +distance) let dx = distance * Math.sin(radian) let dy = distance * Math.cos(radian) targetPos = v3(originPos.x + dx, originPos.y + dy, 0) // 设置节点角度 // Cocos 2D angle 逆时针为正;我们这里 angle 是顺时针为正,所以取负号 this.node.angle = -angle this.straightAtkTo(this.node, targetPos.clone(), v3(1, 1, 1), () => { this.removeBullet() }) return } this.bezierAtkTo(this.node, this.targetPos, () => { this.onBezierAtkToEnd() }) } else if (this.weaponData.flight == SkillType.抛物线没有碰撞) { this.bezierAtkTo(this.node, this.targetPos, () => { this.onBezierAtkToEnd() }) } else if (this.weaponData.flight == SkillType.直线目标持续伤害) { let targetPos = v3(0, 0, 0) let nearestMonster = this.pickNearestMonsterAvoidLast(); if (!nearestMonster?.isValid) { this.removeBullet(); return; } targetPos = this.worldToSkillParentLocal(this.getMonsterAimWorldPos(nearestMonster)); const curScale = this.weaponPropertyData?.volume || this.node.scale.x || 1; let targetScale = v3(curScale, curScale, curScale) this.node.setScale(targetScale); this.straightAtkTo(this.node, targetPos.clone(), targetScale, () => { this._attachStraightSustainScheduleAfterArrive(); }) } else if (this.weaponData.flight == SkillType.无弹道单体) { // 寻找 monster,直接瞬移到其身上,播放武器 Spine,0.5s 后对该怪结算伤害并回收 const nearestMonster = this.pickNearestMonsterByIndex(this.splitIndex); if (!nearestMonster?.isValid) { this.removeBullet(); return; } const monsterComp = nearestMonster.getComponent(Monster); if (!monsterComp || monsterComp.getIsDead()) { this.removeBullet(); return; } const parentUi = this.node.parent?.getComponent(UITransform); const targetPos = parentUi ? parentUi.convertToNodeSpaceAR(this.getMonsterAimWorldPos(nearestMonster)) : this.getMonsterAimWorldPos(nearestMonster); const curScale = this.weaponPropertyData?.volume || this.node.scale.x || 1; const targetScale = v3(curScale, curScale, curScale); this.node.setScale(targetScale); Tween.stopAllByTarget(this.node); TweenSystem.instance.ActionManager.resumeTarget(this.node); const parabola = this.node.getComponent(ConstantSpeedParabola); if (parabola) { parabola.enableRotation = false; } this.node.setPosition(targetPos); // const dx = targetPos.x - originPos.x; // const dy = targetPos.y - originPos.y; // if (dx * dx + dy * dy > 1e-6) { // const angleRad = Math.atan2(dy, dx); // const angleDeg = angleRad * 180 / Math.PI; // const offsetDeg = this.getFacingOffsetDeg(); // const sign = this.getFacingSign(); // this.setFacingAngleDeg(angleDeg * sign + offsetDeg); // } if (this.Sk?.isValid) { this.Sk.setAnimation(0, 'animation', true); } this.scheduleOnce(() => { if (!this.node?.isValid || this.isDead) return; if (!nearestMonster?.isValid) { this.removeBullet(); return; } const mc = nearestMonster.getComponent(Monster); if (!mc || mc.getIsDead()) { this.removeBullet(); return; } // 与碰撞命中一致:特殊技能表现 + buff + 受击动画 + 扣血(内部会调 attackBySpecialWeapon) mc.attackByWeapon(this.weaponData.weaponid, this.node, this.tetriLevel); this.removeBullet(); }, 0.5); } } /** * 直线目标持续伤害:到达目标点后挂 SustainSchedule,按武器表 effect_range 间隔/持续/范围结算,结束时回收子弹。 * allAtk 已含 damageMultiple,tick 内 _damageTimes 传 1 避免叠乘。 */ private _attachStraightSustainScheduleAfterArrive(): void { if (!this.node?.isValid || this.isDead) return; const wp = this.weaponPropertyData; const curScale = this.weaponPropertyData?.volume || 1; const interval = Math.max(0.05, wp?.damageInterval || 1); const continueT = Math.max(interval, wp?.weaponContinueTime || interval); const repeatCount = Math.max(1, Math.floor(continueT / interval)); const radius = Math.max(0, wp?.explodeRadius ?? 0) * curScale; let sustain = this.node.getComponent(SustainSchedule); if (!sustain) sustain = this.node.addComponent(SustainSchedule); sustain.resetSustainAreaEffe( this.node.worldPosition.clone(), radius, 1, this.weaponData.weaponid, interval, repeatCount, continueT, this.node, this.tetriLevel, ); } /** * 若拖尾曾被挂到 ui_skillTraillayer 且 followTarget 为本节点的 bullet03,回收前挂回 prefab 原位(与 Skill.onLoad 成对)。 */ private _restoreDetachedWeaponTrail(): void { const layer = gg?.game?.CurentBattle?.ui_skillTraillayer; if (!layer?.isValid || !this.node?.isValid) return; const bullet03 = find('bullet03', this.node); if (!bullet03?.isValid) return; const trails = layer.getComponentsInChildren(SuperTrail); for (let i = 0; i < trails.length; i++) { const st = trails[i]; if (st.followTarget === bullet03) { st.restoreOriginalPlacement(true); break; } } } /**移除子弹 */ removeBullet() { //console.log(`武器ID ${this.weaponData.weaponid} 移除子弹, this.uuid=${this.node.uuid}, this.isDead=${this.isDead}`) if (this.isDead) { //console.log('removeBullet 已死亡') return } this._restoreDetachedWeaponTrail(); this.isDead = true const n = this.node; this.scheduleOnce(() => { if (isValid(n)) { gg.res.putNode(n); } }, 0); // gg.res.putNode(this.node) } /**是否死亡 */ getIsDead() { return this.isDead } setIsDead(b: boolean) { this.isDead = b } //弹幕武器穿透数量-1 subPiercingCount() { return --this.piercingCount } /**是否碰撞过怪物 */ setColliderMonster(b: boolean) { this.isColliderMonster = b } /**是否碰撞过怪物 */ getColliderMonster(): boolean { return this.isColliderMonster } /** 是否武器20直线穿透(同怪仅一次碰撞伤害) */ isWeapon20StraightPierce(): boolean { return this.weaponData?.flight === SkillType.直线穿透; } hasPierceHitMonster(monsterUuid: string): boolean { return this._pierceHitMonsterUuids.has(monsterUuid); } markPierceHitMonster(monsterUuid: string): void { this._pierceHitMonsterUuids.add(monsterUuid); } /**是否是分裂的母体 */ setSplitParent(b: boolean) { this.isSplitParent = b } /**是否是分裂的母体 */ getSplitParent(): boolean { return this.isSplitParent } /**设置特殊技能效果 */ setSpecialSkillEffect() { let showEffect = (name,children) => { for (let i = 0; i < children.length; i++) { let child = children[i] if (child && child.name.includes(name)) { child.active = true } else if (child && child.name.includes('bullet0')) { child.active = false } } } //this.bulletLevel = gg.game.CurentBattle.getWeaponBulletLevel(this.weaponData.weaponid) //显示 let children = this.node.children if(this.weaponData.weaponid == WeaponType.JiGuangQiang){ children = this.node.getChildByName('line').children } showEffect('bullet03', children) } /**抛物线/落地武器到达落点后的逻辑处理 */ onBezierAtkToEnd() { let isHaveSecondExplodeSkill = false let haveSecondExplodeSkillTime = 0 if (this.weaponData.flight == SkillType.抛物线碰撞) { let isHavaFallDamageSkill = false let isHaveFallBurnAreaSkill = false let isHaveFallRatotionSkill = false let isHaveExtraDamageSkill = false let fallDamageAtkArr = [] let fallBurnAreaAtkArr = [] let fallRatotionAtkArr = [] let extraDamageAtkArr = [] //先统计拥有哪些特殊技能 let specialSkillAddFallEffectInfo = this.weaponPropertyData.specialSkillAddFallEffectInfo for (let i = 0; i < specialSkillAddFallEffectInfo.length; i++) { let buffInfo = specialSkillAddFallEffectInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //添加落地伤害 if (buffType == BezierFallEffectType.fallDamage) { isHavaFallDamageSkill = true fallDamageAtkArr = arr } else if (buffType == BezierFallEffectType.fallBurnArea) { isHaveFallBurnAreaSkill = true fallBurnAreaAtkArr = arr } else if (buffType == BezierFallEffectType.fallRatotion) { isHaveFallRatotionSkill = true fallRatotionAtkArr = arr } } //由于技能之间有叠加效果,这里重新开一个循环 for (let i = 0; i < specialSkillAddFallEffectInfo.length; i++) { let buffInfo = specialSkillAddFallEffectInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //添加落地伤害 if (buffType == BezierFallEffectType.fallDamage) { let damageRadius = Number(arr[1])//伤害半径 let damageTimes = Number(arr[2])//伤害倍数 if (!isHaveFallRatotionSkill) { this.playHalfWaterShockWave('半桶水冲击波', this.targetPos, 1, damageRadius, this.weaponPropertyData.allAtk * damageTimes, this.weaponPropertyData.isCrit, this.weaponPropertyData.addRateAtk) } //落地添加燃烧区域 } else if (buffType == BezierFallEffectType.fallBurnArea) { let damageRadius = Number(arr[1])//伤害半径 let addBuffInterval = Number(arr[2])//添加buff间隔 let areaTime = Number(arr[3])//总区域时间 let buffInterval = Number(arr[4])//buff触发间隔 let damageTimes = Number(arr[5])//伤害倍数 let buffTotalTime = Number(arr[6])//buff持续时间 let repeatCount = Math.floor(areaTime / addBuffInterval) if (!isHaveFallRatotionSkill) { this.playHalfWaterMagma('符文爆炸持续', this.targetPos, 1, damageRadius, damageTimes, this.weaponData.weaponid, buffInterval, buffTotalTime, addBuffInterval, repeatCount) } //落地翻滚造成伤害 } // else if (buffType == BezierFallEffectType.extraDamage) { // //额外伤害 // let damageTimes = Number(arr[1])/10000//伤害倍数 // //添加一个技能表现 作用一次就行 // let repeatCount = 1 // this.playExtraDamage('额外伤害', this.targetPos, 1, damageTimes, this.weaponData.weaponid, repeatCount) // } } if (!isHaveFallRatotionSkill) { this.removeBullet() } } else if (this.weaponData.flight == SkillType.抛物线没有碰撞) { let isAddFire = false let specialSkillInfo = this.weaponPropertyData.specialSkillInfo for (let i = 0; i < specialSkillInfo.length; i++) { let buffInfo = specialSkillInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //汽水炸弹身披火焰 if (buffType == SpecialSkillType.addFire) { isAddFire = true } } // if (this.weaponData.weaponid == WeaponType.QiShuiZhaDan) { // let spineName = isAddFire ? '汽水炸弹落地增强' : '汽水炸弹落地' // this.playColaBomb(spineName, this.targetPos, this.weaponPropertyData.explodeRTime) // } //爆炸范围内的怪物 let pos = this.node.worldPosition; //console.log('爆炸范围内的怪物==', this.weaponPropertyData.explodeRadius) let bombMonsters = gg.game.CurentBattle.MonsterSpawner.getMonstersInArea(pos, this.weaponPropertyData.explodeRadius); //console.log('bombMonsters=',bombMonsters.length) bombMonsters.forEach(monster => { monster.getComponent(Monster).attackByWeapon(this.weaponData.weaponid) }); let specialSkillAddFallEffectInfo = this.weaponPropertyData.specialSkillAddFallEffectInfo for (let i = 0; i < specialSkillAddFallEffectInfo.length; i++) { let buffInfo = specialSkillAddFallEffectInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //落地形成粘液区域 if (buffType == BezierFallEffectType.fallMucusArea) { let damageRadius = Number(arr[1])//伤害半径 let damageInterval = Number(arr[2])//伤害间隔 let damageTimes = Number(arr[3])//伤害倍数 let continueTime = Number(arr[4])//持续时间 let repeatCount = Math.floor(continueTime / damageInterval) this.playColaMucusArea('汽水炸弹粘液', this.targetPos, 1, damageRadius, damageTimes, this.weaponData.weaponid, damageInterval, repeatCount) //落地形成漩涡 } else if (buffType == BezierFallEffectType.fallVortex) { let damageRadius = Number(arr[1])//伤害半径 let damageInterval = Number(arr[2])//伤害间隔 let damageTimes = Number(arr[3])//伤害倍数 let continueTime = Number(arr[4])//持续时间 let repeatCount = Math.floor(continueTime / damageInterval) this.playColaVortexArea('汽水炸弹漩涡', this.targetPos, 1, damageRadius, damageTimes, this.weaponData.weaponid, damageInterval, repeatCount) } else if (buffType == BezierFallEffectType.fallBurnArea) { let damageRadius = Number(arr[1])//伤害半径 let addBuffInterval = Number(arr[2])//Number(arr[2])//添加buff间隔 let areaTime = Number(arr[4])//总区域时间 let buffInterval = Number(arr[2])//buff触发间隔 let damageTimes = Number(arr[3])//伤害倍数 let buffTotalTime = Number(arr[4])//buff持续时间 let repeatCount = Math.floor(areaTime / addBuffInterval) //2,106,1,2,3 this.playHalfWaterMagma('符文爆炸持续', this.targetPos, 1, damageRadius, damageTimes, this.weaponData.weaponid, buffInterval, buffTotalTime, addBuffInterval, repeatCount) //落地翻滚造成伤害 } //落地伤害 if(buffType == BezierFallEffectType.fallDamage){ let damageRadius = Number(arr[1])//伤害半径 let damageTimes = Number(arr[2])//伤害倍数 // let fallMonsters = gg.game.CurentBattle.MonsterSpawner.getMonstersInArea(pos, damageRadius) //播放一次爆炸 this.playColaBomb('符文爆炸', this.node.position.clone(), 1) for(let i = 0; i < fallMonsters.length; i++){ let monster = fallMonsters[i] monster.getComponent(Monster).subHP(this.weaponData.weaponid, this.weaponPropertyData.allAtk * damageTimes, this.weaponPropertyData.isCrit, this.weaponPropertyData.addRateAtk) } } } if (!isHaveSecondExplodeSkill) { this.removeBullet() } } } /**汽水炸弹爆炸 */ public playColaBomb(bombSpine: string, pos: Vec3, scale: number) { if (!BattlePerformance.rollShowCombatVfx()) return; const mapNode = gg.game.CurentBattle?.curMap?.node; if (!mapNode?.isValid) return; const localPos = pos.clone(); void gg.res.getNodeAsync(`prefab/爆炸/`, bombSpine, GBundle.BattleGameWeapon).then((spineNode) => { if (!spineNode?.isValid || !mapNode?.isValid) return; spineNode.setParent(mapNode); spineNode.setPosition(localPos); const adj = BattlePerformance.adjustVfxScale(scale); spineNode.scale = v3(adj, adj, 0); const bombComp = spineNode.getComponent(BombSpine); if (!bombComp) { gg.res.putNode(spineNode); return; } bombComp.playSpine('play', () => { bombComp.destoryNode(); }, 1, false); }); } /**汽水炸弹形成粘液区域 */ public playColaMucusArea(bombSpine: string, pos: Vec3, scale: number, damageRadius: number, damageTimes: number, weaponid, damageInterval: number, repeatCount: number) { let bombComp = this.createBombComponent(bombSpine, pos, scale, null, false) if (!bombComp) return; bombComp.playSpine('出现', () => { bombComp.playSpine('待机', () => { }, 1, true) }, 1, false) let com = bombComp.node.getComponent(ColaMucusAreaSchedule) if (!com) { com = bombComp.node.addComponent(ColaMucusAreaSchedule) } com.resetColaMucusAreaEffe(pos.clone(), damageRadius, damageTimes, weaponid, damageInterval, repeatCount) } /**汽水炸弹形成漩涡区域 */ public playColaVortexArea(bombSpine: string, pos: Vec3, scale: number, damageRadius: number, damageTimes: number, weaponid: number, damageInterval: number, repeatCount: number) { let bombComp = this.createBombComponent(bombSpine, pos, scale, null, false) if (!bombComp) return; bombComp.playSpine('play', () => { }, 1, true) let com = bombComp.node.getComponent(ColaVortexAreaSchedule) if (!com) { com = bombComp.node.addComponent(ColaVortexAreaSchedule) } com.resetColaVortexAreaEffe(pos.clone(), damageRadius, damageTimes, weaponid, damageInterval, repeatCount) } /**半桶水冲击波爆炸 */ public playHalfWaterShockWave(bombSpine: string, pos: Vec3, scale: number, damageRadius: number, damage: number, isCrit: boolean, addRateAtk: number) { //gg.audio.playEffect({ name: "半桶水冲击波", path: 'sound/武器音效/' }); let bombComp = this.createBombComponent(bombSpine, pos, scale) if (bombComp) { bombComp.playSpine('play', () => { bombComp.destoryNode() }, 1, false) } //计算伤害范围内的怪物 let bombMonsters = gg.game.CurentBattle.MonsterSpawner.getMonstersInArea(pos, damageRadius); bombMonsters.forEach(monster => { monster.getComponent(Monster).subHP((WeaponType as any).BanTongShui, damage, isCrit, addRateAtk) }); } public playExtraDamage(bombSpine: string, pos: Vec3, scale: number, damageTimes: number, weaponid: number, repeatCount: number){ // let bombComp = this.createBombComponent(bombSpine, pos, scale) // bombComp.playSpine('play', () => { // bombComp.destoryNode() // }, 1, false) } /**半桶水岩浆燃烧 */ public playHalfWaterMagma(bombSpine: string, pos: Vec3, scale: number, damageRadius: number, damageTimes: number, weaponid: number, buffInterval: number, buffTotalTime: number, addBuffInterval: number, repeatCount: number) { let bombComp = this.createBombComponent(bombSpine, pos, scale, null, false) if (!bombComp) return; bombComp.playSpine('出现', () => { bombComp.playSpine('燃烧', () => { }, 1, true) }, 1, false) let com = bombComp.node.getComponent(MagmaBurnSchedule) if (!com) { com = bombComp.node.addComponent(MagmaBurnSchedule) } com.resetMagmaEffe(pos.clone(), damageRadius, damageTimes, weaponid, buffInterval, buffTotalTime, addBuffInterval, repeatCount) } /**牛皮糖爆炸 */ public playStickyCandyBomb(bombSpine: string, pos: Vec3, scale: number,) { if (BattlePerformance.shouldPlayWeaponSfx('sound/武器音效/', '牛皮糖溅水')) { gg.audio.playEffect({ name: '牛皮糖溅水', path: 'sound/武器音效/' }); } let bombComp = this.createBombComponent(bombSpine, pos, scale) if (!bombComp) return; bombComp.playSpine('爆炸', () => { bombComp.destoryNode() }, 1, false) } /**生成爆炸Component */ public createBombComponent(bombSpine: string, pos: Vec3, scale: number, parentNode: Node = null, allowSkip = true) { if (allowSkip && !BattlePerformance.rollShowCombatVfx()) { return null; } let spineNode = gg.res.getNode(`prefab/爆炸/`, `${bombSpine}`, GBundle.BattleGameWeapon) if (!spineNode) { console.warn('[SkillBase] bombSpine 未就绪=', bombSpine); return null; } const parent = parentNode?.isValid ? parentNode : gg.game.CurentBattle?.curMap?.node; if (!parent?.isValid) { gg.res.putNode(spineNode); return null; } spineNode.setParent(parent); spineNode.setPosition(pos) const adj = BattlePerformance.adjustVfxScale(scale); spineNode.scale = v3(adj, adj, 0) let bombComp = spineNode.getComponent(BombSpine); return bombComp } //生成冰锥 (动画参数)半径范围 伤害 是否暴击 public playIceCone(bombSpine: string, pos: Vec3, scale: number, damageRadius: number, damage: number, isCrit: boolean, addRateAtk: number, parentNode: Node) { //gg.audio.playEffect({ name: '牛皮糖溅水',path:'sound/武器音效/'}); //注意技能会带体积变大 let bombComp = this.createBombComponent(bombSpine, pos, scale, parentNode, false) if (!bombComp) return; bombComp.playSpine('animation', () => { bombComp.destoryNode() }, 1, false) // 伤害范围必须与冰锥落点一致;原先用武器节点位置会导致与特效错位(竖屏战斗更明显) const wp = bombComp.node.worldPosition; let bombMonsters = gg.game.CurentBattle.MonsterSpawner.getMonstersInArea(wp, damageRadius); bombMonsters.forEach(monster => { monster.getComponent(Monster).subHP((WeaponType as any).IceBar, damage, isCrit, addRateAtk) }); } private getMucusSkinByQuality() { if (this.weaponPropertyData?.isHaveOrangeSkill) return "3橙色"; if (this.weaponPropertyData?.isHavePurpleSkill) return "2紫色"; return "1蓝色"; } //跳跳糖形成闪电区域 public playJumpSugerMucusArea(bombSpine: string, pos: Vec3, scale: number, damageRadius: number, damageTimes: number, weaponid, damageInterval: number, repeatCount: number, skinName: string = "") { let bombComp = this.createBombComponent(bombSpine, pos, scale, null, false) if (!bombComp) return; if (skinName) { const sk = bombComp.node.getComponentInChildren(sp.Skeleton); sk?.setSkin(skinName); } bombComp.playSpine('出现', () => { bombComp.playSpine('待机', () => { }, 1, true) }, 1, false) let com = bombComp.node.getComponent(ColaMucusAreaSchedule) if (!com) { com = bombComp.node.addComponent(ColaMucusAreaSchedule) } com.resetColaMucusAreaEffe(pos.clone(), damageRadius, damageTimes, weaponid, damageInterval, repeatCount) } //生成闪电区域 public async playLightningArea(endPosi) { let specialSkillAddFallEffectInfo = this.weaponPropertyData.specialSkillAddFallEffectInfo if (specialSkillAddFallEffectInfo.length == 0) { return } for (let i = 0; i < specialSkillAddFallEffectInfo.length; i++) { let buffInfo = specialSkillAddFallEffectInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //落地形成粘液区域 if (buffType == BezierFallEffectType.fallMucusArea) { let damageRadius = Number(arr[1])//伤害半径 let damageInterval = Number(arr[2])//伤害间隔 let damageTimes = Number(arr[3])//伤害倍数 let continueTime = Number(arr[4])//持续时间 let repeatCount = Math.floor(continueTime / damageInterval) this.playJumpSugerMucusArea('跳跳糖雷电', endPosi, 1, damageRadius, damageTimes, this.weaponData.weaponid, damageInterval, repeatCount) } } } //生成泡泡消毒液 public async playBubbleSanitizer(endPosi) { //需要更新一下this.weaponPropertyData 属性,不然有些泡泡不会带这个技能 let specialSkillAddFallEffectInfo = this.weaponPropertyData.specialSkillAddFallEffectInfo if (specialSkillAddFallEffectInfo.length == 0) { return } for (let i = 0; i < specialSkillAddFallEffectInfo.length; i++) { let buffInfo = specialSkillAddFallEffectInfo[i] let arr = buffInfo.split(',') let buffType = Number(arr[0]) //落地形成粘液区域 if (buffType == BezierFallEffectType.fallMucusArea) { let damageRadius = Number(arr[1])//伤害半径 let damageInterval = Number(arr[2])//伤害间隔 let damageTimes = Number(arr[3])//伤害倍数 let continueTime = Number(arr[4])//持续时间 let repeatCount = Math.floor(continueTime / damageInterval) this.playJumpSugerMucusArea('泡泡粘液', endPosi, 1, damageRadius, damageTimes, this.weaponData.weaponid, damageInterval, repeatCount, this.getMucusSkinByQuality()) } } } /** * 直线攻击到打击点 * @param self * @param enemy * @param enemyTarget * @param speed * @param enemySpeed * @param callback */ public straightAtkTo(self: Node, targetPos: Vec3, targetScale: Vec3, callback: Function) { let dis = Vec3.distance(self.position, targetPos) const rawSpeed = (this.weaponPropertyData?.speed ?? 0) * (gg.game.CurentBattle?.TimeScale ?? 1) const speed = Math.max(1e-4, rawSpeed) let dt = dis / speed if (!Number.isFinite(dt) || dt <= 0) { callback?.() return } Tween.stopAllByTarget(self); TweenSystem.instance.ActionManager.resumeTarget(self) // 若节点挂了抛物线组件,可能存在 onUpdate 每帧覆写旋转;直线飞行时必须关掉旋转控制权 const parabola = self.getComponent(ConstantSpeedParabola); if (parabola) { parabola.enableRotation = false; } tween(self) .to(dt, { position: targetPos, scale: targetScale }) .call(() => { callback && callback() }) .start(); } /** * 抛物线攻击到打击点 * @param self * @param enemy * @param enemyTarget * @param speed * @param enemySpeed * @param callback */ public bezierAtkTo(self: Node, targetPos: Vec3, callback: Function, finishTime: number = -1, speed: number = this.weaponPropertyData.speed) { const parabola = this.node.getComponent(ConstantSpeedParabola); if (parabola) { // 抛物线旋转的偏移与直线朝向偏移保持一致(两边对齐后不会出现“直线/抛物线角度不一致”) (parabola as any).rotationOffsetDeg = this.getFacingOffsetDeg(); parabola.enableRotation = true; parabola.startMotion(self, targetPos, speed, callback, finishTime); } else { // 没挂组件就退化为直线,避免直接报错导致子弹残留 this.straightAtkTo(self, targetPos, self.scale.clone(), () => callback && callback()); } } // 榴莲跟随路径移动 private followPath(deltaTime: number) { if (!this.weaponData) { return } if (this.weaponData.flight != (SkillType as any).榴莲 || this._currentPath.length <= 0 || !gg.game.CurentBattle.canUpdateFrame()) { return; } // 获取当前目标点 const targetPoint = this._currentPath[this._targetIndex]; // 避免 Vec2/Vec3 clone/normalize/multiplyScalar/add 产生的频繁分配 // 直接用 x/y 计算方向与移动 const nx0 = targetPoint.x - this.node.position.x; const ny0 = targetPoint.y - this.node.position.y; const distSq = nx0 * nx0 + ny0 * ny0; const moveDistance = this.weaponPropertyData.speed * deltaTime; const moveDistSq = moveDistance * moveDistance; // 如果距离目标点小于这一帧的移动距离,则直接到达目标点 if (distSq <= moveDistSq || distSq < 1e-8) { // 2D 游戏保持 z=0,避免位置 z 累积误差 this.node.setPosition(targetPoint.x, targetPoint.y, 0); this._targetIndex++; // 检查是否到达终点 if (this._targetIndex >= this._currentPath.length) { this._currentPath = [] this._targetIndex = 0 this.removeBullet() return; } } else { const invDist = 1 / Math.sqrt(distSq); const dx = nx0 * invDist; const dy = ny0 * invDist; const newX = this.node.position.x + dx * moveDistance; const newY = this.node.position.y + dy * moveDistance; this.node.setPosition(newX, newY, 0); } } // 暂停该节点上所有动作 pauseTween() { if (!this.isTweenPaused) { TweenSystem.instance.ActionManager.pauseTarget(this.node); } this.isTweenPaused = true } // 恢复该节点上所有动作 resumeTween() { if (this.isTweenPaused) { TweenSystem.instance.ActionManager.resumeTarget(this.node); } this.isTweenPaused = false } /**仍在与「上一只弹射命中怪」重叠时,跳过该怪的碰撞(避免换靶瞬间二次结算) */ public shouldIgnoreCatapultContactFrom(monsterUuid: string): boolean { return !!this._catapultLastHitMonsterUuid && monsterUuid === this._catapultLastHitMonsterUuid } findNextTarget(excludeMonsterUuid?: string) { //继续寻找目标(弹射次数在“命中碰撞”时扣减;这里不扣,避免 miss 也扣次数) if (excludeMonsterUuid) { this._catapultLastHitMonsterUuid = excludeMonsterUuid } const launch_range = Math.max(1, Number(this.weaponData?.launch_range) || 0) const nearestMonster = this.pickNearestMonsterInArea(launch_range) //继续朝着nearestMonster目标发射 if(nearestMonster){ // 开始这一段飞行前先清掉“已命中”标记,用于到达终点时判断是否 miss this.setColliderMonster(false) const parentUi = this.node.parent?.getComponent(UITransform); let targetPos = parentUi ? parentUi.convertToNodeSpaceAR(this.getMonsterAimWorldPos(nearestMonster)) : this.getMonsterAimWorldPos(nearestMonster); let targetScale = this.node.scale.clone(); // 朝着目标方向旋转角度(保持与 startAtk() 相同的美术朝向规则) const originPos = this.node.position; // 沿飞向新目标方向微移,减轻仍卡在上一只怪 AABB 内时同帧反复触发碰撞 { const nx = targetPos.x - originPos.x const ny = targetPos.y - originPos.y const len = Math.hypot(nx, ny) if (len > 1e-4) { const step = Math.min(14, Math.max(4, len * 0.04)) this.node.setPosition(originPos.x + (nx / len) * step, originPos.y + (ny / len) * step, originPos.z) } } const originAfterNudge = this.node.position const dx2 = targetPos.x - originAfterNudge.x; const dy2 = targetPos.y - originAfterNudge.y; if (dx2 * dx2 + dy2 * dy2 > 1e-6) { const angleRad = Math.atan2(dy2, dx2); // 保持与 startAtk() 相同的美术朝向规则(两处必须一致,否则会出现“反向飞”) const angleDeg = angleRad * 180 / Math.PI; const offsetDeg = this.getFacingOffsetDeg(); const sign = this.getFacingSign(); const appliedDeg = angleDeg * sign + offsetDeg; this.setFacingAngleDeg(appliedDeg); } this.straightAtkTo(this.node, targetPos.clone(), targetScale, () => { // 到达目标点但这一段没发生碰撞:大概率是 miss(怪移动/死亡/判定没触发),兜底回收避免残留。 if (!this.getColliderMonster()) { this.removeBullet() } }) }else{ this.removeBullet() } //朝着怪物方向旋转角度,武器的初始角度y轴正方向朝向 } /***选取怪物 */ public pickNearestMonsterByIndex(index: number): Node | null { //根据传递来的index来选取怪物。比如index=0选择最近,index=1选择第二近,index=2选择第三近,以此类推 const spawner = gg.game.CurentBattle?.MonsterSpawner; const weaponNode = this.node; if (!spawner || !weaponNode || !weaponNode.isValid) return null; const monsters: Node[] = spawner.node?.children ?? []; if (monsters.length <= 0) return null; const arr: Array<{ n: Node; d: number }> = []; for (let i = 0; i < monsters.length; i++) { const mNode = monsters[i]; if (!mNode || !mNode.isValid) continue; const mComp = mNode.getComponent(Monster); if (!mComp || mComp.getIsDead()) continue; const agent = mNode.getComponent(MapAgent); if (!agent) continue; const dis = agent.getNearestMonster(weaponNode); if (!Number.isFinite(dis)) continue; arr.push({ n: mNode, d: dis }); } if (arr.length <= 0) return null; arr.sort((a, b) => a.d - b.d); const idx = Math.max(0, Math.floor(index)); if (idx >= arr.length) return null; const out = arr[idx].n; return isValid(out) ? out : null; } /** * 以子弹世界坐标为圆心选最近活怪。launch_range 往往很小,命中后圆内常只剩当前怪; * 血厚的怪不会立刻消失,排除黑名单后小圆会空 → 需逐步扩大半径并最后全量兜底。 */ public pickNearestMonsterInArea(area: number): Node | null { const spawner = gg.game.CurentBattle?.MonsterSpawner; const weaponNode = this.node; if (!spawner || !weaponNode?.isValid) return null; const wp = weaponNode.worldPosition; const baseR = Math.max(1, Math.floor(Number(area) || 0)); const skipUuid = this._catapultLastHitMonsterUuid const collectFromNodes = (nodes: Node[]): Array<{ n: Node; d: number }> => { const arr: Array<{ n: Node; d: number }> = []; for (let i = 0; i < nodes.length; i++) { const mNode = nodes[i]; if (!mNode?.isValid) continue; if (skipUuid && mNode.uuid === skipUuid) continue; const mComp = mNode.getComponent(Monster); if (!mComp || mComp.getIsDead()) continue; if (!mNode.getComponent(MapAgent)) continue; const d = Vec3.distance(wp, mNode.worldPosition); if (!Number.isFinite(d)) continue; arr.push({ n: mNode, d }); } return arr; }; const pickNearest = (arr: Array<{ n: Node; d: number }>): Node | null => { if (arr.length <= 0) return null; arr.sort((a, b) => a.d - b.d); const out = arr[0].n; if (!out?.isValid) return null; this._lastTargetMonsterUuid = out.uuid; return out; }; const radii: number[] = []; const pushR = (r: number) => { const x = Math.max(1, Math.floor(r)); if (!radii.includes(x)) radii.push(x); }; pushR(baseR); pushR(Math.max(baseR, 360)); pushR(baseR * 2); pushR(baseR * 4); pushR(720); pushR(1600); for (let ri = 0; ri < radii.length; ri++) { const r = radii[ri]; const inArea = spawner.getMonstersInArea(wp, r) ?? []; const picked = pickNearest(collectFromNodes(inArea)); if (picked) return picked; } const all = spawner.node?.children ?? []; return pickNearest(collectFromNodes(all)); } /** 选择最近怪物,且尽量不与上一次目标重复;若该最近怪处于 ATTACKING,则固定选它(允许与上次重复)。 */ public pickNearestMonsterAvoidLast(): Node | null { const spawner = gg.game.CurentBattle?.MonsterSpawner; const weaponNode = this.node; if (!spawner || !weaponNode || !weaponNode.isValid) return null; const first = spawner.getAllPathDisMinMonster(weaponNode); if (!first || !isValid(first)) return null; const firstMonster = first.getComponent(Monster); if (firstMonster && firstMonster.state === MonsterState.ATTACKING) { this._lastTargetMonsterUuid = first.uuid; return first; } if (!this._lastTargetMonsterUuid || first.uuid !== this._lastTargetMonsterUuid) { this._lastTargetMonsterUuid = first.uuid; return first; } // 当前目标与上次相同:找第二近 let minDis = Infinity; let target: Node | null = null; const monsters: Node[] = spawner.node?.children ?? []; for (let i = 0; i < monsters.length; i++) { const mNode = monsters[i]; if (!mNode || !mNode.isValid) continue; if (mNode.uuid === this._lastTargetMonsterUuid) continue; const mComp = mNode.getComponent(Monster); if (!mComp || mComp.getIsDead()) continue; const agent = mNode.getComponent(MapAgent); if (!agent) continue; const dis = agent.getNearestMonster(weaponNode); if (dis < minDis) { minDis = dis; target = mNode; } } const out = target && isValid(target) ? target : first; this._lastTargetMonsterUuid = out.uuid; return out; } //splitNumbers get splitContinueNumbers() { return this._splitContinueNumbers } set splitContinueNumbers(value: number) { this._splitContinueNumbers = value } get isSplitBulletInvincible() { return this._isSplitBulletInvincible } set isSplitBulletInvincible(value: boolean) { this._isSplitBulletInvincible = value } }