Files
healer-man/src/game/enemyPeriodicDamage.ts
T
2026-10-06 22:25:34 -04:00

259 lines
8.4 KiB
TypeScript

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<Record<EnemyPeriodicDamageSourceProfile, Readonly<
Partial<Record<EnemyPeriodicDamageSource["spellId"], EnemyPeriodicDamageSource>>
>>> = 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,
};
}