import { creatureTemplateBaseDamage, creatureTemplateReference, } from "./azerothCoreReference"; /** * Source-backed periodic damage used by Stockade Rend. Values describe the * 3.3.5 Spell.dbc source rows before AzerothCore's caster-level scaling, * spell damage modifiers, and target mitigation are applied. */ export type EnemyPeriodicDamageSourceProfile = "ascension-patch-d" | "epoch-client"; export interface EnemyPeriodicDamageSource { readonly spellId: 6547 | 16509; readonly sourceProfile: EnemyPeriodicDamageSourceProfile; readonly durationMs: 15_000; readonly tickIntervalMs: 3_000; readonly tickCount: 5; readonly school: "physical"; readonly schoolMask: 1; /** Spell.dbc SpellDamageClass: 2 (melee). */ readonly damageClass: 2; readonly attackKind: "melee"; /** Spell.dbc effect opcode and aura type: APPLY_AURA / PERIODIC_DAMAGE. */ readonly effectType: 6; readonly auraType: 3; /** Spell.dbc MECHANIC_BLEED. */ readonly mechanic: 15; /** Spell.dbc implicit target A 6, with no secondary target. */ readonly implicitTargetA: 6; readonly implicitTargetB: 0; readonly durationIndex: 8; readonly stackAmount: 0; readonly effectBasePoints: number; readonly effectDieSides: 1; readonly effectRealPointsPerLevel: 0; readonly spellLevel: 20; readonly baseLevel: 20; readonly maxLevel: 0; readonly scalesWithCreatureLevel: boolean; /** * The core CalcValue result before creature-level scaling and damage * modifiers: BasePoints plus a roll from 1 through DieSides. */ readonly dbcBaseTickAmount: number; /** Periodic bleed damage bypasses armor in the referenced core rules. */ readonly periodicIgnoresArmor: true; /** Ordinary same-caster casts refresh duration/ticks and restart the tick phase. */ readonly sameCasterRefresh: "duration-and-tick-count-reset-phase"; /** TRIGGERED_NO_PERIODIC_RESET casts refresh duration/ticks but keep the old phase. */ readonly triggeredNoPeriodicResetRefresh: "duration-and-tick-count-preserve-phase"; /** Independent periodic auras from different casters can coexist. */ readonly differentCasterStacking: "independent"; } export type EnemyPeriodicDamageTickAmountResult = | { readonly status: "resolved"; /** Source core SpellEffectInfo::CalcValue result, before spell/target modifiers. */ readonly tickAmount: number; readonly dbcBaseTickAmount: number; readonly creatureLevelScale: number; } | { readonly status: "unsupported"; readonly reason: "invalid-creature-level" | "creature-template-missing" | "creature-base-damage-missing"; }; const STOCKADE_REND_SOURCE: Readonly> >>> = Object.freeze({ "ascension-patch-d": Object.freeze({ 6547: Object.freeze({ spellId: 6547, sourceProfile: "ascension-patch-d", durationMs: 15_000, tickIntervalMs: 3_000, tickCount: 5, school: "physical", schoolMask: 1, damageClass: 2, attackKind: "melee", effectType: 6, auraType: 3, mechanic: 15, implicitTargetA: 6, implicitTargetB: 0, durationIndex: 8, stackAmount: 0, effectBasePoints: 13, effectDieSides: 1, effectRealPointsPerLevel: 0, spellLevel: 20, baseLevel: 20, maxLevel: 0, scalesWithCreatureLevel: false, dbcBaseTickAmount: 14, periodicIgnoresArmor: true, sameCasterRefresh: "duration-and-tick-count-reset-phase", triggeredNoPeriodicResetRefresh: "duration-and-tick-count-preserve-phase", differentCasterStacking: "independent", }), 16509: Object.freeze({ spellId: 16509, sourceProfile: "ascension-patch-d", durationMs: 15_000, tickIntervalMs: 3_000, tickCount: 5, school: "physical", schoolMask: 1, damageClass: 2, attackKind: "melee", effectType: 6, auraType: 3, mechanic: 15, implicitTargetA: 6, implicitTargetB: 0, durationIndex: 8, stackAmount: 0, effectBasePoints: 26, effectDieSides: 1, effectRealPointsPerLevel: 0, spellLevel: 20, baseLevel: 20, maxLevel: 0, scalesWithCreatureLevel: true, dbcBaseTickAmount: 27, periodicIgnoresArmor: true, sameCasterRefresh: "duration-and-tick-count-reset-phase", triggeredNoPeriodicResetRefresh: "duration-and-tick-count-preserve-phase", differentCasterStacking: "independent", }), }), "epoch-client": Object.freeze({ 6547: Object.freeze({ spellId: 6547, sourceProfile: "epoch-client", durationMs: 15_000, tickIntervalMs: 3_000, tickCount: 5, school: "physical", schoolMask: 1, damageClass: 2, attackKind: "melee", effectType: 6, auraType: 3, mechanic: 15, implicitTargetA: 6, implicitTargetB: 0, durationIndex: 8, stackAmount: 0, effectBasePoints: 9, effectDieSides: 1, effectRealPointsPerLevel: 0, spellLevel: 20, baseLevel: 20, maxLevel: 0, scalesWithCreatureLevel: false, dbcBaseTickAmount: 10, periodicIgnoresArmor: true, sameCasterRefresh: "duration-and-tick-count-reset-phase", triggeredNoPeriodicResetRefresh: "duration-and-tick-count-preserve-phase", differentCasterStacking: "independent", }), 16509: Object.freeze({ spellId: 16509, sourceProfile: "epoch-client", durationMs: 15_000, tickIntervalMs: 3_000, tickCount: 5, school: "physical", schoolMask: 1, damageClass: 2, attackKind: "melee", effectType: 6, auraType: 3, mechanic: 15, implicitTargetA: 6, implicitTargetB: 0, durationIndex: 8, stackAmount: 0, effectBasePoints: 26, effectDieSides: 1, effectRealPointsPerLevel: 0, spellLevel: 20, baseLevel: 20, maxLevel: 0, scalesWithCreatureLevel: true, dbcBaseTickAmount: 27, periodicIgnoresArmor: true, sameCasterRefresh: "duration-and-tick-count-reset-phase", triggeredNoPeriodicResetRefresh: "duration-and-tick-count-preserve-phase", differentCasterStacking: "independent", }), }), }); /** Returns an audited Rend source row, or undefined for an unverified spell/profile pair. */ export function enemyPeriodicDamageSource( spellId: number, sourceProfile: EnemyPeriodicDamageSourceProfile, ): EnemyPeriodicDamageSource | undefined { if (spellId !== 6547 && spellId !== 16509) return undefined; return STOCKADE_REND_SOURCE[sourceProfile][spellId]; } /** Initial application ticks at one full source amplitude after the aura is applied. */ export function enemyPeriodicTickTimes( source: EnemyPeriodicDamageSource, appliedAtMs: number, ): number[] { return Array.from({ length: source.tickCount }, (_, index) => ( appliedAtMs + (index + 1) * source.tickIntervalMs )); } /** * Resolves the NPC-side SpellEffectInfo::CalcValue amount for these audited * Rend rows. For 16509, this mirrors the referenced core's creature base * damage ratio and final int32 truncation. Spell damage bonuses and target * modifiers are applied later by the periodic damage resolver. */ export function enemyPeriodicDamageTickAmount( serverEntry: number, creatureLevel: number, source: EnemyPeriodicDamageSource, ): EnemyPeriodicDamageTickAmountResult { if (!Number.isInteger(creatureLevel) || creatureLevel < 1 || creatureLevel > 80) { return { status: "unsupported", reason: "invalid-creature-level" }; } if (!source.scalesWithCreatureLevel || creatureLevel === source.spellLevel) { return { status: "resolved", tickAmount: source.dbcBaseTickAmount, dbcBaseTickAmount: source.dbcBaseTickAmount, creatureLevelScale: 1, }; } if (!creatureTemplateReference(serverEntry)) { return { status: "unsupported", reason: "creature-template-missing" }; } const creatureStats = creatureTemplateBaseDamage(serverEntry, creatureLevel); const spellLevelStats = creatureTemplateBaseDamage(serverEntry, source.spellLevel); if (!creatureStats || !spellLevelStats) { return { status: "unsupported", reason: "creature-base-damage-missing" }; } const creatureLevelScale = creatureStats.baseDamage / spellLevelStats.baseDamage; return { status: "resolved", tickAmount: Math.trunc(source.dbcBaseTickAmount * creatureLevelScale), dbcBaseTickAmount: source.dbcBaseTickAmount, creatureLevelScale, }; }