import { ActiveAura, AuraDefinition } from "./combatAuras"; import { WOW335_TALENT_NODES } from "./wow335Talents.generated"; export const WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG = "wow335-death-knight-mark-of-blood"; export const WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_SPELL_ID = 49005; export const WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID = "wow335-death-knight-mark-of-blood"; export interface Wow335DeathKnightMarkOfBloodProfile { readonly healingFraction: number; readonly durationMs: number; readonly hitCap: number; } export interface Wow335DeathKnightMarkOfBloodDamageResult { readonly auras: readonly ActiveAura[]; readonly triggered: boolean; readonly healingAmount: number; readonly sourceActorId?: string; readonly abilityId?: string; readonly remainingHits?: number; } const MARK_OF_BLOOD_TALENT = WOW335_TALENT_NODES.find((node) => node.id === "talent-1949"); /** Read Mark of Blood's heal, duration, and charge cap from the local generated rank. */ export function wow335DeathKnightMarkOfBloodProfile(rank = 1): Wow335DeathKnightMarkOfBloodProfile | null { const normalizedRank = Math.trunc(rank); const description = MARK_OF_BLOOD_TALENT?.rankDescriptions[normalizedRank - 1]; const rankEffect = MARK_OF_BLOOD_TALENT?.rankEffects[normalizedRank - 1]; if (!description || !rankEffect) return null; const healPercent = /healed for\s+(\d+(?:\.\d+)?)% of its maximum health/i.exec(description); const durationSeconds = /Lasts for\s+(\d+(?:\.\d+)?) seconds/i.exec(description); const healingFraction = healPercent ? Number(healPercent[1]) / 100 : 0; const durationMs = durationSeconds ? Math.round(Number(durationSeconds[1]) * 1_000) : 0; const hitCap = Math.floor(rankEffect.procCharges ?? 0); if (!(healingFraction > 0) || !(durationMs > 0) || !(hitCap > 0)) return null; return { healingFraction, durationMs, hitCap }; } /** Attach the generated Mark of Blood hit count and trigger tag to its DBC aura. */ export function wow335DeathKnightMarkOfBloodAuraDefinition( definition: AuraDefinition, ): AuraDefinition { const profile = wow335DeathKnightMarkOfBloodProfile(); if (!profile) return definition; return { ...definition, durationMs: profile.durationMs, maxStacks: profile.hitCap, initialStacks: profile.hitCap, stackBehavior: "replace", utilityTags: [...new Set([...(definition.utilityTags ?? []), WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG])], }; } /** Consume one Mark charge for landed damage; only surviving targets can receive the heal. */ export function wow335DeathKnightMarkOfBloodOnDamage( auras: readonly ActiveAura[], markedEnemyId: string, targetHealthAfterDamage: number, targetMaxHealth: number, now: number, landedDamage: boolean, ): Wow335DeathKnightMarkOfBloodDamageResult { if (!landedDamage || !(targetMaxHealth > 0)) { return { auras, triggered: false, healingAmount: 0 }; } const mark = auras.find((aura) => aura.targetId === markedEnemyId && (aura.expiresAt === null || aura.expiresAt > now) && aura.definition.utilityTags?.includes(WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG)); const profile = wow335DeathKnightMarkOfBloodProfile(); if (!mark || !profile || mark.stacks <= 0) { return { auras, triggered: false, healingAmount: 0 }; } const remainingHits = mark.stacks - 1; const nextAuras = remainingHits > 0 ? auras.map((aura) => aura.instanceId === mark.instanceId ? { ...aura, stacks: remainingHits } : aura) : auras.filter((aura) => aura.instanceId !== mark.instanceId); return { auras: nextAuras, triggered: true, healingAmount: targetHealthAfterDamage > 0 ? Math.round(targetMaxHealth * profile.healingFraction) : 0, sourceActorId: mark.sourceId, abilityId: WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID, remainingHits, }; }