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

296 lines
9.6 KiB
TypeScript

import type { ActiveAura, AuraDefinition, AuraModifier } from "./combatAuras";
/** Spell.dbc profile selected by the dungeon recipe. */
export type EnemyStanceSourceProfile = "ascension" | "epoch";
export type EnemyStanceSpellId = 7164 | 7165;
export type EnemyStanceForm = 17 | 18;
export interface EnemyStanceEffectSignature {
readonly effectType: number;
readonly auraType: number;
readonly basePoints: number;
readonly mechanic: number;
readonly implicitTargetA: number;
readonly implicitTargetB: number;
readonly radiusIndex: number;
readonly radius: number;
readonly miscValue: number;
readonly periodMs: number;
readonly triggerSpellId: number;
}
export interface EnemyStanceSpellScalars {
readonly attributes0: number;
readonly dispel: number;
readonly durationIndex: number;
readonly durationMs: number;
readonly rangeIndex: number;
readonly schoolMask: number;
readonly damageClass: number;
readonly stackAmount: number;
readonly stanceMask: readonly [number, number];
readonly stanceExcludeMask: readonly [number, number];
readonly equippedItemClass: number;
readonly equippedItemSubclassMask: number;
readonly equippedItemInventoryMask: number;
}
export interface EnemyStanceSpellSignature extends EnemyStanceSpellScalars {
readonly effects: readonly EnemyStanceEffectSignature[];
}
export interface UnsupportedEnemyStanceEffect extends EnemyStanceEffectSignature {
readonly reason: string;
}
export interface EnemyStanceContract {
readonly spellId: EnemyStanceSpellId;
readonly name: "Defensive Stance" | "Battle Stance";
readonly profile: EnemyStanceSourceProfile;
readonly form: EnemyStanceForm;
/** Runtime duration; null means permanent until replaced or explicitly removed. */
readonly durationMs: number | null;
readonly target: "self";
readonly exclusiveGroup: "enemy-warrior-stance";
readonly signature: EnemyStanceSpellSignature;
/** AzerothCore's form handler applies and removes this passive by form ID. */
readonly childPassiveSpellId: number;
/** Attribute 0 identifies the linked Spell.dbc row as passive in both clients. */
readonly childPassiveScalars: EnemyStanceSpellScalars;
/** Passive Spell.dbc row applied by the historical server's stance handler. */
readonly childPassiveEffects: readonly EnemyStanceEffectSignature[];
/** Modifier subset supported by ActiveAura's current stat and damage resolver. */
readonly modifiers: readonly AuraModifier[];
/** Child passive effects whose behavior is outside the current runtime. */
readonly unsupportedEffects: readonly UnsupportedEnemyStanceEffect[];
}
export const ENEMY_STANCE_SOURCE_PINS = Object.freeze({
ascension: Object.freeze({
spellDbcPath: "../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc",
spellDbcSha256: "2f77f18b91ffbae32a19568e4bc05ec78ee384b6ba8dd2a6e05f0e97abbb2dce",
durationSource: "shared patch-S SpellDuration.dbc fallback",
durationDbcSha256: "eeae44ef0185924f3d6ec57ab2701c66566db86e51ab0bae017a9449126eb3a0",
}),
epoch: Object.freeze({
spellDbcPath: "../HealerMan-Storage/pipeline-work/dungeons/epoch-client/DBFilesClient/Spell.dbc",
spellDbcSha256: "d5cce1a83550dcfa9eb2f0251dbb11fd24c272534b2b1a9b230924a44d817ab3",
durationSource: "shared patch-S SpellDuration.dbc fallback",
durationDbcSha256: "eeae44ef0185924f3d6ec57ab2701c66566db86e51ab0bae017a9449126eb3a0",
}),
});
const effect = (
effectType: number,
auraType: number,
basePoints: number,
miscValue = 0,
triggerSpellId = 0,
): EnemyStanceEffectSignature => Object.freeze({
effectType,
auraType,
basePoints,
mechanic: 0,
implicitTargetA: 1,
implicitTargetB: 0,
radiusIndex: 0,
radius: 0,
miscValue,
periodMs: 0,
triggerSpellId,
});
const spellScalars = (input: {
attributes0: number;
durationIndex: number;
durationMs: number;
damageClass: number;
}): EnemyStanceSpellScalars => Object.freeze({
...input,
dispel: 0,
rangeIndex: 1,
schoolMask: 1,
stackAmount: 0,
stanceMask: Object.freeze([0, 0]) as readonly [number, number],
stanceExcludeMask: Object.freeze([0, 0]) as readonly [number, number],
equippedItemClass: -1,
equippedItemSubclassMask: 0,
equippedItemInventoryMask: 0,
});
const stanceSignature = (
durationMs: number,
form: EnemyStanceForm,
durationIndex: number,
damageClass: number,
): EnemyStanceSpellSignature => Object.freeze({
attributes0: 327_696,
dispel: 0,
durationIndex,
durationMs,
rangeIndex: 1,
schoolMask: 1,
damageClass,
stackAmount: 0,
stanceMask: Object.freeze([0, 0]) as readonly [number, number],
stanceExcludeMask: Object.freeze([0, 0]) as readonly [number, number],
equippedItemClass: -1,
equippedItemSubclassMask: 0,
equippedItemInventoryMask: 0,
effects: Object.freeze([effect(6, 36, 0, form)]),
});
const unsupported = (
sourceEffect: EnemyStanceEffectSignature,
reason: string,
): UnsupportedEnemyStanceEffect => Object.freeze({ ...sourceEffect, reason });
const allSchoolDamageModifier = (
direction: "dealt" | "taken",
value: number,
): AuraModifier => Object.freeze({
kind: "damage",
direction,
operation: "percent",
value,
multiplicative: true,
});
const threatModifier = (value: number): AuraModifier => Object.freeze({
kind: "stat",
stat: "threat",
operation: "percent",
value,
multiplicative: true,
});
const contracts: Readonly<Record<EnemyStanceSourceProfile, Readonly<Record<EnemyStanceSpellId, EnemyStanceContract>>>> = Object.freeze({
ascension: Object.freeze({
7164: Object.freeze({
spellId: 7164,
name: "Defensive Stance",
profile: "ascension",
form: 18,
durationMs: 180_000,
target: "self",
exclusiveGroup: "enemy-warrior-stance",
signature: stanceSignature(180_000, 18, 25, 0),
childPassiveSpellId: 7376,
childPassiveScalars: spellScalars({ attributes0: 327_888, durationIndex: 0, durationMs: 0, damageClass: 0 }),
childPassiveEffects: Object.freeze([
effect(6, 87, -10, 127),
effect(6, 79, -5, 127),
effect(6, 4, 42, 127),
]),
modifiers: Object.freeze([
allSchoolDamageModifier("taken", -0.10),
allSchoolDamageModifier("dealt", -0.05),
]),
unsupportedEffects: Object.freeze([
unsupported(effect(6, 4, 42, 127), "Aura 4 is DUMMY; this row has no sourced trigger spell or supported runtime behavior."),
]),
}),
7165: Object.freeze({
spellId: 7165,
name: "Battle Stance",
profile: "ascension",
form: 17,
durationMs: null,
target: "self",
exclusiveGroup: "enemy-warrior-stance",
signature: stanceSignature(-1, 17, 21, 2),
childPassiveSpellId: 21156,
childPassiveScalars: spellScalars({ attributes0: 327_888, durationIndex: 0, durationMs: 0, damageClass: 0 }),
childPassiveEffects: Object.freeze([
effect(6, 10, 0, 127),
effect(6, 280, 0, 0),
]),
modifiers: Object.freeze([]),
unsupportedEffects: Object.freeze([]),
}),
}),
epoch: Object.freeze({
7164: Object.freeze({
spellId: 7164,
name: "Defensive Stance",
profile: "epoch",
form: 18,
durationMs: 180_000,
target: "self",
exclusiveGroup: "enemy-warrior-stance",
signature: stanceSignature(180_000, 18, 25, 0),
childPassiveSpellId: 7376,
childPassiveScalars: spellScalars({ attributes0: 327_888, durationIndex: 0, durationMs: 0, damageClass: 0 }),
childPassiveEffects: Object.freeze([
effect(6, 87, -10, 127),
effect(6, 79, -5, 127),
effect(6, 10, 45, 127),
]),
modifiers: Object.freeze([
allSchoolDamageModifier("taken", -0.10),
allSchoolDamageModifier("dealt", -0.05),
threatModifier(0.45),
]),
unsupportedEffects: Object.freeze([]),
}),
7165: Object.freeze({
spellId: 7165,
name: "Battle Stance",
profile: "epoch",
form: 17,
durationMs: null,
target: "self",
exclusiveGroup: "enemy-warrior-stance",
signature: stanceSignature(-1, 17, 21, 2),
childPassiveSpellId: 21156,
childPassiveScalars: spellScalars({ attributes0: 327_888, durationIndex: 0, durationMs: 0, damageClass: 0 }),
childPassiveEffects: Object.freeze([
effect(6, 10, -20, 127),
]),
modifiers: Object.freeze([threatModifier(-0.20)]),
unsupportedEffects: Object.freeze([]),
}),
}),
});
/** Return a pinned form spell and its profile-specific server-linked passive row. */
export function enemyStanceContract(
spellId: number,
profile: EnemyStanceSourceProfile,
): EnemyStanceContract | undefined {
if (spellId !== 7164 && spellId !== 7165) return undefined;
return contracts[profile][spellId];
}
/** Build a state aura, retaining stance identity even where no modifier is supported. */
export function supportedEnemyStanceAuraDefinition(
contract: EnemyStanceContract,
): AuraDefinition {
return {
id: `enemy-source-spell-${contract.spellId}`,
name: contract.name,
disposition: "buff",
durationMs: contract.durationMs,
stackBehavior: "replace",
exclusiveGroup: contract.exclusiveGroup,
utilityTags: [
"enemy-warrior-stance",
`enemy-warrior-form:${contract.form}`,
`enemy-passive-spell:${contract.childPassiveSpellId}`,
],
modifiers: contract.modifiers,
};
}
/** Remove the actor's active source stance during an explicit form/reset transition. */
export function removeEnemyStanceAura(
current: readonly ActiveAura[],
actorId: string,
): readonly ActiveAura[] {
return current.filter((aura) => !(
aura.targetId === actorId
&& aura.sourceId === actorId
&& aura.definition.exclusiveGroup === "enemy-warrior-stance"
));
}