forked from phenom/healer-man
123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
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<Record<string, number>>,
|
|
): 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<Record<string, number>>,
|
|
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)
|
|
));
|
|
}
|