|
|
|
|
@ -81,6 +81,8 @@ export enum PassiveResultKind { |
|
|
|
|
SwordWave = 11, |
|
|
|
|
/** 闪电链:蔓延范围像素,蔓延数量,伤害系数万分比 */ |
|
|
|
|
LightningChain = 12, |
|
|
|
|
/** 分裂冰锥:索敌半径像素,分裂数量,伤害系数万分比 */ |
|
|
|
|
IceConeSplit = 13, |
|
|
|
|
/** 爆炸:圆形半径像素,伤害系数万分比 */ |
|
|
|
|
Explosion = 14, |
|
|
|
|
} |
|
|
|
|
@ -103,6 +105,10 @@ const SWORD_WAVE_DMG = 'passive_sword_wave'; |
|
|
|
|
const LIGHTNING_CHAIN_DMG = 'passive_lightning_chain'; |
|
|
|
|
/** 爆炸伤害类型(不回触发 condition) */ |
|
|
|
|
const EXPLOSION_DMG = 'passive_explosion'; |
|
|
|
|
/** 分裂冰锥伤害类型(不回触发 condition) */ |
|
|
|
|
const ICE_CONE_DMG = 'passive_ice_cone'; |
|
|
|
|
/** 分裂冰锥飞行像素/秒(与水弹一致) */ |
|
|
|
|
const ICE_CONE_SPEED = 1280; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 是否为被动 result 造成的伤害(不回触发任何 condition)。 |
|
|
|
|
@ -122,6 +128,8 @@ export const LINE_DRAGON_EVENT = 'LineDragonApplied'; |
|
|
|
|
export const SWORD_WAVE_EVENT = 'SwordWaveApplied'; |
|
|
|
|
/** 闪电链表现事件名 */ |
|
|
|
|
export const LIGHTNING_CHAIN_EVENT = 'LightningChainApplied'; |
|
|
|
|
/** 分裂冰锥表现事件名 */ |
|
|
|
|
export const ICE_CONE_SPLIT_EVENT = 'IceConeSplitApplied'; |
|
|
|
|
/** 直线火龙预制体宽 100,命中半长(再加半格占地) */ |
|
|
|
|
const LINE_DRAGON_HALF_LEN = 50; |
|
|
|
|
/** 直线火龙预制体高 30,命中半宽(再加半格占地) */ |
|
|
|
|
@ -341,6 +349,38 @@ interface LineDragonFlight { |
|
|
|
|
hitIds: EntityId[]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 分裂冰锥一枚(待落地) */ |
|
|
|
|
interface IceConePending { |
|
|
|
|
/** 施法者 */ |
|
|
|
|
caster: EntityId; |
|
|
|
|
/** 锁定目标 */ |
|
|
|
|
target: EntityId; |
|
|
|
|
/** Skill.id */ |
|
|
|
|
skillId: number; |
|
|
|
|
/** 伤害倍率 */ |
|
|
|
|
scale: number; |
|
|
|
|
/** 距落地(秒) */ |
|
|
|
|
wait: number; |
|
|
|
|
/** atk_buff1 */ |
|
|
|
|
buff1: string; |
|
|
|
|
/** atk_buff2 */ |
|
|
|
|
buff2: string; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 分裂冰锥表现 cue(一枚) */ |
|
|
|
|
export interface IceConeSplitCue { |
|
|
|
|
/** 起点逻辑 X */ |
|
|
|
|
fromX: number; |
|
|
|
|
/** 起点逻辑 Y */ |
|
|
|
|
fromY: number; |
|
|
|
|
/** 终点逻辑 X */ |
|
|
|
|
toX: number; |
|
|
|
|
/** 终点逻辑 Y */ |
|
|
|
|
toY: number; |
|
|
|
|
/** 飞行秒 */ |
|
|
|
|
duration: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 待按间隔放出的剑气 */ |
|
|
|
|
interface SwordWavePending { |
|
|
|
|
/** 施法者 */ |
|
|
|
|
@ -423,6 +463,7 @@ export class PassiveSkillHost { |
|
|
|
|
private readonly _wavePending: SwordWavePending[] = []; |
|
|
|
|
private readonly _waves: SwordWaveFlight[] = []; |
|
|
|
|
private readonly _chains: LightningChainRun[] = []; |
|
|
|
|
private readonly _iceCones: IceConePending[] = []; |
|
|
|
|
private _session: BattleSession | null = null; |
|
|
|
|
/** result 结算中(含其产生的伤害/击杀),禁止再判任何 condition */ |
|
|
|
|
private _resultGate = 0; |
|
|
|
|
@ -446,6 +487,7 @@ export class PassiveSkillHost { |
|
|
|
|
this._wavePending.length = 0; |
|
|
|
|
this._waves.length = 0; |
|
|
|
|
this._chains.length = 0; |
|
|
|
|
this._iceCones.length = 0; |
|
|
|
|
this._session = null; |
|
|
|
|
this._atkOverride = null; |
|
|
|
|
} |
|
|
|
|
@ -460,6 +502,7 @@ export class PassiveSkillHost { |
|
|
|
|
this._removeZonesByCaster(entityId); |
|
|
|
|
this._removeWavesByCaster(entityId); |
|
|
|
|
this._removeChainsByCaster(entityId); |
|
|
|
|
this._removeIceConesByCaster(entityId); |
|
|
|
|
const idx = this._indexOf(entityId); |
|
|
|
|
if (idx >= 0) this._binds.splice(idx, 1); |
|
|
|
|
} |
|
|
|
|
@ -518,6 +561,7 @@ export class PassiveSkillHost { |
|
|
|
|
this._tickSwordWavePending(dt); |
|
|
|
|
this._tickSwordWaves(dt); |
|
|
|
|
this._tickLightningChains(dt); |
|
|
|
|
this._tickIceCones(dt); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
@ -781,6 +825,9 @@ export class PassiveSkillHost { |
|
|
|
|
case PassiveResultKind.LightningChain: |
|
|
|
|
this._applyLightningChain(session, caster, target, bind.skillId, p); |
|
|
|
|
return; |
|
|
|
|
case PassiveResultKind.IceConeSplit: |
|
|
|
|
this._applyIceConeSplit(session, caster, target, bind, p); |
|
|
|
|
return; |
|
|
|
|
case PassiveResultKind.Explosion: |
|
|
|
|
this._applyExplosion(session, caster, target, bind, p); |
|
|
|
|
return; |
|
|
|
|
@ -1409,6 +1456,134 @@ export class PassiveSkillHost { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 分裂冰锥:从触发目标向半径内其它敌发射最多 N 枚,落地伤并施加行上 atk_buff。 |
|
|
|
|
* @param session 战斗会话 |
|
|
|
|
* @param caster 施法者 |
|
|
|
|
* @param target 触发目标 |
|
|
|
|
* @param bind 被动绑定 |
|
|
|
|
* @param p 索敌半径, 分裂数量, 伤害万分比 |
|
|
|
|
*/ |
|
|
|
|
private _applyIceConeSplit(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, p: number[]): void { |
|
|
|
|
const radius = p[0]; |
|
|
|
|
const count = Math.floor(p[1]); |
|
|
|
|
const coeffWan = p[2]; |
|
|
|
|
if (!(radius > 0) || !(count > 0) || !(coeffWan > 0)) { |
|
|
|
|
console.log(`[PassiveSkill] result 13 参数无效 skill=${bind.skillId}`, p.join(',')); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const origin = this._logicPos(session, target > 0 ? target : caster) |
|
|
|
|
?? this._logicPos(session, caster); |
|
|
|
|
if (!origin) { |
|
|
|
|
console.log(`[PassiveSkill] result 13 无落点 skill=${bind.skillId}`); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const casterTeam = session.world.getComponent(caster, TeamComp); |
|
|
|
|
const cell = session.grid.worldToCell(origin.x, origin.y); |
|
|
|
|
const radiusCells = radius / Math.max(1, session.grid.cellSize); |
|
|
|
|
const ids = session.spatial.queryRadius(cell, radiusCells + 1); |
|
|
|
|
const r2 = radius * radius; |
|
|
|
|
const candidates: { id: EntityId; dist2: number }[] = []; |
|
|
|
|
for (let i = 0; i < ids.length; i++) { |
|
|
|
|
const id = ids[i]; |
|
|
|
|
if (id === caster || id === target) continue; |
|
|
|
|
if (!session.world.isAlive(id)) continue; |
|
|
|
|
const hp = session.world.getComponent(id, HealthComp); |
|
|
|
|
if (!hp || !(hp.hp > 0)) continue; |
|
|
|
|
const move = session.world.getComponent(id, MovementComp); |
|
|
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue; |
|
|
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue; |
|
|
|
|
const pos = this._logicPos(session, id); |
|
|
|
|
if (!pos) continue; |
|
|
|
|
const dx = pos.x - origin.x; |
|
|
|
|
const dy = pos.y - origin.y; |
|
|
|
|
const dist2 = dx * dx + dy * dy; |
|
|
|
|
if (dist2 > r2) continue; |
|
|
|
|
candidates.push({ id, dist2 }); |
|
|
|
|
} |
|
|
|
|
if (candidates.length === 0) return; |
|
|
|
|
this._sortByDist2(candidates); |
|
|
|
|
const scale = coeffWan / 10000; |
|
|
|
|
const buff1 = String(bind.atkBuff1 ?? '').trim(); |
|
|
|
|
const buff2 = String(bind.atkBuff2 ?? '').trim(); |
|
|
|
|
const n = Math.min(count, candidates.length); |
|
|
|
|
for (let i = 0; i < n; i++) { |
|
|
|
|
const tid = candidates[i].id; |
|
|
|
|
const aim = this._logicPos(session, tid); |
|
|
|
|
if (!aim) continue; |
|
|
|
|
const dx = aim.x - origin.x; |
|
|
|
|
const dy = aim.y - origin.y; |
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
const duration = Math.max(0.12, dist / ICE_CONE_SPEED); |
|
|
|
|
this._iceCones.push({ |
|
|
|
|
caster, |
|
|
|
|
target: tid, |
|
|
|
|
skillId: bind.skillId, |
|
|
|
|
scale, |
|
|
|
|
wait: duration, |
|
|
|
|
buff1, |
|
|
|
|
buff2, |
|
|
|
|
}); |
|
|
|
|
const cue: IceConeSplitCue = { |
|
|
|
|
fromX: origin.x, |
|
|
|
|
fromY: origin.y, |
|
|
|
|
toX: aim.x, |
|
|
|
|
toY: aim.y, |
|
|
|
|
duration, |
|
|
|
|
}; |
|
|
|
|
session.events.emit(BattleEventFactory.custom(ICE_CONE_SPLIT_EVENT, cue)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 推进分裂冰锥落地。 |
|
|
|
|
* @param dt 逻辑秒 |
|
|
|
|
*/ |
|
|
|
|
private _tickIceCones(dt: number): void { |
|
|
|
|
if (!(dt > 0) || this._iceCones.length <= 0) return; |
|
|
|
|
const session = this._session; |
|
|
|
|
if (!session) return; |
|
|
|
|
for (let i = this._iceCones.length - 1; i >= 0; i--) { |
|
|
|
|
const d = this._iceCones[i]; |
|
|
|
|
d.wait -= dt; |
|
|
|
|
if (d.wait > 0) continue; |
|
|
|
|
this._iceCones.splice(i, 1); |
|
|
|
|
if (!session.world.isAlive(d.caster) || !session.world.isAlive(d.target)) continue; |
|
|
|
|
const hp = session.world.getComponent(d.target, HealthComp); |
|
|
|
|
if (!hp || !(hp.hp > 0)) continue; |
|
|
|
|
const move = session.world.getComponent(d.target, MovementComp); |
|
|
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue; |
|
|
|
|
this._dealResultDamage(session, d.caster, d.target, d.skillId, d.scale, ICE_CONE_DMG); |
|
|
|
|
if (!d.buff1 && !d.buff2) continue; |
|
|
|
|
if (!session.world.isAlive(d.target)) continue; |
|
|
|
|
const abilityCtx = createAbilityContext(session, d.caster, `passive_${d.skillId}`, [d.target]); |
|
|
|
|
SkillAtkBuffExecutor.applyPair(session, abilityCtx, d.target, d.buff1, d.buff2, { fromPassiveResult: true }); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 卸下施法者时清其分裂冰锥 */ |
|
|
|
|
private _removeIceConesByCaster(caster: EntityId): void { |
|
|
|
|
for (let i = this._iceCones.length - 1; i >= 0; i--) { |
|
|
|
|
if (this._iceCones[i].caster === caster) this._iceCones.splice(i, 1); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 按 dist2 升序(插入排序,避免函数内回调)。 |
|
|
|
|
* @param list 候选 |
|
|
|
|
*/ |
|
|
|
|
private _sortByDist2(list: { id: EntityId; dist2: number }[]): void { |
|
|
|
|
for (let i = 1; i < list.length; i++) { |
|
|
|
|
const cur = list[i]; |
|
|
|
|
let j = i - 1; |
|
|
|
|
while (j >= 0 && list[j].dist2 > cur.dist2) { |
|
|
|
|
list[j + 1] = list[j]; |
|
|
|
|
j -= 1; |
|
|
|
|
} |
|
|
|
|
list[j + 1] = cur; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 爆炸:以触发目标为圆心圈伤;行上 atk_buff 非空时对命中目标施加。 |
|
|
|
|
* @param session 战斗会话 |
|
|
|
|
|