import type { ActiveAura, AuraDefinition } from "./combatAuras"; export const ROGUE_INSTANT_POISON_TAG = "rogue-instant-poison"; export const ROGUE_DEADLY_POISON_COATING_TAG = "rogue-deadly-poison-coating"; export const ROGUE_DEADLY_POISON_DOSE_TAG = "rogue-deadly-poison-dose"; export const ROGUE_ENVENOM_BUFF_TAG = "rogue-envenom-poison-buff"; export const ROGUE_DEADLY_POISON_AURA: AuraDefinition = Object.freeze({ id: "rogue-deadly-poison-doses", name: "Deadly Poison", description: "Deals Nature damage every 3 sec. Stacks up to 5 times.", disposition: "debuff", durationMs: 12_000, maxStacks: 5, stackBehavior: "add", uniqueBySource: true, utilityTags: [ROGUE_DEADLY_POISON_DOSE_TAG], dispelCategory: "poison", }); export interface RoguePoisonFinisherScaling { readonly doseAuraTag: string; readonly maximumDoses: number; readonly basePointsPerDose: number; readonly attackPowerCoefficientPerDose: number; } export interface RoguePoisonApplicationChances { readonly instant: number; readonly deadly: number; } const MASTER_POISONER_TALENT_ID = "talent-1715"; const INSTANT_POISON_BASE_BY_LEVEL: readonly (readonly [level: number, amount: number])[] = [ [20, 22], [28, 43], [36, 79], [44, 121], [52, 170], [60, 225], [68, 284], [73, 350], ]; const DEADLY_POISON_TICK_BY_LEVEL: readonly (readonly [level: number, amount: number])[] = [ [30, 12], [38, 20], [46, 28], [54, 36], [60, 44], [62, 52], [70, 62], [76, 74], ]; function rankAmount(level: number, ranks: readonly (readonly [level: number, amount: number])[]): number { let amount = ranks[0]?.[1] ?? 1; for (const [requiredLevel, rankAmountValue] of ranks) { if (level < requiredLevel) break; amount = rankAmountValue; } return amount; } export function rogueInstantPoisonDamage(level: number, attackPower: number): number { return Math.max(1, Math.round(rankAmount(level, INSTANT_POISON_BASE_BY_LEVEL) + Math.max(0, attackPower) * 0.1)); } export function rogueDeadlyPoisonTickDamage(level: number, attackPower: number, stacks: number): number { const boundedStacks = Math.max(0, Math.min(5, Math.floor(stacks))); return Math.max(0, Math.round( (rankAmount(level, DEADLY_POISON_TICK_BY_LEVEL) + Math.max(0, attackPower) * 0.03) * boundedStacks, )); } export function roguePoisonApplicationChances(envenomBuffActive: boolean): RoguePoisonApplicationChances { return Object.freeze({ // Envenom increases Instant Poison's application frequency by 75% and // adds 15 percentage points to Deadly Poison's application chance. instant: 0.2 * (envenomBuffActive ? 1.75 : 1), deadly: 0.3 + (envenomBuffActive ? 0.15 : 0), }); } export function rogueEnvenomPreservationChance( talentRanks: Readonly>, ): number { const rank = Math.max(0, Math.min(3, Math.floor(talentRanks[MASTER_POISONER_TALENT_ID] ?? 0))); return [0, 0.33, 0.66, 1][rank] ?? 0; } function stableRogueUnit(seed: string): number { let hash = 2166136261; for (let index = 0; index < seed.length; index += 1) { hash ^= seed.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0) / 0x1_0000_0000; } export function rogueEnvenomPreservesDeadlyPoison( talentRanks: Readonly>, seed: string, ): boolean { const chance = rogueEnvenomPreservationChance(talentRanks); return chance >= 1 || (chance > 0 && stableRogueUnit(seed) < chance); } export function matchingAuraStacks( auras: readonly ActiveAura[], tag: string, sourceId: string, targetId: string, now: number, ): number { return auras.find((aura) => ( aura.sourceId === sourceId && aura.targetId === targetId && (aura.expiresAt === null || aura.expiresAt > now) && aura.definition.utilityTags?.includes(tag) ))?.stacks ?? 0; } export function hasActiveAuraTag( auras: readonly ActiveAura[], tag: string, targetId: string, now: number, ): boolean { return auras.some((aura) => ( aura.targetId === targetId && (aura.expiresAt === null || aura.expiresAt > now) && aura.definition.utilityTags?.includes(tag) )); }