消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1352 lines
58 KiB

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<string>();
/**每个技能实例单独随机到的殊技能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
}
}