146 lines
5.8 KiB
TypeScript
146 lines
5.8 KiB
TypeScript
import type { ActiveAura, AuraDefinition, AttackKind } from "./combatAuras";
|
|
|
|
export const ROM_ROGUE_POISON_SKILL_ID = 490329;
|
|
export const ROM_ROGUE_POISON_COMPONENT_ID = 500715;
|
|
export const ROM_ROGUE_POISON_TRIGGER_SPELL_ID = 490357;
|
|
export const ROM_ROGUE_POISON_AURA_TAG = "rom-poisonous";
|
|
export const ROM_ROGUE_POISONED_TARGET_TAG = "rom-poisonous-hands-debuff";
|
|
export const ROM_ROGUE_POISON_PROC_CHANCE = 0.5;
|
|
export const ROM_ROGUE_POISON_MAX_STACKS = 10;
|
|
export const ROM_ROGUE_POISON_DAMAGE_REDUCTION = 0.08;
|
|
export const ROM_ROGUE_POISON_ATTACK_SPEED_PER_STACK_PERCENT = 1;
|
|
export const ROM_ROGUE_POISON_DEBUFF_DURATION_MS = 900_000;
|
|
export const ROM_ROGUE_POISON_ATTACK_SPEED_TAG = "rom-poisonous-hands-attack-speed";
|
|
const ROM_ROGUE_POISON_ATTACK_SPEED_TARGET_PREFIX = "rom-poisonous-hands-target:";
|
|
|
|
export interface RomRoguePoisonAttackContext {
|
|
readonly classId: string;
|
|
readonly sourceActorId: string;
|
|
readonly targetActorId: string;
|
|
readonly attackKind: AttackKind;
|
|
readonly isNormalAttack: boolean;
|
|
readonly attackLanded: boolean;
|
|
readonly poisonActive: boolean;
|
|
readonly roll: number;
|
|
/** Target represented by the Rogue's current Poisonous Hands speed buff, if any. */
|
|
readonly attackSpeedTargetId?: string | null;
|
|
/** Existing Poisonous Hands stacks on this attack's target for this Rogue. */
|
|
readonly targetPoisonStacks?: number;
|
|
}
|
|
|
|
export interface RomRoguePoisonApplication {
|
|
readonly sourceId: string;
|
|
readonly targetId: string;
|
|
readonly definition: AuraDefinition;
|
|
readonly stacks: 1;
|
|
}
|
|
|
|
export interface RomRoguePoisonAttackResult {
|
|
/** Clear the prior target's speed ramp when the Rogue changes targets. */
|
|
readonly resetAttackSpeed: boolean;
|
|
/** Set only when this attack procs the child Poisoned effect. */
|
|
readonly application: RomRoguePoisonApplication | null;
|
|
/** A successful Poisoned application also advances the current-target speed ramp. */
|
|
readonly attackSpeedApplication: RomRoguePoisonApplication | null;
|
|
}
|
|
|
|
export function romRoguePoisonAttackSpeedTargetId(aura: ActiveAura): string | null {
|
|
return aura.definition.utilityTags
|
|
?.find((tag) => tag.startsWith(ROM_ROGUE_POISON_ATTACK_SPEED_TARGET_PREFIX))
|
|
?.slice(ROM_ROGUE_POISON_ATTACK_SPEED_TARGET_PREFIX.length) ?? null;
|
|
}
|
|
|
|
export function romRoguePoisonAttackSpeedAuraForTarget(targetId: string): AuraDefinition {
|
|
return {
|
|
id: "rom-poisonous-hands-attack-speed",
|
|
name: "Poisonous Hands Attack Speed",
|
|
disposition: "buff",
|
|
// CoA does not list a separate timer for the ramp; scope it to the same
|
|
// 900-second Poisonous Hands window and clear it on a target switch.
|
|
durationMs: ROM_ROGUE_POISON_DEBUFF_DURATION_MS,
|
|
maxStacks: ROM_ROGUE_POISON_MAX_STACKS,
|
|
stackBehavior: "add",
|
|
uniqueBySource: true,
|
|
utilityTags: [ROM_ROGUE_POISON_ATTACK_SPEED_TAG, `${ROM_ROGUE_POISON_ATTACK_SPEED_TARGET_PREFIX}${targetId}`],
|
|
modifiers: [{
|
|
kind: "stat",
|
|
stat: "melee-haste",
|
|
operation: "flat",
|
|
value: ROM_ROGUE_POISON_ATTACK_SPEED_PER_STACK_PERCENT,
|
|
perStack: true,
|
|
}],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Current CoA Poisonous Hands (490329/500715) triggers child 490357 on a
|
|
* successful normal attack. Each proc stacks Poisoned on the target to ten,
|
|
* with a fixed 8% outgoing-damage reduction. The current-target speed ramp
|
|
* advances on the same proc and is reset by a landed normal attack on a new
|
|
* target. The source gives the exact 50% trigger rate locally; CoA patch notes
|
|
* supply the revised ten-stack and attack-speed behavior.
|
|
*/
|
|
export function romRoguePoisonAttackResultForAttack(
|
|
context: RomRoguePoisonAttackContext,
|
|
): RomRoguePoisonAttackResult | null {
|
|
if (context.classId !== "rom-rogue"
|
|
|| !context.sourceActorId
|
|
|| !context.targetActorId
|
|
|| context.sourceActorId === context.targetActorId
|
|
|| context.attackKind === "spell"
|
|
|| !context.isNormalAttack
|
|
|| !context.attackLanded
|
|
|| !context.poisonActive
|
|
|| !Number.isFinite(context.roll)
|
|
|| context.roll < 0) return null;
|
|
|
|
const resetAttackSpeed = context.attackSpeedTargetId != null
|
|
&& context.attackSpeedTargetId !== context.targetActorId;
|
|
const application = context.roll < ROM_ROGUE_POISON_PROC_CHANCE
|
|
? {
|
|
sourceId: context.sourceActorId,
|
|
targetId: context.targetActorId,
|
|
stacks: 1 as const,
|
|
definition: {
|
|
id: `rom-rogue-poisoned:${ROM_ROGUE_POISON_TRIGGER_SPELL_ID}`,
|
|
name: "Poisoned",
|
|
disposition: "debuff" as const,
|
|
durationMs: ROM_ROGUE_POISON_DEBUFF_DURATION_MS,
|
|
maxStacks: ROM_ROGUE_POISON_MAX_STACKS,
|
|
stackBehavior: "add" as const,
|
|
uniqueBySource: true,
|
|
dispelCategory: "poison" as const,
|
|
utilityTags: [ROM_ROGUE_POISONED_TARGET_TAG],
|
|
modifiers: [{
|
|
kind: "damage" as const,
|
|
direction: "dealt" as const,
|
|
operation: "percent" as const,
|
|
value: -ROM_ROGUE_POISON_DAMAGE_REDUCTION,
|
|
// CoA says the target's damage is reduced by 8%; later stacks do not
|
|
// multiply that penalty (patch 12.1.0.4000).
|
|
perStack: false,
|
|
}],
|
|
},
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
resetAttackSpeed,
|
|
application,
|
|
attackSpeedApplication: application && Math.max(0, context.targetPoisonStacks ?? 0) < ROM_ROGUE_POISON_MAX_STACKS ? {
|
|
sourceId: context.sourceActorId,
|
|
// Player runtime maps this logical self target to its aura entity ID.
|
|
targetId: context.sourceActorId,
|
|
definition: romRoguePoisonAttackSpeedAuraForTarget(context.targetActorId),
|
|
stacks: 1,
|
|
} : null,
|
|
};
|
|
}
|
|
|
|
/** Compatibility helper for callers/tests that only need the target debuff. */
|
|
export function romRoguePoisonApplicationForAttack(
|
|
context: RomRoguePoisonAttackContext,
|
|
): RomRoguePoisonApplication | null {
|
|
return romRoguePoisonAttackResultForAttack(context)?.application ?? null;
|
|
}
|