Files
healer-man/src/game/wow335RogueKillingSpreeRuntime.ts
T
2026-10-04 13:13:35 -04:00

120 lines
3.9 KiB
TypeScript

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