259 lines
8.4 KiB
TypeScript
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,
|
|
};
|
|
}
|