forked from phenom/healer-man
465 lines
19 KiB
TypeScript
465 lines
19 KiB
TypeScript
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,
|
|
}),
|
|
});
|
|
}
|
|
|
|
// Aura 185 is MOD_ATTACKER_RANGED_HIT_CHANCE; it is not a Disarm aura.
|
|
const DETERRENCE_RANGED_HIT_PENALTY = 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_RANGED_HIT_PENALTY]),
|
|
}),
|
|
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_RANGED_HIT_PENALTY, DETERRENCE_UNKNOWN]),
|
|
1167801: deterrenceContract("ascension", 1167801, 5_000, [DETERRENCE_RANGED_HIT_PENALTY]),
|
|
}),
|
|
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_RANGED_HIT_PENALTY]),
|
|
}),
|
|
});
|
|
|
|
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;
|
|
}
|