forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,463 @@
|
||||
import type { ActiveAura, AuraDefinition } from "./combatAuras";
|
||||
import type { EnemyAuraEffectSignature, EnemyAuraSpellSignature } from "./enemySourceAuras";
|
||||
|
||||
/** AzerothCore SpellAuraDefines.h aura ids consumed by ordinary NPC blocking. */
|
||||
export const SPELL_AURA_MOD_BLOCK_PERCENT = 51;
|
||||
export const SPELL_AURA_MOD_BLOCK_CRIT_CHANCE = 253;
|
||||
export const SPELL_AURA_IGNORE_HIT_DIRECTION = 288;
|
||||
|
||||
export type EnemyBlockAuraSourceProfile = "wow335" | "ascension" | "epoch";
|
||||
|
||||
export interface EnemyBlockAuraContract {
|
||||
readonly spellId: number;
|
||||
readonly name: string;
|
||||
readonly profile: EnemyBlockAuraSourceProfile;
|
||||
/** Raw Spell.dbc duration, with -1 representing the permanent duration row. */
|
||||
readonly signature: EnemyAuraSpellSignature;
|
||||
/** Runtime duration; null means until explicitly removed. */
|
||||
readonly durationMs: number | null;
|
||||
readonly disposition: "buff";
|
||||
readonly target: "self";
|
||||
readonly stackAmount: number;
|
||||
readonly effects: readonly EnemyAuraEffectSignature[];
|
||||
readonly unsupportedEffects: readonly {
|
||||
readonly effectIndex: number;
|
||||
readonly auraType: number;
|
||||
readonly basePoints: number;
|
||||
readonly miscValue: number;
|
||||
readonly reason: string;
|
||||
}[];
|
||||
readonly blockInputs: EnemyNpcBlockAuraInputs;
|
||||
}
|
||||
|
||||
export interface EnemyNpcBlockAuraInputs {
|
||||
/** Sum of active SPELL_AURA_MOD_BLOCK_PERCENT amounts, in percentage points. */
|
||||
readonly blockChanceAuraPercent: number;
|
||||
/** Integer chance passed to AzerothCore's roll_chance_i after a successful block roll. */
|
||||
readonly criticalBlockChancePercent: number;
|
||||
/** Any active SPELL_AURA_IGNORE_HIT_DIRECTION effect bypasses facing. */
|
||||
readonly ignoresHitDirection: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applicability evidence required before importing Critical Block's aura-107
|
||||
* siblings as no-ops. The core pin describes the default AzerothCore path;
|
||||
* callers must not assert it when custom scripts/modules change that path.
|
||||
*/
|
||||
export interface EnemyNpcCriticalBlockImportProof {
|
||||
readonly defenderId: string;
|
||||
readonly casterId: string;
|
||||
readonly targetId: string;
|
||||
readonly defenderKind: "ordinary-creature";
|
||||
readonly isNativeDefender: false;
|
||||
readonly isSummonedDefender: false;
|
||||
readonly isCharacterDefender: false;
|
||||
readonly aura107Applicability: "azerothcore-e822d718-default-spellmod-path";
|
||||
}
|
||||
|
||||
export const ENEMY_BLOCK_AURA_SOURCE_PINS = Object.freeze({
|
||||
wow335: Object.freeze({
|
||||
spellDbcPath: "../wow-pvt-server-backups-2/wow335a/Data/patch-A.MPQ/DBFilesClient/Spell.dbc",
|
||||
spellDbcSha256: "57011d528fd66cf39f5ab6fb168e389ab9e4cc1b8050f7201a54239d489b415c",
|
||||
durationDbcPath: "../LadiksMPQEditor/client-current/patch-S/DBFilesClient/SpellDuration.dbc",
|
||||
durationDbcSha256: "eeae44ef0185924f3d6ec57ab2701c66566db86e51ab0bae017a9449126eb3a0",
|
||||
}),
|
||||
ascension: Object.freeze({
|
||||
spellDbcPath: "../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc",
|
||||
spellDbcSha256: "2f77f18b91ffbae32a19568e4bc05ec78ee384b6ba8dd2a6e05f0e97abbb2dce",
|
||||
durationDbcPath: "../LadiksMPQEditor/client-current/patch-S/DBFilesClient/SpellDuration.dbc",
|
||||
durationDbcSha256: "eeae44ef0185924f3d6ec57ab2701c66566db86e51ab0bae017a9449126eb3a0",
|
||||
}),
|
||||
epoch: Object.freeze({
|
||||
spellDbcPath: "../HealerMan-Storage/pipeline-work/dungeons/epoch-client/DBFilesClient/Spell.dbc",
|
||||
spellDbcSha256: "d5cce1a83550dcfa9eb2f0251dbb11fd24c272534b2b1a9b230924a44d817ab3",
|
||||
durationDbcPath: "../LadiksMPQEditor/client-current/patch-S/DBFilesClient/SpellDuration.dbc",
|
||||
durationDbcSha256: "eeae44ef0185924f3d6ec57ab2701c66566db86e51ab0bae017a9449126eb3a0",
|
||||
}),
|
||||
});
|
||||
|
||||
const tagPrefix = "enemy-npc-block-aura:";
|
||||
const tag = (auraType: number, amount: number): string => `${tagPrefix}${auraType}:${amount}`;
|
||||
const auraInputTags = Object.freeze({
|
||||
blockChance: (amount: number) => tag(SPELL_AURA_MOD_BLOCK_PERCENT, amount),
|
||||
criticalBlockChance: (amount: number) => tag(SPELL_AURA_MOD_BLOCK_CRIT_CHANCE, amount),
|
||||
ignoreHitDirection: tag(SPELL_AURA_IGNORE_HIT_DIRECTION, 0),
|
||||
});
|
||||
|
||||
const effect = (
|
||||
effectType: number,
|
||||
auraType: number,
|
||||
basePoints: number,
|
||||
miscValue = 0,
|
||||
): EnemyAuraEffectSignature => Object.freeze({
|
||||
effectType,
|
||||
auraType,
|
||||
basePoints,
|
||||
mechanic: 0,
|
||||
implicitTargetA: 1,
|
||||
implicitTargetB: 0,
|
||||
radius: 0,
|
||||
miscValue,
|
||||
periodMs: 0,
|
||||
});
|
||||
|
||||
const signature = (
|
||||
durationMs: number,
|
||||
effects: readonly EnemyAuraEffectSignature[],
|
||||
): EnemyAuraSpellSignature => Object.freeze({
|
||||
rangeIndex: 1,
|
||||
schoolMask: 1,
|
||||
damageClass: 0,
|
||||
durationMs,
|
||||
effects: Object.freeze([...effects]),
|
||||
});
|
||||
|
||||
const unsupported = (
|
||||
effectIndex: number,
|
||||
auraType: number,
|
||||
basePoints: number,
|
||||
miscValue: number,
|
||||
): EnemyBlockAuraContract["unsupportedEffects"][number] => Object.freeze({
|
||||
effectIndex,
|
||||
auraType,
|
||||
basePoints,
|
||||
miscValue,
|
||||
reason: "This source effect is preserved in the signature but is outside ordinary NPC block aura inputs.",
|
||||
});
|
||||
|
||||
function blockChanceContract(profile: EnemyBlockAuraSourceProfile): EnemyBlockAuraContract {
|
||||
const sourceEffect = effect(6, SPELL_AURA_MOD_BLOCK_PERCENT, 75);
|
||||
return Object.freeze({
|
||||
spellId: 3419,
|
||||
name: "Improved Blocking",
|
||||
profile,
|
||||
signature: signature(6_000, [sourceEffect]),
|
||||
durationMs: 6_000,
|
||||
disposition: "buff",
|
||||
target: "self",
|
||||
stackAmount: 0,
|
||||
effects: Object.freeze([sourceEffect]),
|
||||
unsupportedEffects: Object.freeze([]),
|
||||
blockInputs: Object.freeze({
|
||||
blockChanceAuraPercent: 75,
|
||||
criticalBlockChancePercent: 0,
|
||||
ignoresHitDirection: false,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function criticalBlockContract(
|
||||
profile: EnemyBlockAuraSourceProfile,
|
||||
spellId: 47294 | 47295 | 47296,
|
||||
critChance: number,
|
||||
spellModifierRanks: readonly number[],
|
||||
): EnemyBlockAuraContract {
|
||||
const effects = [
|
||||
effect(6, SPELL_AURA_MOD_BLOCK_CRIT_CHANCE, critChance),
|
||||
...spellModifierRanks.map((value, index) => effect(6, 107, value, index === 0 ? 7 : 3)),
|
||||
];
|
||||
return Object.freeze({
|
||||
spellId,
|
||||
name: "Critical Block",
|
||||
profile,
|
||||
signature: signature(-1, effects),
|
||||
durationMs: null,
|
||||
disposition: "buff",
|
||||
target: "self",
|
||||
stackAmount: 0,
|
||||
effects: Object.freeze(effects),
|
||||
unsupportedEffects: Object.freeze(effects.slice(1).map((sourceEffect, index) => (
|
||||
unsupported(index + 1, sourceEffect.auraType, sourceEffect.basePoints, sourceEffect.miscValue)
|
||||
))),
|
||||
blockInputs: Object.freeze({
|
||||
blockChanceAuraPercent: 0,
|
||||
criticalBlockChancePercent: critChance,
|
||||
ignoresHitDirection: false,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function deterrenceContract(
|
||||
profile: EnemyBlockAuraSourceProfile,
|
||||
spellId: 67801 | 667801 | 1167801,
|
||||
durationMs: number,
|
||||
additionalEffects: readonly EnemyAuraEffectSignature[],
|
||||
): EnemyBlockAuraContract {
|
||||
const direction = effect(6, SPELL_AURA_IGNORE_HIT_DIRECTION, 100);
|
||||
const effects = [direction, ...additionalEffects];
|
||||
return Object.freeze({
|
||||
spellId,
|
||||
name: "Deterrence",
|
||||
profile,
|
||||
signature: signature(durationMs, effects),
|
||||
durationMs,
|
||||
disposition: "buff",
|
||||
target: "self",
|
||||
stackAmount: 0,
|
||||
effects: Object.freeze(effects),
|
||||
unsupportedEffects: Object.freeze(additionalEffects.map((sourceEffect, index) => (
|
||||
unsupported(index + 1, sourceEffect.auraType, sourceEffect.basePoints, sourceEffect.miscValue)
|
||||
))),
|
||||
blockInputs: Object.freeze({
|
||||
blockChanceAuraPercent: 0,
|
||||
criticalBlockChancePercent: 0,
|
||||
ignoresHitDirection: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const DETERRENCE_DISARM = effect(6, 185, -100);
|
||||
const DETERRENCE_UNKNOWN = effect(6, 27, 0);
|
||||
|
||||
const CONTRACTS: Readonly<Record<EnemyBlockAuraSourceProfile, Readonly<Record<number, EnemyBlockAuraContract>>>> = Object.freeze({
|
||||
wow335: Object.freeze({
|
||||
3419: blockChanceContract("wow335"),
|
||||
47294: criticalBlockContract("wow335", 47294, 20, [5]),
|
||||
47295: criticalBlockContract("wow335", 47295, 40, [10]),
|
||||
47296: criticalBlockContract("wow335", 47296, 60, [15]),
|
||||
67801: deterrenceContract("wow335", 67801, 5_000, [DETERRENCE_DISARM]),
|
||||
}),
|
||||
ascension: Object.freeze({
|
||||
3419: blockChanceContract("ascension"),
|
||||
47294: criticalBlockContract("ascension", 47294, 100, [4, 8]),
|
||||
47295: criticalBlockContract("ascension", 47295, 100, [7, 16]),
|
||||
47296: criticalBlockContract("ascension", 47296, 100, [10, 25]),
|
||||
67801: deterrenceContract("ascension", 67801, 60_000, [effect(6, 4, 0), DETERRENCE_UNKNOWN]),
|
||||
667801: deterrenceContract("ascension", 667801, 5_000, [DETERRENCE_DISARM, DETERRENCE_UNKNOWN]),
|
||||
1167801: deterrenceContract("ascension", 1167801, 5_000, [DETERRENCE_DISARM]),
|
||||
}),
|
||||
epoch: Object.freeze({
|
||||
3419: blockChanceContract("epoch"),
|
||||
47294: criticalBlockContract("epoch", 47294, 20, [5]),
|
||||
47295: criticalBlockContract("epoch", 47295, 40, [10]),
|
||||
47296: criticalBlockContract("epoch", 47296, 60, [15]),
|
||||
67801: deterrenceContract("epoch", 67801, 5_000, [DETERRENCE_DISARM]),
|
||||
}),
|
||||
});
|
||||
|
||||
interface ReviewedCriticalBlockRow {
|
||||
readonly criticalBlockChance: number;
|
||||
readonly spellModifierEffects: readonly {
|
||||
readonly basePoints: number;
|
||||
readonly miscValue: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
const REVIEWED_CRITICAL_BLOCK_ROWS: Readonly<Record<EnemyBlockAuraSourceProfile, Readonly<Record<number, ReviewedCriticalBlockRow>>>> = Object.freeze({
|
||||
wow335: Object.freeze({
|
||||
47294: Object.freeze({ criticalBlockChance: 20, spellModifierEffects: Object.freeze([{ basePoints: 5, miscValue: 7 }]) }),
|
||||
47295: Object.freeze({ criticalBlockChance: 40, spellModifierEffects: Object.freeze([{ basePoints: 10, miscValue: 7 }]) }),
|
||||
47296: Object.freeze({ criticalBlockChance: 60, spellModifierEffects: Object.freeze([{ basePoints: 15, miscValue: 7 }]) }),
|
||||
}),
|
||||
ascension: Object.freeze({
|
||||
47294: Object.freeze({ criticalBlockChance: 100, spellModifierEffects: Object.freeze([{ basePoints: 4, miscValue: 7 }, { basePoints: 8, miscValue: 3 }]) }),
|
||||
47295: Object.freeze({ criticalBlockChance: 100, spellModifierEffects: Object.freeze([{ basePoints: 7, miscValue: 7 }, { basePoints: 16, miscValue: 3 }]) }),
|
||||
47296: Object.freeze({ criticalBlockChance: 100, spellModifierEffects: Object.freeze([{ basePoints: 10, miscValue: 7 }, { basePoints: 25, miscValue: 3 }]) }),
|
||||
}),
|
||||
epoch: Object.freeze({
|
||||
47294: Object.freeze({ criticalBlockChance: 20, spellModifierEffects: Object.freeze([{ basePoints: 5, miscValue: 7 }]) }),
|
||||
47295: Object.freeze({ criticalBlockChance: 40, spellModifierEffects: Object.freeze([{ basePoints: 10, miscValue: 7 }]) }),
|
||||
47296: Object.freeze({ criticalBlockChance: 60, spellModifierEffects: Object.freeze([{ basePoints: 15, miscValue: 7 }]) }),
|
||||
}),
|
||||
});
|
||||
|
||||
/** Returns the reviewed Spell.dbc aura row for one supported source profile. */
|
||||
export function enemyBlockAuraContract(
|
||||
spellId: number,
|
||||
profile: EnemyBlockAuraSourceProfile,
|
||||
): EnemyBlockAuraContract | undefined {
|
||||
return CONTRACTS[profile]?.[spellId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an input-only aura fixture. It retains only aura 51/253/288 values;
|
||||
* callers must not use this as an imported full-spell definition when the
|
||||
* contract also has unsupported sibling effects.
|
||||
*/
|
||||
export function enemyBlockAuraInputDefinition(contract: EnemyBlockAuraContract): AuraDefinition {
|
||||
const tags: string[] = [];
|
||||
for (const sourceEffect of contract.effects) {
|
||||
if (sourceEffect.auraType === SPELL_AURA_MOD_BLOCK_PERCENT) {
|
||||
tags.push(auraInputTags.blockChance(sourceEffect.basePoints));
|
||||
} else if (sourceEffect.auraType === SPELL_AURA_MOD_BLOCK_CRIT_CHANCE) {
|
||||
tags.push(auraInputTags.criticalBlockChance(sourceEffect.basePoints));
|
||||
} else if (sourceEffect.auraType === SPELL_AURA_IGNORE_HIT_DIRECTION) {
|
||||
tags.push(auraInputTags.ignoreHitDirection);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
id: `enemy-source-spell-${contract.spellId}`,
|
||||
name: contract.name,
|
||||
disposition: contract.disposition,
|
||||
durationMs: contract.durationMs,
|
||||
stackBehavior: "refresh",
|
||||
utilityTags: Object.freeze(tags),
|
||||
});
|
||||
}
|
||||
|
||||
/** Builds an importable definition only when every source effect is represented. */
|
||||
export function supportedEnemyBlockAuraDefinition(
|
||||
contract: EnemyBlockAuraContract,
|
||||
): AuraDefinition | undefined {
|
||||
if (contract.unsupportedEffects.length > 0
|
||||
|| contract.effects.some((sourceEffect) => ![
|
||||
SPELL_AURA_MOD_BLOCK_PERCENT,
|
||||
SPELL_AURA_MOD_BLOCK_CRIT_CHANCE,
|
||||
SPELL_AURA_IGNORE_HIT_DIRECTION,
|
||||
].includes(sourceEffect.auraType))) return undefined;
|
||||
return enemyBlockAuraInputDefinition(contract);
|
||||
}
|
||||
|
||||
function sameEffectSignature(
|
||||
actual: EnemyAuraEffectSignature | undefined,
|
||||
expected: EnemyAuraEffectSignature,
|
||||
): boolean {
|
||||
return actual !== undefined
|
||||
&& actual.effectType === expected.effectType
|
||||
&& actual.auraType === expected.auraType
|
||||
&& actual.basePoints === expected.basePoints
|
||||
&& actual.mechanic === expected.mechanic
|
||||
&& actual.implicitTargetA === expected.implicitTargetA
|
||||
&& actual.implicitTargetB === expected.implicitTargetB
|
||||
&& actual.radius === expected.radius
|
||||
&& actual.miscValue === expected.miscValue
|
||||
&& actual.periodMs === expected.periodMs;
|
||||
}
|
||||
|
||||
function isReviewedCriticalBlockContract(contract: EnemyBlockAuraContract | null | undefined): boolean {
|
||||
if (!contract || typeof contract !== "object") return false;
|
||||
const row = REVIEWED_CRITICAL_BLOCK_ROWS[contract.profile]?.[contract.spellId];
|
||||
if (!row || !contract.blockInputs || !contract.signature || contract.name !== "Critical Block" || contract.durationMs !== null
|
||||
|| contract.disposition !== "buff" || contract.target !== "self" || contract.stackAmount !== 0
|
||||
|| contract.blockInputs.blockChanceAuraPercent !== 0
|
||||
|| contract.blockInputs.criticalBlockChancePercent !== row.criticalBlockChance
|
||||
|| contract.blockInputs.ignoresHitDirection !== false) return false;
|
||||
|
||||
const expectedEffects = [
|
||||
effect(6, SPELL_AURA_MOD_BLOCK_CRIT_CHANCE, row.criticalBlockChance),
|
||||
...row.spellModifierEffects.map(({ basePoints, miscValue }) => effect(6, 107, basePoints, miscValue)),
|
||||
];
|
||||
const sourceEffects = contract.signature.effects;
|
||||
if (contract.signature.rangeIndex !== 1 || contract.signature.schoolMask !== 1
|
||||
|| contract.signature.damageClass !== 0 || contract.signature.durationMs !== -1
|
||||
|| !Array.isArray(sourceEffects) || sourceEffects.length !== expectedEffects.length
|
||||
|| !Array.isArray(contract.effects) || contract.effects.length !== expectedEffects.length
|
||||
|| !Array.isArray(contract.unsupportedEffects)
|
||||
|| contract.unsupportedEffects.length !== expectedEffects.length - 1) return false;
|
||||
|
||||
for (let index = 0; index < expectedEffects.length; index += 1) {
|
||||
if (!sameEffectSignature(sourceEffects[index], expectedEffects[index])
|
||||
|| !sameEffectSignature(contract.effects[index], expectedEffects[index])) return false;
|
||||
}
|
||||
return row.spellModifierEffects.every((sourceEffect, index) => {
|
||||
const unsupportedEffect = contract.unsupportedEffects[index];
|
||||
return unsupportedEffect?.effectIndex === index + 1
|
||||
&& unsupportedEffect.auraType === 107
|
||||
&& unsupportedEffect.basePoints === sourceEffect.basePoints
|
||||
&& unsupportedEffect.miscValue === sourceEffect.miscValue;
|
||||
});
|
||||
}
|
||||
|
||||
function isOrdinaryCreatureSelfTargetProof(
|
||||
proof: EnemyNpcCriticalBlockImportProof | null | undefined,
|
||||
): boolean {
|
||||
if (!proof || typeof proof !== "object" || proof.defenderKind !== "ordinary-creature"
|
||||
|| proof.isNativeDefender !== false || proof.isSummonedDefender !== false
|
||||
|| proof.isCharacterDefender !== false
|
||||
|| proof.aura107Applicability !== "azerothcore-e822d718-default-spellmod-path") return false;
|
||||
const { defenderId, casterId, targetId } = proof;
|
||||
return typeof defenderId === "string" && defenderId.trim().length > 0
|
||||
&& defenderId === casterId && casterId === targetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports Critical Block as a full aura only for a reviewed ordinary Creature
|
||||
* self-cast. Aura 107 remains in the checked raw signature and is treated as a
|
||||
* no-op only under the pinned AzerothCore default spell-mod applicability.
|
||||
* The generic importer intentionally remains fail-closed for these rows.
|
||||
*/
|
||||
export function ordinaryCreatureSelfTargetCriticalBlockAuraDefinition(
|
||||
contract: EnemyBlockAuraContract | null | undefined,
|
||||
proof: EnemyNpcCriticalBlockImportProof | null | undefined,
|
||||
): AuraDefinition | undefined {
|
||||
if (!isReviewedCriticalBlockContract(contract) || !isOrdinaryCreatureSelfTargetProof(proof)) return undefined;
|
||||
return enemyBlockAuraInputDefinition(contract!);
|
||||
}
|
||||
|
||||
function decodeAuraTag(value: string): { readonly auraType: number; readonly basePoints: number } | null {
|
||||
if (!value.startsWith(tagPrefix)) return null;
|
||||
const match = /^(\d+):(-?\d+)$/.exec(value.slice(tagPrefix.length));
|
||||
if (!match) return null;
|
||||
const auraType = Number(match[1]);
|
||||
const basePoints = Number(match[2]);
|
||||
if (!Number.isSafeInteger(auraType) || !Number.isSafeInteger(basePoints)) return null;
|
||||
return { auraType, basePoints };
|
||||
}
|
||||
|
||||
/** Sums block-related inputs from active, target-local enemy source auras. */
|
||||
export function resolveEnemyNpcBlockAuraInputs(input: {
|
||||
readonly auras: readonly ActiveAura[];
|
||||
readonly targetId: string;
|
||||
readonly now: number;
|
||||
}): EnemyNpcBlockAuraInputs | null {
|
||||
if (typeof input.targetId !== "string" || !input.targetId
|
||||
|| !Number.isFinite(input.now) || !Array.isArray(input.auras)) return null;
|
||||
|
||||
let blockChanceAuraPercent = 0;
|
||||
let criticalBlockChanceAuraPercent = 0;
|
||||
let ignoresHitDirection = false;
|
||||
|
||||
for (const aura of input.auras) {
|
||||
if (aura.targetId !== input.targetId || (aura.expiresAt !== null && aura.expiresAt <= input.now)) continue;
|
||||
const tags = aura.definition.utilityTags ?? [];
|
||||
let carriesBlockChanceTag = false;
|
||||
for (const value of tags) {
|
||||
const decoded = decodeAuraTag(value);
|
||||
if (!decoded) continue;
|
||||
if (decoded.auraType === SPELL_AURA_MOD_BLOCK_PERCENT) {
|
||||
carriesBlockChanceTag = true;
|
||||
blockChanceAuraPercent += decoded.basePoints;
|
||||
} else if (decoded.auraType === SPELL_AURA_MOD_BLOCK_CRIT_CHANCE) {
|
||||
criticalBlockChanceAuraPercent += decoded.basePoints;
|
||||
} else if (decoded.auraType === SPELL_AURA_IGNORE_HIT_DIRECTION) {
|
||||
ignoresHitDirection = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Existing 3419 enemy-source aura definitions predate these utility tags;
|
||||
// their source-backed flat block stat still supplies the same aura-51 input.
|
||||
if (!carriesBlockChanceTag) {
|
||||
for (const modifier of aura.definition.modifiers ?? []) {
|
||||
if (modifier.kind === "stat" && modifier.stat === "block" && modifier.operation === "flat") {
|
||||
blockChanceAuraPercent += modifier.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(blockChanceAuraPercent) || !Number.isFinite(criticalBlockChanceAuraPercent)) return null;
|
||||
return Object.freeze({
|
||||
blockChanceAuraPercent,
|
||||
// Aura amounts are integer source values; roll_chance_i accepts an int.
|
||||
criticalBlockChancePercent: Math.trunc(criticalBlockChanceAuraPercent),
|
||||
ignoresHitDirection,
|
||||
});
|
||||
}
|
||||
|
||||
/** Mirrors the integer chance argument consumed by AzerothCore roll_chance_i. */
|
||||
export function enemyNpcCriticalBlockChanceForRoll(chancePercent: number): number | null {
|
||||
if (!Number.isFinite(chancePercent)) return null;
|
||||
return Math.trunc(chancePercent);
|
||||
}
|
||||
|
||||
/** Doubles ordinary creature block value only after the independent crit-block roll succeeds. */
|
||||
export function enemyNpcBlockValueAfterCritical(
|
||||
blockValue: number,
|
||||
criticalBlockSucceeded: boolean,
|
||||
): number | null {
|
||||
if (!Number.isSafeInteger(blockValue) || blockValue < 0
|
||||
|| typeof criticalBlockSucceeded !== "boolean") return null;
|
||||
const result = criticalBlockSucceeded ? blockValue * 2 : blockValue;
|
||||
return Number.isSafeInteger(result) ? result : null;
|
||||
}
|
||||
Reference in new issue
Block a user