120 lines
3.9 KiB
TypeScript
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;
|
|
}
|