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
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
|
|
}
|
|
}
|
|
|