import type { BattleSession } from 'db://assets/mx/module/battle'; import { BattleEventFactory, BattleTeamId } from 'db://assets/mx/module/battle'; import { createAbilityContext } from 'db://assets/mx/module/battle/combat/AbilityContext'; import { VAR_LAST_DAMAGE_AMOUNT } from 'db://assets/mx/module/battle/combat/DamagePipeline'; import { AttrId, AttributeComp, HealthComp, MovementComp, TeamComp, TransformComp, isBajiaoBlowRetreating, isYanSoulClaimed, } from 'db://assets/mx/module/battle/ecs/Components'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { TableNames } from '../../game/ConfigTableData'; import { applySkillTableKnockback } from './AtkHookEffects'; import { applySkillAtkBuffBoardEffect } from './SkillAtkBuffBoardBridge'; import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor'; /** 被动 result 10 覆盖普攻(由装载服务实现,避免本文件再引用 Loadout) */ export interface IPassiveAtkOverrideHost { /** * 覆盖宿主普攻 atk_type / atk_para 并重建 Ability。 * @param session 战斗会话 * @param entityId 触发宿主 * @param atkType 新 atk_type * @param atkPara 新 atk_para * @param sourceSkillId 被动 Skill.id */ applyNormalAtkOverride(session: BattleSession, entityId: EntityId, atkType: string, atkPara: string, sourceSkillId: number): void; } /** bindFromLoadout 只需武器被动槽 */ interface PassiveLoadoutBind { /** 武器技能 Skill.id */ weaponSkillId: number; /** 武器技能是否为主动 */ weaponSkillActive: boolean; } /** 被动触发条件类型 */ export enum PassiveConditionKind { /** 造成伤害触发,参数:概率万分比 */ OnDealDamage = 1, /** 默认生效(部署时触发一次) */ Always = 2, /** 连续攻击同一单位 */ SameTarget = 3, /** 首次命中某单位 */ FirstHitTarget = 4, /** 击杀,参数:概率万分比 */ OnKill = 5, /** 技能释放次数(含普攻 AbilityCast),参数:次数 */ SkillCastCount = 6, } /** 被动触发结果类型 */ export enum PassiveResultKind { /** 剑阵:半径像素,间隔秒,伤害系数万分比,持续秒 */ SwordArray = 1, /** 提升攻速:加成万分比,最大层数,持续秒 */ Aspd = 2, /** 提升射程:倍数 */ RangeMul = 3, /** 击退:距离像素 */ Knockback = 4, /** 添加桃子:数量 */ Peach = 5, /** 眩晕:时长秒 */ Stun = 6, /** 爆炸火球多向穿透:最大数量(角=360/数量),伤害系数万分比,飞行速度像素/秒,飞行时间秒 */ HomingDragon = 7, /** 上毒:间隔秒,伤害系数万分比,持续秒 */ Poison = 8, /** 直线火龙:飞行速度像素/秒,伤害系数万分比,持续秒 */ LineDragon = 9, /** 修改普攻:覆盖宿主普攻 Skill.atk_type / atk_para */ ModifyAttack = 10, /** 直线剑气:数量,间隔秒,伤害系数万分比 */ SwordWave = 11, /** 闪电链:蔓延范围像素,蔓延数量,伤害系数万分比 */ LightningChain = 12, /** 分裂冰锥:索敌半径像素,分裂数量,伤害系数万分比 */ IceConeSplit = 13, /** 爆炸:圆形半径像素,伤害系数万分比 */ Explosion = 14, } /** 攻速 modifier group 前缀 */ const ASPD_GROUP = 'passive_aspd_'; /** 射程 modifier group 前缀 */ const RANGE_GROUP = 'passive_range_'; /** 毒跳伤害类型(不回触发 condition) */ const POISON_DMG = 'passive_poison'; /** 剑阵跳伤类型(不回触发 condition) */ const SWORD_ARRAY_DMG = 'passive_sword_array'; /** 爆炸火球伤害类型(不回触发 condition) */ const HOMING_DRAGON_DMG = 'passive_homing_dragon'; /** 直线火龙伤害类型(不回触发 condition) */ const LINE_DRAGON_DMG = 'passive_line_dragon'; /** 直线剑气伤害类型(不回触发 condition) */ const SWORD_WAVE_DMG = 'passive_sword_wave'; /** 闪电链伤害类型(不回触发 condition) */ 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)。 * @param damageType 伤害类型(约定 `passive_` 前缀) */ export function isPassiveResultDamage(damageType?: string): boolean { return !!damageType && damageType.indexOf('passive_') === 0; } /** 剑阵表现事件名 */ export const SWORD_ARRAY_EVENT = 'SwordArrayApplied'; /** 爆炸火球表现事件名 */ export const HOMING_DRAGON_EVENT = 'HomingDragonApplied'; /** 直线火龙表现事件名 */ 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,命中半宽(再加半格占地) */ const LINE_DRAGON_HALF_WID = 15; /** 剑气飞行速度(配表无速度段;与直线火龙常见 400 同量级) */ const SWORD_WAVE_SPEED = 400; /** 轩辕剑刀光动画 */ const SWORD_WAVE_ANIM_BLUE = '剑气蓝'; /** 吴钩双剑刀光动画 */ const SWORD_WAVE_ANIM_RED = '剑气红'; /** 玉帝刀光碰撞盒宽 100,命中半长 */ const SWORD_WAVE_HALF_LEN = 50; /** 玉帝刀光碰撞盒高 200,命中半宽 */ const SWORD_WAVE_HALF_WID = 100; /** 出界判定外扩(半长 + 一格) */ const SWORD_WAVE_BOUND_PAD = SWORD_WAVE_HALF_LEN + 80; /** 电母「普通雷电」资源时长 */ const LIGHTNING_BOLT_ANIM_SEC = 0.5333; /** 相对资源原速(与老项目传导倍率一致) */ const LIGHTNING_ANIM_SPEED = 2; /** 两跳间隔(逻辑秒) */ const LIGHTNING_HOP_SEC = LIGHTNING_BOLT_ANIM_SEC / LIGHTNING_ANIM_SPEED; /** 剑阵表现 cue */ export interface SwordArrayCue { /** 施法者 */ caster: EntityId; /** 落点目标(可 0) */ target: EntityId; /** 持续秒 */ duration: number; /** 半径像素(表现按预制体边长缩放) */ radius: number; } /** 爆炸火球一枚(径向直线) */ export interface HomingDragonMissile { /** 方向单位 X */ ux: number; /** 方向单位 Y */ uy: number; /** 终点逻辑 X */ destX: number; /** 终点逻辑 Y */ destY: number; /** 飞行秒 */ duration: number; } /** 爆炸火球表现 cue */ export interface HomingDragonCue { /** 施法者 */ caster: EntityId; /** 触发生成点实体(击杀目标等,可已死) */ originTarget: EntityId; /** 生成点逻辑 X */ originX: number; /** 生成点逻辑 Y */ originY: number; /** 各枚火球 */ missiles: HomingDragonMissile[]; } /** 直线火龙表现 cue */ export interface LineDragonCue { /** 施法者 */ caster: EntityId; /** 触发朝向目标 */ target: EntityId; /** 起点逻辑 X(施法者) */ originX: number; /** 起点逻辑 Y */ originY: number; /** 终点逻辑 X */ destX: number; /** 终点逻辑 Y */ destY: number; /** 飞行秒 */ duration: number; } /** 直线剑气表现 cue(玉帝刀光) */ export interface SwordWaveCue { /** 施法者 */ caster: EntityId; /** 触发朝向目标 */ target: EntityId; /** 起点逻辑 X */ originX: number; /** 起点逻辑 Y */ originY: number; /** 方向单位 X */ ux: number; /** 方向单位 Y */ uy: number; /** 飞行速度像素/秒 */ speed: number; /** Spine 动画:轩辕剑「剑气蓝」/ 吴钩双剑「剑气红」 */ animation: string; } /** 闪电链一跳表现 cue(电母-大招 / 电母-大招受击) */ export interface LightningChainCue { /** 施法者 */ caster: EntityId; /** 本跳起点实体(可已死) */ from: EntityId; /** 本跳落点实体 */ to: EntityId; /** 起点逻辑 X */ fromX: number; /** 起点逻辑 Y */ fromY: number; /** 落点逻辑 X */ toX: number; /** 落点逻辑 Y */ toY: number; /** 是否拉电弧(落点与起点重合则否) */ showBolt: boolean; } /** 单条被动绑定 */ export interface PassiveSkillBind { /** Skill.id */ skillId: number; /** condition 首段类型 */ conditionKind: number; /** condition 参数(不含类型) */ conditionParams: number[]; /** result 首段类型 */ resultKind: number; /** result 参数(不含类型) */ resultParams: number[]; /** 冷却剩余(秒);0=可触发 */ cdLeft: number; /** 配表冷却(秒) */ cd: number; /** 上次命中目标(condition 3) */ lastTarget: EntityId; /** 已首次命中过的目标(condition 4) */ hitTargets: EntityId[]; /** 已累计释放次数(condition 6) */ castCount: number; /** 攻速当前层数(result 2) */ stackCount: number; /** 攻速剩余持续(秒) */ stackLeft: number; /** Skill.atk_buff1 原文(空=无) */ atkBuff1: string; /** Skill.atk_buff2 原文(空=无) */ atkBuff2: string; } /** 毒 DoT */ interface PoisonDot { /** 施毒者 */ caster: EntityId; /** 中毒目标 */ target: EntityId; /** Skill.id */ skillId: number; /** 跳伤间隔(秒) */ interval: number; /** 伤害倍率(攻击力) */ scale: number; /** 剩余持续(秒) */ left: number; /** 距下次跳伤(秒) */ wait: number; } /** 剑阵持续伤害区 */ interface SwordArrayZone { /** 施法者 */ caster: EntityId; /** Skill.id */ skillId: number; /** 圆心逻辑 X */ x: number; /** 圆心逻辑 Y */ y: number; /** 半径像素 */ radius: number; /** 跳伤间隔(秒) */ interval: number; /** 伤害倍率 */ scale: number; /** 剩余持续(秒) */ left: number; /** 距下次跳伤(秒) */ wait: number; } /** 直线穿透火球/火龙在飞 */ interface LineDragonFlight { /** 施法者 */ caster: EntityId; /** Skill.id */ skillId: number; /** 当前逻辑 X */ x: number; /** 当前逻辑 Y */ y: number; /** 方向单位 X */ ux: number; /** 方向单位 Y */ uy: number; /** 飞行速度像素/秒 */ speed: number; /** 伤害倍率 */ scale: number; /** 剩余飞行(秒) */ left: number; /** 伤害类型(不回触发 condition) */ dmgType: string; /** 已命中实体(每目标一次) */ 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 { /** 施法者 */ caster: EntityId; /** 朝向参考目标 */ target: EntityId; /** Skill.id */ skillId: number; /** 起点逻辑 X */ originX: number; /** 起点逻辑 Y */ originY: number; /** 方向单位 X */ ux: number; /** 方向单位 Y */ uy: number; /** 伤害倍率 */ scale: number; /** 距放出(秒) */ wait: number; } /** 直线剑气在飞(穿出地图边界后回收) */ interface SwordWaveFlight { /** 施法者 */ caster: EntityId; /** Skill.id */ skillId: number; /** 当前逻辑 X */ x: number; /** 当前逻辑 Y */ y: number; /** 方向单位 X */ ux: number; /** 方向单位 Y */ uy: number; /** 飞行速度像素/秒 */ speed: number; /** 伤害倍率 */ scale: number; /** 已命中实体(每目标一次) */ hitIds: EntityId[]; } /** 进行中的闪电链 */ interface LightningChainRun { /** 施法者 */ caster: EntityId; /** Skill.id */ skillId: number; /** 伤害倍率 */ scale: number; /** 蔓延半径像素 */ range: number; /** 剩余可打目标数 */ remain: number; /** 距结算当前 pending(秒) */ wait: number; /** 待结算伤害目标(0=无) */ pending: EntityId; /** 当前搜索原点逻辑 X */ fromX: number; /** 当前搜索原点逻辑 Y */ fromY: number; /** 上一跳起点实体(表现用) */ fromId: EntityId; /** 已蔓延实体(不重复) */ visited: EntityId[]; } /** * 实体被动技能运行时(Skill.type=2)。 * 逻辑 result 在此执行;需表现的类型发 cue 或告警。 */ export class PassiveSkillHost { private readonly _binds: { entityId: EntityId; list: PassiveSkillBind[] }[] = []; private readonly _dots: PoisonDot[] = []; private readonly _zones: SwordArrayZone[] = []; private readonly _lineDragons: LineDragonFlight[] = []; 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; /** 普攻覆盖(装载服务注册) */ private _atkOverride: IPassiveAtkOverrideHost | null = null; /** * 注册/清除普攻覆盖器。 * @param host 装载服务;null 清除 */ bindAtkOverride(host: IPassiveAtkOverrideHost | null): void { this._atkOverride = host; } /** 清空全部绑定(开局 / 销毁) */ clear(): void { this._binds.length = 0; this._dots.length = 0; this._zones.length = 0; this._lineDragons.length = 0; this._wavePending.length = 0; this._waves.length = 0; this._chains.length = 0; this._iceCones.length = 0; this._session = null; this._atkOverride = null; } /** * 卸下实体被动并清属性/毒。 * @param entityId 塔实体 */ unbind(entityId: EntityId): void { this._stripMods(entityId); this._removeDotsByCaster(entityId); 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); } /** * 按武器被动装载;condition=2 在此触发一次。 * @param session 战斗会话 * @param entityId 塔实体 * @param loadout 三槽装载 */ bindFromLoadout(session: BattleSession, entityId: EntityId, loadout: PassiveLoadoutBind): void { this._session = session; this.unbind(entityId); const list: PassiveSkillBind[] = []; if (loadout.weaponSkillId > 0 && !loadout.weaponSkillActive) { const bind = this._parseSkill(loadout.weaponSkillId); if (bind) list.push(bind); } if (list.length === 0) return; this._binds.push({ entityId, list }); this._fireAlways(session, entityId, list); } /** * 查询实体已绑定被动。 * @param entityId 塔实体 */ getBinds(entityId: EntityId): readonly PassiveSkillBind[] { const idx = this._indexOf(entityId); return idx >= 0 ? this._binds[idx].list : []; } /** * 推进冷却、攻速层数、毒跳、剑阵与火球。 * @param dt 逻辑秒 */ tick(dt: number): void { for (let i = 0; i < this._binds.length; i++) { const entityId = this._binds[i].entityId; const list = this._binds[i].list; for (let j = 0; j < list.length; j++) { const b = list[j]; if (b.cdLeft > 0) b.cdLeft = Math.max(0, b.cdLeft - dt); if (b.stackLeft > 0) { b.stackLeft = Math.max(0, b.stackLeft - dt); if (b.stackLeft <= 0) { b.stackCount = 0; this._session?.attributes.removeModifiersByGroup(entityId, ASPD_GROUP + b.skillId); } } } } this._tickDots(dt); this._tickZones(dt); this._tickLineDragons(dt); this._tickSwordWavePending(dt); this._tickSwordWaves(dt); this._tickLightningChains(dt); this._tickIceCones(dt); } /** * 目标消失时清状态,避免实体 id 复用误触发。 * @param target 消失的实体 */ forgetTarget(target: EntityId): void { if (!(target > 0)) return; for (let i = 0; i < this._binds.length; i++) { const list = this._binds[i].list; for (let j = 0; j < list.length; j++) { const b = list[j]; if (b.lastTarget === target) b.lastTarget = 0; this._removeHit(b, target); } } for (let i = this._dots.length - 1; i >= 0; i--) { if (this._dots[i].target === target) this._dots.splice(i, 1); } } /** * 造成伤害后尝试 condition 1/4。 * @param session 战斗会话 * @param caster 伤害来源 * @param target 受击目标 * @param amount 本次伤害量 */ onDealDamage(session: BattleSession, caster: EntityId, target: EntityId, amount: number): boolean { if (this._resultGate > 0) return false; const idx = this._indexOf(caster); if (idx < 0) return false; const list = this._binds[idx].list; let fired = false; for (let i = 0; i < list.length; i++) { if (this._tryOnHit(session, caster, target, list[i], amount)) fired = true; } return fired; } /** * 击杀后尝试 condition 5。 * @param session 战斗会话 * @param caster 击杀者 * @param target 死者 */ onKill(session: BattleSession, caster: EntityId, target: EntityId): boolean { if (this._resultGate > 0) return false; const idx = this._indexOf(caster); if (idx < 0) return false; const list = this._binds[idx].list; let fired = false; for (let i = 0; i < list.length; i++) { const b = list[i]; if (b.conditionKind !== PassiveConditionKind.OnKill) continue; if (b.cdLeft > 0) continue; if (!this._rollWan(session, b.conditionParams[0], b.skillId)) continue; this._fire(session, caster, target, b, 0); fired = true; } return fired; } /** * 技能释放后尝试 condition 3/6(按主目标,一击一次)。 * @param session 战斗会话 * @param caster 施法者 * @param target 主目标(可 0) */ onAbilityCast(session: BattleSession, caster: EntityId, target: EntityId): boolean { const idx = this._indexOf(caster); if (idx < 0) return false; const list = this._binds[idx].list; let fired = false; for (let i = 0; i < list.length; i++) { const b = list[i]; if (b.conditionKind === PassiveConditionKind.SameTarget) { if (this._trySameTarget(session, caster, target, b)) fired = true; continue; } if (b.conditionKind !== PassiveConditionKind.SkillCastCount) continue; if (b.cdLeft > 0) continue; const need = b.conditionParams[0]; if (!(need > 0)) { console.log(`[PassiveSkill] condition 6 次数无效 skill=${b.skillId}`); continue; } b.castCount += 1; if (b.castCount < need) continue; b.castCount = 0; this._fire(session, caster, target, b, 0); fired = true; } return fired; } /** 部署时触发 condition 2 */ private _fireAlways(session: BattleSession, entityId: EntityId, list: PassiveSkillBind[]): void { for (let i = 0; i < list.length; i++) { const b = list[i]; if (b.conditionKind !== PassiveConditionKind.Always) continue; this._fire(session, entityId, 0, b, 0); } } /** 命中判定 condition 1/4 */ private _tryOnHit(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, amount: number): boolean { const kind = bind.conditionKind; if (kind === PassiveConditionKind.OnDealDamage) { if (bind.cdLeft > 0) return false; if (!this._rollWan(session, bind.conditionParams[0], bind.skillId)) return false; this._fire(session, caster, target, bind, amount); return true; } if (kind === PassiveConditionKind.FirstHitTarget) { if (!(target > 0) || this._hasHit(bind, target)) return false; this._markHit(bind, target); if (bind.cdLeft > 0) return false; this._fire(session, caster, target, bind, amount); return true; } return false; } /** * condition 3:连续出手主目标相同则叠层。 * @param session 战斗会话 * @param caster 施法者 * @param target 主目标 * @param bind 绑定 */ private _trySameTarget(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind): boolean { const same = bind.lastTarget === target && target > 0; if (target > 0) bind.lastTarget = target; if (!same || bind.cdLeft > 0) return false; this._fire(session, caster, target, bind, 0); return true; } /** 先进入冷却再执行 result;result 期间抬闸,其伤害/击杀不再判 condition */ private _fire(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, lastDamage: number): void { bind.cdLeft = bind.cd > 0 ? bind.cd : 0; this._beginResultGate(); try { this._applyResult(session, caster, target, bind, lastDamage); } finally { this._endResultGate(); } } /** result 结算开始 */ private _beginResultGate(): void { this._resultGate += 1; } /** result 结算结束 */ private _endResultGate(): void { this._resultGate = Math.max(0, this._resultGate - 1); } /** * result 产生的伤害。fromPassiveResult 跳过入口钩子,闸内不回触发 condition。 * @param session 战斗会话 * @param caster 伤害来源 * @param target 受击目标 * @param skillId 被动 Skill.id * @param scale 伤害倍率 * @param damageType `passive_` 前缀类型 */ private _dealResultDamage(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, scale: number, damageType: string): void { this._beginResultGate(); try { const ctx = createAbilityContext(session, caster, `passive_${skillId}`, [target]); session.damage.dealToTarget(caster, target, { scale, allowCrit: false, damageType, fromPassiveResult: true, }, ctx); session.events.flush(); } finally { this._endResultGate(); } } /** * 万分比掷骰;缺参则失败并告警。 * @param session 战斗会话 * @param wan 万分比 * @param skillId 用于告警 */ private _rollWan(session: BattleSession, wan: number, skillId: number): boolean { if (!(wan >= 0)) { console.log(`[PassiveSkill] 概率无效 skill=${skillId} wan=${wan}`); return false; } if (wan >= 10000) return true; return session.rng.next() * 10000 < wan; } /** 是否已首次命中过该目标 */ private _hasHit(bind: PassiveSkillBind, target: EntityId): boolean { const ids = bind.hitTargets; for (let i = 0; i < ids.length; i++) { if (ids[i] === target) return true; } return false; } /** 记录首次命中 */ private _markHit(bind: PassiveSkillBind, target: EntityId): void { bind.hitTargets.push(target); } /** 目标消失时移除首次命中记录 */ private _removeHit(bind: PassiveSkillBind, target: EntityId): void { const ids = bind.hitTargets; for (let i = ids.length - 1; i >= 0; i--) { if (ids[i] === target) ids.splice(i, 1); } } /** 按 result 类型执行逻辑效果 */ private _applyResult(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, lastDamage: number): void { const kind = bind.resultKind; const p = bind.resultParams; switch (kind) { case PassiveResultKind.Aspd: this._applyAspd(session, caster, bind, p); return; case PassiveResultKind.RangeMul: this._applyRangeMul(session, caster, bind.skillId, p); return; case PassiveResultKind.Knockback: this._applyKnockback(session, caster, target, lastDamage, p, bind.skillId); return; case PassiveResultKind.Peach: this._applyPeach(session, caster, p, bind.skillId); return; case PassiveResultKind.Stun: this._applyStun(session, caster, target, p, bind.skillId); return; case PassiveResultKind.Poison: this._applyPoison(caster, target, bind.skillId, p); return; case PassiveResultKind.SwordArray: this._applySwordArray(session, caster, target, bind.skillId, p); return; case PassiveResultKind.HomingDragon: this._applyHomingDragon(session, caster, target, bind.skillId, p); return; case PassiveResultKind.LineDragon: this._applyLineDragon(session, caster, target, bind.skillId, p); return; case PassiveResultKind.ModifyAttack: this._applyModifyAttack(session, caster, bind.skillId, p); return; case PassiveResultKind.SwordWave: this._applySwordWave(session, caster, target, bind.skillId, p); return; 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; default: console.log(`[PassiveSkill] 未知 result skill=${bind.skillId} kind=${kind}`); } } /** * 覆盖宿主普攻 atk_type / atk_para。 * result 参数:atk_type, atk_para... * @param session 战斗会话 * @param caster 触发宿主 * @param skillId 被动 Skill.id * @param p atk_type 与 atk_para 各段 */ private _applyModifyAttack(session: BattleSession, caster: EntityId, skillId: number, p: number[]): void { const atkType = Math.floor(p[0] || 0); if (!(atkType > 0)) { console.log(`[PassiveSkill] result 10 缺 atk_type skill=${skillId}`, p.join(',')); return; } if (p.length < 2) { console.log(`[PassiveSkill] result 10 缺 atk_para skill=${skillId}`, p.join(',')); return; } if (!this._atkOverride) { console.log(`[PassiveSkill] result 10 无装载覆盖器 skill=${skillId}`); return; } this._atkOverride.applyNormalAtkOverride(session, caster, String(atkType), p.slice(1).join(','), skillId); } /** * 攻速:层数叠加,每触发刷新持续。 * @param session 战斗会话 * @param caster 施法者 * @param bind 绑定 * @param p 加成万分比, 最大层数, 持续秒 */ private _applyAspd(session: BattleSession, caster: EntityId, bind: PassiveSkillBind, p: number[]): void { const bonusWan = p[0]; const maxStack = p[1]; const dur = p[2]; if (!(bonusWan > 0) || !(maxStack > 0) || !(dur > 0)) { console.log(`[PassiveSkill] result 2 参数无效 skill=${bind.skillId}`, p.join(',')); return; } this._ensureAttr(session, caster); bind.stackCount = Math.min(Math.floor(maxStack), bind.stackCount + 1); bind.stackLeft = dur; session.attributes.addModifier(caster, { attr: 'aspd_add', op: 'Add', value: bonusWan * bind.stackCount, source: ASPD_GROUP + bind.skillId, group: ASPD_GROUP + bind.skillId, }); } /** * 射程乘倍(永久,同 skill 覆盖)。 * @param session 战斗会话 * @param caster 施法者 * @param skillId Skill.id * @param p 倍数 */ private _applyRangeMul(session: BattleSession, caster: EntityId, skillId: number, p: number[]): void { const mul = p[0]; if (!(mul > 0)) { console.log(`[PassiveSkill] result 3 倍数无效 skill=${skillId} mul=${mul}`); return; } this._ensureAttr(session, caster); session.attributes.addModifier(caster, { attr: AttrId.Range, op: 'Mul', value: mul, source: RANGE_GROUP + skillId, group: RANGE_GROUP + skillId, }); } /** * 击退当前目标。 * @param session 战斗会话 * @param caster 施法者 * @param target 目标 * @param lastDamage 本次伤害 * @param p 距离像素 * @param skillId Skill.id */ private _applyKnockback(session: BattleSession, caster: EntityId, target: EntityId, lastDamage: number, p: number[], skillId: number): void { const pixels = p[0]; if (!(pixels > 0)) { console.log(`[PassiveSkill] result 4 距离无效 skill=${skillId} px=${pixels}`); return; } if (!(target > 0) || !session.world.isAlive(target)) return; if (!(lastDamage > 0)) { console.log(`[PassiveSkill] result 4 无伤害 skill=${skillId}`); return; } const ctx = createAbilityContext(session, caster, `passive_${skillId}`, [target]); ctx.vars.set(VAR_LAST_DAMAGE_AMOUNT, lastDamage); applySkillTableKnockback(session, ctx, target, pixels); } /** * 给施法者玩家加桃子。 * @param session 战斗会话 * @param caster 施法者 * @param p 数量 * @param skillId Skill.id */ private _applyPeach(session: BattleSession, caster: EntityId, p: number[], skillId: number): void { const n = Math.floor(p[0]); if (!(n > 0)) { console.log(`[PassiveSkill] result 5 数量无效 skill=${skillId} n=${p[0]}`); return; } const team = session.world.getComponent(caster, TeamComp); applySkillAtkBuffBoardEffect(session, { kind: 5, ownerPlayerId: team?.ownerId ?? team?.team ?? 0, peachCount: n, target: caster, }); } /** * 眩晕目标(debuff_4)。 * @param session 战斗会话 * @param caster 施法者 * @param target 目标 * @param p 时长秒 * @param skillId Skill.id */ private _applyStun(session: BattleSession, caster: EntityId, target: EntityId, p: number[], skillId: number): void { const dur = p[0]; if (!(dur > 0)) { console.log(`[PassiveSkill] result 6 时长无效 skill=${skillId} dur=${dur}`); return; } if (!(target > 0) || !session.world.isAlive(target)) return; if (!session.config.getBuff('debuff_4')) { console.log(`[PassiveSkill] 缺 buff debuff_4 skill=${skillId}`); return; } session.buffs.add(target, 'debuff_4', caster, 1, { duration: dur }); } /** * 上毒:立即首跳,再按间隔跳至结束。 * @param caster 施法者 * @param target 目标 * @param skillId Skill.id * @param p 间隔秒, 系数万分比, 持续秒 */ private _applyPoison(caster: EntityId, target: EntityId, skillId: number, p: number[]): void { const interval = p[0]; const coeffWan = p[1]; const dur = p[2]; if (!(interval > 0) || !(coeffWan > 0) || !(dur > 0)) { console.log(`[PassiveSkill] result 8 参数无效 skill=${skillId}`, p.join(',')); return; } if (!(target > 0)) return; const scale = coeffWan / 10000; for (let i = 0; i < this._dots.length; i++) { const d = this._dots[i]; if (d.caster === caster && d.target === target && d.skillId === skillId) { d.interval = interval; d.scale = scale; d.left = dur; d.wait = 0; return; } } this._dots.push({ caster, target, skillId, interval, scale, left: dur, wait: 0 }); } /** * 剑阵:落点半径内周期伤害,并发表现 cue。同塔同技能只留一团,再次触发刷新。 * @param session 战斗会话 * @param caster 施法者 * @param target 落点目标 * @param skillId Skill.id * @param p 半径像素, 间隔秒, 系数万分比, 持续秒 */ private _applySwordArray(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void { const radius = p[0]; const interval = p[1]; const coeffWan = p[2]; const dur = p[3]; if (!(radius > 0) || !(interval > 0) || !(coeffWan > 0) || !(dur > 0)) { console.log(`[PassiveSkill] result 1 参数无效 skill=${skillId}`, p.join(',')); return; } const pos = this._logicPos(session, target > 0 ? target : caster); if (!pos) { console.log(`[PassiveSkill] result 1 无落点 skill=${skillId}`); return; } const scale = coeffWan / 10000; const existed = this._findZone(caster, skillId); if (existed) { existed.x = pos.x; existed.y = pos.y; existed.radius = radius; existed.interval = interval; existed.scale = scale; existed.left = dur; existed.wait = 0; } else { this._zones.push({ caster, skillId, x: pos.x, y: pos.y, radius, interval, scale, left: dur, wait: 0, }); } const cue: SwordArrayCue = { caster, target, duration: dur, radius }; session.events.emit(BattleEventFactory.custom(SWORD_ARRAY_EVENT, cue)); } /** * 爆炸火球:击杀点按 360/数量 均分方向直线穿透,飞满配表时间后结束。 * @param session 战斗会话 * @param caster 施法者 * @param originTarget 触发落点实体 * @param skillId Skill.id * @param p 最大数量, 系数万分比, 飞行速度像素/秒, 飞行时间秒 */ private _applyHomingDragon(session: BattleSession, caster: EntityId, originTarget: EntityId, skillId: number, p: number[]): void { const maxCount = Math.floor(p[0]); const coeffWan = p[1]; const speed = p[2]; const dur = p[3]; if (!(maxCount > 0) || !(coeffWan > 0) || !(speed > 0) || !(dur > 0)) { console.log(`[PassiveSkill] result 7 参数无效 skill=${skillId}`, p.join(',')); return; } const origin = this._logicPos(session, originTarget > 0 ? originTarget : caster) ?? this._logicPos(session, caster); if (!origin) { console.log(`[PassiveSkill] result 7 无落点 skill=${skillId}`); return; } const scale = coeffWan / 10000; const travel = speed * dur; const stepRad = (Math.PI * 2) / maxCount; const missiles: HomingDragonMissile[] = []; for (let i = 0; i < maxCount; i++) { const rad = i * stepRad; const ux = Math.cos(rad); const uy = Math.sin(rad); const flight: LineDragonFlight = { caster, skillId, x: origin.x, y: origin.y, ux, uy, speed, scale, left: dur, dmgType: HOMING_DRAGON_DMG, hitIds: [], }; this._lineDragons.push(flight); this._hitLineDragon(session, flight, 0); missiles.push({ ux, uy, destX: origin.x + ux * travel, destY: origin.y + uy * travel, duration: dur, }); } const cue: HomingDragonCue = { caster, originTarget, originX: origin.x, originY: origin.y, missiles, }; session.events.emit(BattleEventFactory.custom(HOMING_DRAGON_EVENT, cue)); } /** * 直线火龙:从施法者沿触发目标方向匀速飞 duration 秒,穿透且每敌一次。 * @param session 战斗会话 * @param caster 施法者 * @param target 触发朝向目标 * @param skillId Skill.id * @param p 飞行速度像素/秒, 系数万分比, 持续秒 */ private _applyLineDragon(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void { const speed = p[0]; const coeffWan = p[1]; const dur = p[2]; if (!(speed > 0) || !(coeffWan > 0) || !(dur > 0)) { console.log(`[PassiveSkill] result 9 参数无效 skill=${skillId}`, p.join(',')); return; } const origin = this._logicPos(session, caster); if (!origin) { console.log(`[PassiveSkill] result 9 无起点 skill=${skillId}`); return; } const aim = this._logicPos(session, target); if (!aim) { console.log(`[PassiveSkill] result 9 无朝向 skill=${skillId} target=${target}`); return; } const dx = aim.x - origin.x; const dy = aim.y - origin.y; const len = Math.sqrt(dx * dx + dy * dy); if (!(len > 1e-4)) { console.log(`[PassiveSkill] result 9 朝向为零 skill=${skillId}`); return; } const ux = dx / len; const uy = dy / len; const travel = speed * dur; const flight: LineDragonFlight = { caster, skillId, x: origin.x, y: origin.y, ux, uy, speed, scale: coeffWan / 10000, left: dur, dmgType: LINE_DRAGON_DMG, hitIds: [], }; this._lineDragons.push(flight); this._hitLineDragon(session, flight, 0); const cue: LineDragonCue = { caster, target, originX: origin.x, originY: origin.y, destX: origin.x + ux * travel, destY: origin.y + uy * travel, duration: dur, }; session.events.emit(BattleEventFactory.custom(LINE_DRAGON_EVENT, cue)); } /** * 玉帝刀光动画:轩辕剑蓝、吴钩双剑红。 * @param skillId 被动 Skill.id */ private _swordWaveAnim(skillId: number): string { const row = gg.data.table.getTableData(TableNames.Skill, skillId); const name = String(row?.name ?? '').trim(); if (name === '吴钩双剑') return SWORD_WAVE_ANIM_RED; if (name === '轩辕剑') return SWORD_WAVE_ANIM_BLUE; console.log(`[PassiveSkill] result 11 未配置剑气色 skill=${skillId} name=${name}`); return SWORD_WAVE_ANIM_BLUE; } /** * 直线剑气:按间隔连发 count 道,优先各朝不同敌人;敌数不足则循环复用。 * @param session 战斗会话 * @param caster 施法者 * @param target 触发主目标(优先作为第一道朝向) * @param skillId Skill.id * @param p 数量, 间隔秒, 系数万分比 */ private _applySwordWave(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void { const count = Math.floor(p[0]); const interval = p[1]; const coeffWan = p[2]; if (!(count > 0) || !(interval >= 0) || !(coeffWan > 0)) { console.log(`[PassiveSkill] result 11 参数无效 skill=${skillId}`, p.join(',')); return; } const origin = this._logicPos(session, caster); if (!origin) { console.log(`[PassiveSkill] result 11 无起点 skill=${skillId}`); return; } const picks: EntityId[] = []; this._collectHostileIds(session, caster, picks); if (picks.length === 0) { console.log(`[PassiveSkill] result 11 无敌可朝向 skill=${skillId}`); return; } // 触发主目标优先,其余打乱;敌数不足时按序循环复用 const ordered: EntityId[] = []; if (target > 0) { for (let i = 0; i < picks.length; i++) { if (picks[i] === target) { ordered.push(target); picks[i] = picks[picks.length - 1]; picks.length -= 1; break; } } } this._shuffleIds(session, picks); for (let i = 0; i < picks.length; i++) ordered.push(picks[i]); const scale = coeffWan / 10000; let spawned = 0; for (let i = 0; i < count; i++) { let ux = 0; let uy = 0; let tid: EntityId = 0; let ok = false; for (let k = 0; k < ordered.length; k++) { tid = ordered[(i + k) % ordered.length]; const aim = this._logicPos(session, tid); if (!aim) continue; const dx = aim.x - origin.x; const dy = aim.y - origin.y; const len = Math.sqrt(dx * dx + dy * dy); if (!(len > 1e-4)) continue; ux = dx / len; uy = dy / len; ok = true; break; } if (!ok) break; this._wavePending.push({ caster, target: tid, skillId, originX: origin.x, originY: origin.y, ux, uy, scale, wait: spawned * interval, }); spawned += 1; } if (spawned <= 0) { console.log(`[PassiveSkill] result 11 无有效朝向 skill=${skillId}`); return; } this._tickSwordWavePending(0); } /** * 收集可被火龙索敌的敌对存活单位。 * @param session 战斗会话 * @param caster 施法者 * @param out 输出 id */ private _collectHostileIds(session: BattleSession, caster: EntityId, out: EntityId[]): void { const casterTeam = session.world.getComponent(caster, TeamComp); session.world.view(HealthComp).forEach((id, hp) => { if (id === caster) return; if (!session.world.isAlive(id)) return; if (!(hp.hp > 0)) return; const move = session.world.getComponent(id, MovementComp); if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return; if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) return; out.push(id); }); } /** * 原地打乱 id(session.rng)。 * @param session 战斗会话 * @param ids 列表 */ private _shuffleIds(session: BattleSession, ids: EntityId[]): void { for (let i = ids.length - 1; i > 0; i--) { const j = Math.floor(session.rng.next() * (i + 1)); const tmp = ids[i]; ids[i] = ids[j]; ids[j] = tmp; } } /** 推进直线火球/火龙位移与穿透命中 */ private _tickLineDragons(dt: number): void { const session = this._session; if (!session) return; for (let i = this._lineDragons.length - 1; i >= 0; i--) { const d = this._lineDragons[i]; const step = d.speed * dt; d.x += d.ux * step; d.y += d.uy * step; d.left -= dt; this._hitLineDragon(session, d, step * 0.5); if (d.left <= 0) this._lineDragons.splice(i, 1); } } /** 推进待放出剑气 */ private _tickSwordWavePending(dt: number): void { const session = this._session; if (!session) return; for (let i = this._wavePending.length - 1; i >= 0; i--) { const p = this._wavePending[i]; p.wait -= dt; if (p.wait > 0) continue; this._wavePending.splice(i, 1); this._spawnSwordWave(session, p); } } /** * 放出一道剑气并通知表现。 * @param session 战斗会话 * @param p 待放参数 */ private _spawnSwordWave(session: BattleSession, p: SwordWavePending): void { if (!session.world.isAlive(p.caster)) return; const flight: SwordWaveFlight = { caster: p.caster, skillId: p.skillId, x: p.originX, y: p.originY, ux: p.ux, uy: p.uy, speed: SWORD_WAVE_SPEED, scale: p.scale, hitIds: [], }; this._waves.push(flight); this._hitSwordWave(session, flight); const cue: SwordWaveCue = { caster: p.caster, target: p.target, originX: p.originX, originY: p.originY, ux: p.ux, uy: p.uy, speed: SWORD_WAVE_SPEED, animation: this._swordWaveAnim(p.skillId), }; session.events.emit(BattleEventFactory.custom(SWORD_WAVE_EVENT, cue)); } /** 推进剑气位移、穿透命中与出界回收 */ private _tickSwordWaves(dt: number): void { const session = this._session; if (!session) return; for (let i = this._waves.length - 1; i >= 0; i--) { const w = this._waves[i]; const step = w.speed * dt; w.x += w.ux * step; w.y += w.uy * step; this._hitSwordWave(session, w); if (this._isSwordWaveOut(session, w)) this._waves.splice(i, 1); } } /** * 剑气是否已飞出地图外扩边界。 * @param session 战斗会话 * @param w 在飞剑气 */ private _isSwordWaveOut(session: BattleSession, w: SwordWaveFlight): boolean { const grid = session.grid; const maxX = grid.cols * grid.cellSize; const maxY = grid.rows * grid.cellSize; const pad = SWORD_WAVE_BOUND_PAD; return w.x < -pad || w.y < -pad || w.x > maxX + pad || w.y > maxY + pad; } /** * 剑气当前体盒内敌方各伤一次。 * @param session 战斗会话 * @param w 在飞剑气 */ private _hitSwordWave(session: BattleSession, w: SwordWaveFlight): void { if (!session.world.isAlive(w.caster)) return; const casterTeam = session.world.getComponent(w.caster, TeamComp); const cell = session.grid.worldToCell(w.x, w.y); const cellSize = Math.max(1, session.grid.cellSize); const radiusCells = Math.max(SWORD_WAVE_HALF_LEN, SWORD_WAVE_HALF_WID) / cellSize + 2; const ids = session.spatial.queryRadius(cell, radiusCells); session.events.flush(); this._beginResultGate(); try { for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (id === w.caster) continue; if (this._swordWaveHit(w, id)) 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 rx = pos.x - w.x; const ry = pos.y - w.y; const lx = rx * w.ux + ry * w.uy; const ly = rx * w.uy - ry * w.ux; if (Math.abs(lx) > SWORD_WAVE_HALF_LEN || Math.abs(ly) > SWORD_WAVE_HALF_WID) continue; w.hitIds.push(id); this._dealResultDamage(session, w.caster, id, w.skillId, w.scale, SWORD_WAVE_DMG); } session.events.flush(); } finally { this._endResultGate(); } } /** * 该剑气是否已命中过该实体。 * @param w 在飞剑气 * @param id 实体 */ private _swordWaveHit(w: SwordWaveFlight, id: EntityId): boolean { const ids = w.hitIds; for (let i = 0; i < ids.length; i++) { if (ids[i] === id) return true; } 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 战斗会话 * @param caster 施法者 * @param target 触发落点 * @param bind 被动绑定 * @param p 半径像素, 伤害万分比 */ private _applyExplosion(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, p: number[]): void { const radius = p[0]; const coeffWan = p[1]; if (!(radius > 0) || !(coeffWan > 0)) { console.log(`[PassiveSkill] result 14 参数无效 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 14 无落点 skill=${bind.skillId}`); return; } const scale = coeffWan / 10000; 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 hitIds: EntityId[] = []; this._beginResultGate(); try { for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (id === caster) 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; if (dx * dx + dy * dy > r2) continue; this._dealResultDamage(session, caster, id, bind.skillId, scale, EXPLOSION_DMG); hitIds.push(id); } session.events.flush(); } finally { this._endResultGate(); } const buff1 = String(bind.atkBuff1 ?? '').trim(); const buff2 = String(bind.atkBuff2 ?? '').trim(); if (!buff1 && !buff2) return; const abilityCtx = createAbilityContext(session, caster, `passive_${bind.skillId}`, hitIds); for (let i = 0; i < hitIds.length; i++) { const id = hitIds[i]; if (!session.world.isAlive(id)) continue; SkillAtkBuffExecutor.applyPair(session, abilityCtx, id, buff1, buff2, { fromPassiveResult: true }); } } /** * 从触发目标起跳,向范围内未打过的最近敌蔓延 hop 次。 * @param session 战斗会话 * @param caster 施法者 * @param target 触发目标 * @param skillId Skill.id * @param p 蔓延范围像素, 蔓延数量, 系数万分比 */ private _applyLightningChain(session: BattleSession, caster: EntityId, target: EntityId, skillId: number, p: number[]): void { const range = p[0]; const count = Math.floor(p[1]); const coeffWan = p[2]; if (!(range > 0) || !(count > 0) || !(coeffWan > 0)) { console.log(`[PassiveSkill] result 12 参数无效 skill=${skillId}`, p.join(',')); return; } const origin = this._logicPos(session, target); if (!origin) { console.log(`[PassiveSkill] result 12 无起点 skill=${skillId} target=${target}`); return; } this._chains.push({ caster, skillId, scale: coeffWan / 10000, range, remain: count, wait: 0, pending: 0, fromX: origin.x, fromY: origin.y, fromId: target, visited: [], }); this._tickLightningChains(0); } /** 推进闪电链 hop:先结算上一跳伤害,再选下一目标。 */ private _tickLightningChains(dt: number): void { const session = this._session; if (!session) return; for (let i = this._chains.length - 1; i >= 0; i--) { const run = this._chains[i]; run.wait -= dt; let alive = true; while (alive && run.wait <= 0) { alive = this._stepLightningHop(session, run); } if (!alive) this._chains.splice(i, 1); } } /** * 结算 pending 并尝试放出下一跳。 * @param session 战斗会话 * @param run 进行中的链 * @returns 链是否继续 */ private _stepLightningHop(session: BattleSession, run: LightningChainRun): boolean { if (run.pending > 0) { this._dealLightningHop(session, run, run.pending); run.pending = 0; } if (run.remain <= 0) return false; if (!session.world.isAlive(run.caster)) return false; const next = this._pickLightningHop(session, run); if (next === 0) return false; const aim = this._logicPos(session, next); if (!aim) return false; run.visited.push(next); run.remain -= 1; const dx = aim.x - run.fromX; const dy = aim.y - run.fromY; const dist = Math.sqrt(dx * dx + dy * dy); const showBolt = dist > 1; const cue: LightningChainCue = { caster: run.caster, from: run.fromId, to: next, fromX: run.fromX, fromY: run.fromY, toX: aim.x, toY: aim.y, showBolt, }; session.events.emit(BattleEventFactory.custom(LIGHTNING_CHAIN_EVENT, cue)); run.pending = next; run.fromId = next; run.fromX = aim.x; run.fromY = aim.y; run.wait = showBolt ? LIGHTNING_HOP_SEC : 0; return true; } /** * 范围内未访问的最近敌;没有则结束(不重复打已蔓延目标)。 * @param session 战斗会话 * @param run 进行中的链 */ private _pickLightningHop(session: BattleSession, run: LightningChainRun): EntityId { const ids: EntityId[] = []; this._collectHostileIds(session, run.caster, ids); const r2 = run.range * run.range; let best: EntityId = 0; let bestD2 = r2; for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (this._chainVisited(run, id)) continue; const pos = this._logicPos(session, id); if (!pos) continue; const dx = pos.x - run.fromX; const dy = pos.y - run.fromY; const d2 = dx * dx + dy * dy; if (d2 > r2) continue; if (best === 0 || d2 < bestD2) { best = id; bestD2 = d2; } } return best; } /** * 该链是否已蔓延过该实体。 * @param run 进行中的链 * @param id 实体 */ private _chainVisited(run: LightningChainRun, id: EntityId): boolean { const ids = run.visited; for (let i = 0; i < ids.length; i++) { if (ids[i] === id) return true; } return false; } /** * 对一跳目标结算伤害。 * @param session 战斗会话 * @param run 进行中的链 * @param target 落点 */ private _dealLightningHop(session: BattleSession, run: LightningChainRun, target: EntityId): void { if (!session.world.isAlive(run.caster) || !session.world.isAlive(target)) return; const hp = session.world.getComponent(target, HealthComp); if (!hp || !(hp.hp > 0)) return; this._dealResultDamage(session, run.caster, target, run.skillId, run.scale, LIGHTNING_CHAIN_DMG); } /** * 直线火龙当前体盒内敌方各伤一次(穿透,每敌一次)。 * 占地按半格放大,与 atk_type 12 散射箭一致,避免只打点心漏掉同线怪。 * @param session 战斗会话 * @param d 在飞火龙 * @param extraLen 本帧步进半长,防穿透漏检 */ private _hitLineDragon(session: BattleSession, d: LineDragonFlight, extraLen: number): void { if (!session.world.isAlive(d.caster)) return; const casterTeam = session.world.getComponent(d.caster, TeamComp); const cell = session.grid.worldToCell(d.x, d.y); const cellSize = Math.max(1, session.grid.cellSize); const halfBody = cellSize * 0.5; const radiusCells = (LINE_DRAGON_HALF_LEN + extraLen + halfBody) / cellSize + 2; const ids = session.spatial.queryRadius(cell, radiusCells); session.events.flush(); this._beginResultGate(); try { for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (id === d.caster) continue; if (this._lineDragonHit(d, id)) 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 rx = pos.x - d.x; const ry = pos.y - d.y; const lx = rx * d.ux + ry * d.uy; const ly = rx * d.uy - ry * d.ux; if (Math.abs(lx) > LINE_DRAGON_HALF_LEN + extraLen + halfBody) continue; if (Math.abs(ly) > LINE_DRAGON_HALF_WID + halfBody) continue; d.hitIds.push(id); this._dealResultDamage(session, d.caster, id, d.skillId, d.scale, d.dmgType); } session.events.flush(); } finally { this._endResultGate(); } } /** * 该火龙是否已命中过该实体。 * @param d 在飞火龙 * @param id 实体 */ private _lineDragonHit(d: LineDragonFlight, id: EntityId): boolean { const ids = d.hitIds; for (let i = 0; i < ids.length; i++) { if (ids[i] === id) return true; } return false; } /** 推进毒跳 */ private _tickDots(dt: number): void { const session = this._session; if (!session) return; for (let i = this._dots.length - 1; i >= 0; i--) { const d = this._dots[i]; if (!session.world.isAlive(d.caster) || !session.world.isAlive(d.target)) { this._dots.splice(i, 1); continue; } d.left -= dt; d.wait -= dt; if (d.wait <= 0) { this._dealResultDamage(session, d.caster, d.target, d.skillId, d.scale, POISON_DMG); d.wait += d.interval; } if (d.left <= 0) this._removeDot(d); } } /** 推进剑阵跳伤 */ private _tickZones(dt: number): void { const session = this._session; if (!session) return; for (let i = this._zones.length - 1; i >= 0; i--) { const z = this._zones[i]; if (!session.world.isAlive(z.caster)) { this._zones.splice(i, 1); continue; } z.left -= dt; z.wait -= dt; if (z.wait <= 0) { this._pulseZone(session, z); z.wait += z.interval; } if (z.left <= 0) this._removeZone(z); } } /** * 剑阵一跳:半径内敌方造成伤害。 * @param session 战斗会话 * @param zone 剑阵区 */ private _pulseZone(session: BattleSession, zone: SwordArrayZone): void { session.events.flush(); const casterTeam = session.world.getComponent(zone.caster, TeamComp); const cell = session.grid.worldToCell(zone.x, zone.y); const radiusCells = zone.radius / Math.max(1, session.grid.cellSize); const ids = session.spatial.queryRadius(cell, radiusCells + 1); const r2 = zone.radius * zone.radius; this._beginResultGate(); try { for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (id === zone.caster) 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 - zone.x; const dy = pos.y - zone.y; if (dx * dx + dy * dy > r2) continue; this._dealResultDamage(session, zone.caster, id, zone.skillId, zone.scale, SWORD_ARRAY_DMG); } session.events.flush(); } finally { this._endResultGate(); } } /** * 实体逻辑世界坐标。 * @param session 战斗会话 * @param entity 实体 */ private _logicPos(session: BattleSession, entity: EntityId): { x: number; y: number } | null { const move = session.world.getComponent(entity, MovementComp); if (move && (move.worldX !== 0 || move.worldY !== 0)) { return { x: move.worldX, y: move.worldY }; } const tf = session.world.getComponent(entity, TransformComp); if (!tf) return null; return session.grid.cellToWorld({ row: tf.row, col: tf.col }); } /** * 按引用移除剑阵区。 * @param zone 剑阵区 */ private _removeZone(zone: SwordArrayZone): void { for (let i = this._zones.length - 1; i >= 0; i--) { if (this._zones[i] === zone) { this._zones.splice(i, 1); return; } } } /** * 按引用移除毒跳(伤害回调可能已 splice,避免错删)。 * @param dot 毒跳 */ private _removeDot(dot: PoisonDot): void { for (let i = this._dots.length - 1; i >= 0; i--) { if (this._dots[i] === dot) { this._dots.splice(i, 1); return; } } } /** 卸下时清攻速/射程 modifier */ private _stripMods(entityId: EntityId): void { const session = this._session; if (!session || !session.world.isAlive(entityId)) return; const idx = this._indexOf(entityId); if (idx < 0) return; const list = this._binds[idx].list; for (let i = 0; i < list.length; i++) { const id = list[i].skillId; session.attributes.removeModifiersByGroup(entityId, ASPD_GROUP + id); session.attributes.removeModifiersByGroup(entityId, RANGE_GROUP + id); } } /** 卸下施法者时清其毒 */ private _removeDotsByCaster(caster: EntityId): void { for (let i = this._dots.length - 1; i >= 0; i--) { if (this._dots[i].caster === caster) this._dots.splice(i, 1); } } /** * 同施法者同技能的已有剑阵。 * @param caster 施法者 * @param skillId Skill.id */ private _findZone(caster: EntityId, skillId: number): SwordArrayZone | null { for (let i = 0; i < this._zones.length; i++) { const z = this._zones[i]; if (z.caster === caster && z.skillId === skillId) return z; } return null; } /** 卸下施法者时清其剑阵 */ private _removeZonesByCaster(caster: EntityId): void { for (let i = this._zones.length - 1; i >= 0; i--) { if (this._zones[i].caster === caster) this._zones.splice(i, 1); } } /** 卸下施法者时清其剑气 */ private _removeWavesByCaster(caster: EntityId): void { for (let i = this._wavePending.length - 1; i >= 0; i--) { if (this._wavePending[i].caster === caster) this._wavePending.splice(i, 1); } for (let i = this._waves.length - 1; i >= 0; i--) { if (this._waves[i].caster === caster) this._waves.splice(i, 1); } } /** 卸下施法者时清其闪电链 */ private _removeChainsByCaster(caster: EntityId): void { for (let i = this._chains.length - 1; i >= 0; i--) { if (this._chains[i].caster === caster) this._chains.splice(i, 1); } } /** * 确保 AttributeComp 存在。 * @param session 战斗会话 * @param entity 实体 */ private _ensureAttr(session: BattleSession, entity: EntityId): void { if (!session.world.getComponent(entity, AttributeComp)) { session.world.addComponent(entity, new AttributeComp()); } session.attributes.syncCombatToAttributes(entity); } /** * 解析 Skill.condition / result。 * @param skillId Skill.id */ private _parseSkill(skillId: number): PassiveSkillBind | null { const row = gg.data.table.getTableData(TableNames.Skill, skillId); if (!row) { console.log(`[PassiveSkill] 缺表 Skill.id=${skillId}`); return null; } if (Number(row.type) !== 2) return null; const cond = String(row.condition ?? '').trim(); const result = String(row.result ?? '').trim(); if (!cond || !result) { console.log(`[PassiveSkill] condition/result 为空 Skill.id=${skillId}`); return null; } const condParts = cond.split(',').map(Number); const resultParts = result.split(',').map(Number); const conditionKind = condParts[0] || 0; if (conditionKind < 1 || conditionKind > 6) { console.log(`[PassiveSkill] 未知 condition skill=${skillId} kind=${conditionKind}`); } return { skillId, conditionKind, conditionParams: condParts.slice(1), resultKind: resultParts[0] || 0, resultParams: resultParts.slice(1), cdLeft: 0, cd: Number(row.cd) || 0, lastTarget: 0, hitTargets: [], castCount: 0, stackCount: 0, stackLeft: 0, atkBuff1: String(row.atk_buff1 ?? ''), atkBuff2: String(row.atk_buff2 ?? ''), }; } /** * 查找实体绑定下标。 * @param entityId 塔实体 */ private _indexOf(entityId: EntityId): number { for (let i = 0; i < this._binds.length; i++) { if (this._binds[i].entityId === entityId) return i; } return -1; } } let _host: PassiveSkillHost | null = null; /** 全局被动宿主 */ export function getPassiveSkillHost(): PassiveSkillHost { if (!_host) _host = new PassiveSkillHost(); return _host; } /** 测试用重置 */ export function resetPassiveSkillHostForTests(): void { _host = null; }