296 lines
9.6 KiB
TypeScript
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"
|
|
));
|
|
}
|