export const WOW335_WARLOCK_SOUL_LINK_TAG = "wow335-warlock-soul-link"; const WOW335_SOUL_LINKABLE_DEMON_SPELLS = new Set([ "wow335-warlock-summon-voidwalker", "wow335-warlock-summon-succubus", "wow335-warlock-summon-felhunter", "wow335-warlock-summon-felguard", "wow335-warlock-inferno", ]); export interface WarlockSoulLinkSummon { readonly sourceAbilityId: string; readonly ownerActorId: string; readonly health: number; readonly expiresAt: number; readonly controlledUntil: number; readonly position: readonly [number, number, number]; } /** Soul Link is 6%, plus Demonic Embrace's local rank tooltip of 2/4/6%. */ export function warlockSoulLinkSharePercent( soulLinkRank: number, demonicEmbraceRank: number, ): number { const linkRank = Math.max(0, Math.min(1, Math.trunc(Number.isFinite(soulLinkRank) ? soulLinkRank : 0))); if (linkRank === 0) return 0; const embraceRank = Math.max(0, Math.min(3, Math.trunc(Number.isFinite(demonicEmbraceRank) ? demonicEmbraceRank : 0))); return 6 + 2 * embraceRank; } /** * Selects the caster's live, controlled Warlock demon within the generated * talent tooltip's 30-yard link distance. The simulator represents yards as * ten combat-position units. */ export function warlockSoulLinkDemon( summons: readonly T[], ownerActorId: string, ownerPosition: readonly [number, number, number], now: number, ): T | null { return summons.find((summon) => ( WOW335_SOUL_LINKABLE_DEMON_SPELLS.has(summon.sourceAbilityId) && summon.ownerActorId === ownerActorId && summon.health > 0 && summon.expiresAt > now && summon.controlledUntil <= now && Math.hypot( summon.position[0] - ownerPosition[0], summon.position[1] - ownerPosition[1], summon.position[2] - ownerPosition[2], ) <= 300 )) ?? null; } /** Split post-mitigation incoming damage; the transferred portion bypasses prevention. */ export function warlockSoulLinkDamageSplit(amount: number, sharePercent: number): { readonly casterDamage: number; readonly demonDamage: number; } { const safeAmount = Math.max(0, Math.trunc(Number.isFinite(amount) ? amount : 0)); const safePercent = Math.max(0, Math.min(100, Number.isFinite(sharePercent) ? sharePercent : 0)); const demonDamage = Math.min(safeAmount, Math.round(safeAmount * safePercent / 100)); return { casterDamage: safeAmount - demonDamage, demonDamage }; }