export const WOW335_ROGUE_KILLING_SPREE_DURATION_MS = 2_000; export const WOW335_ROGUE_KILLING_SPREE_INTERVAL_MS = 500; export const WOW335_ROGUE_KILLING_SPREE_ASSAULTS = 5; export interface Wow335KillingSpreeWeaponSnapshot { readonly hand: "main-hand" | "off-hand"; readonly weaponType?: string; readonly damageMin: number; readonly damageMax: number; readonly speedMs: number; } export interface Wow335KillingSpreeHit { readonly hand: "main-hand" | "off-hand"; readonly damage: number; readonly speedMs: number; readonly dbcSpellId: 57841 | 57842; } export interface Wow335KillingSpreeWindow { readonly ownerActorId: string; readonly abilityId: string; readonly role: import("./partyRoles").PartyRole; readonly startedAt: number; readonly endsAt: number; readonly nextAssaultAt: number; readonly completedAssaults: number; readonly targetIds: readonly string[]; readonly hits: readonly Wow335KillingSpreeHit[]; } export interface Wow335KillingSpreeAssault { readonly at: number; readonly index: number; readonly targetIds: readonly string[]; } /** * The source's 51690 dummy aura has a 500-ms period, a 2-second duration, * and EXTRA_INITIAL_PERIOD, yielding an immediate tick plus four 500-ms ticks. */ export function createWow335KillingSpreeWindow(input: { readonly ownerActorId: string; readonly abilityId: string; readonly role: import("./partyRoles").PartyRole; readonly now: number; readonly targetIds: readonly string[]; readonly hands: readonly Wow335KillingSpreeWeaponSnapshot[]; readonly attackPower: number; readonly fallbackDamage: number; }): Wow335KillingSpreeWindow { const hands = input.hands.length > 0 ? input.hands : [{ hand: "main-hand" as const, damageMin: 0, damageMax: 0, speedMs: 2_000, }]; const hits = hands.map((weapon): Wow335KillingSpreeHit => { const hasWeaponDamage = weapon.damageMin > 0 || weapon.damageMax > 0; const coefficient = weapon.weaponType === "dagger" ? 1.95 : 1.3; const damage = hasWeaponDamage ? Math.max(1, Math.round(((weapon.damageMin + weapon.damageMax) / 2 + input.attackPower / 14 * weapon.speedMs / 1_000) * coefficient)) : Math.max(1, Math.round(input.fallbackDamage)); return { hand: weapon.hand, damage, speedMs: weapon.speedMs, dbcSpellId: weapon.hand === "off-hand" ? 57842 : 57841, }; }); const uniqueTargetIds = [...new Set(input.targetIds)]; return { ownerActorId: input.ownerActorId, abilityId: input.abilityId, role: input.role, startedAt: input.now, endsAt: input.now + WOW335_ROGUE_KILLING_SPREE_DURATION_MS, nextAssaultAt: input.now, completedAssaults: 0, targetIds: uniqueTargetIds, hits, }; } /** Advance one scheduled assault and retain its cast-time target list. */ export function advanceWow335KillingSpreeAssault( window: Wow335KillingSpreeWindow, targetIds: readonly string[], ): { readonly assault: Wow335KillingSpreeAssault; readonly window: Wow335KillingSpreeWindow | null } | null { if (window.completedAssaults >= WOW335_ROGUE_KILLING_SPREE_ASSAULTS) return null; const assault: Wow335KillingSpreeAssault = { at: window.nextAssaultAt, index: window.completedAssaults, targetIds, }; const completedAssaults = window.completedAssaults + 1; return { assault, window: completedAssaults >= WOW335_ROGUE_KILLING_SPREE_ASSAULTS ? null : { ...window, targetIds, completedAssaults, nextAssaultAt: window.nextAssaultAt + WOW335_ROGUE_KILLING_SPREE_INTERVAL_MS, }, }; } /** Source retains successful targets, so selection is with replacement. */ export function selectWow335KillingSpreeTarget( targetIds: readonly string[], roll: number, ): string | null { if (targetIds.length === 0) return null; const normalizedRoll = Number.isFinite(roll) ? Math.max(0, Math.min(0.999_999_999, roll)) : 0; return targetIds[Math.floor(normalizedRoll * targetIds.length)] ?? null; }