import { classifyEnemyNpcSpellBlockPhase, enemyNpcSourceSpellBlockMetadata, type EnemyNpcSpellDefenseMetadata, type EnemyNpcSpellSourceProfile, } from "./enemyBlocking"; import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog"; export type EnemyUtilitySpellCasterKind = "player" | "party" | "enemy-npc" | "rom-native" | "unknown"; export type EnemyUtilitySpellTargetKind = "ordinary-npc" | "character" | "native-rom" | "summon" | "unknown"; export type EnemyUtilitySpellHitOutcome = "hit" | "crit" | "miss" | "dodge" | "parry" | "block" | "resist"; export type EnemyUtilitySpellBlockDisposition = "apply" | "blocked" | "avoided" | "fallback"; export interface EnemyUtilitySpellBlockSourceContract { readonly spellId: 72 | 1766 | 6552; readonly sourceProfile: "wow335"; readonly abilityId: string; /** Raw imported chain ID, accepted only with the same exact source projection. */ readonly sourceAbilityId?: string; readonly classId: "warrior" | "rogue"; readonly abilityName: "Shield Bash" | "Kick" | "Pummel"; readonly spellDefense: EnemyNpcSpellDefenseMetadata; /** Relevant source fields from the exact selected Spell.dbc rank. */ readonly dbcEffects: readonly EnemyUtilitySpellBlockDbcEffect[]; /** Exact projection produced by the playable catalog for this selected rank. */ readonly projectedEffects: readonly AbilityEffect[]; } interface EnemyUtilitySpellBlockDbcEffect { readonly index: number; readonly effectType: number; readonly auraType: number; readonly mechanic: number; readonly targetA: number; readonly targetB: number; readonly basePoints: number; readonly triggerSpellId: number; } interface EnemyUtilitySpellBlockRankContract { readonly spellId: EnemyUtilitySpellBlockSourceContract["spellId"]; readonly abilityId: string; readonly sourceAbilityId?: string; readonly classId: "warrior" | "rogue"; readonly abilityName: EnemyUtilitySpellBlockSourceContract["abilityName"]; readonly dbcEffects: readonly EnemyUtilitySpellBlockDbcEffect[]; readonly projectedEffects: readonly AbilityEffect[]; } const UTILITY_SPELL_BLOCK_RANKS: readonly EnemyUtilitySpellBlockRankContract[] = Object.freeze([ Object.freeze({ spellId: 72, abilityId: "wow335-warrior-shield-bash", classId: "warrior", abilityName: "Shield Bash", dbcEffects: Object.freeze([ Object.freeze({ index: 0, effectType: 68, auraType: 0, mechanic: 26, targetA: 6, targetB: 0, basePoints: 0, triggerSpellId: 0 }), Object.freeze({ index: 1, effectType: 64, auraType: 0, mechanic: 0, targetA: 6, targetB: 0, basePoints: 0, triggerSpellId: 29703 }), ]), projectedEffects: Object.freeze([ Object.freeze({ kind: "interrupt", lockoutMs: 6_000 }), Object.freeze({ kind: "trigger-spell", spellId: 29_703 }), ]), }), Object.freeze({ spellId: 1766, abilityId: "rogue-kick", sourceAbilityId: "wow335-rogue-kick", classId: "rogue", abilityName: "Kick", dbcEffects: Object.freeze([ Object.freeze({ index: 0, effectType: 68, auraType: 0, mechanic: 26, targetA: 6, targetB: 0, basePoints: 1, triggerSpellId: 0 }), Object.freeze({ index: 1, effectType: 0, auraType: 0, mechanic: 0, targetA: 0, targetB: 0, basePoints: 0, triggerSpellId: 0 }), ]), projectedEffects: Object.freeze([ Object.freeze({ kind: "interrupt", lockoutMs: 5_000 }), ]), }), Object.freeze({ spellId: 6552, abilityId: "wow335-warrior-pummel", classId: "warrior", abilityName: "Pummel", dbcEffects: Object.freeze([ Object.freeze({ index: 0, effectType: 68, auraType: 0, mechanic: 26, targetA: 6, targetB: 0, basePoints: 1, triggerSpellId: 0 }), Object.freeze({ index: 1, effectType: 0, auraType: 0, mechanic: 0, targetA: 0, targetB: 0, basePoints: 0, triggerSpellId: 0 }), ]), projectedEffects: Object.freeze([ Object.freeze({ kind: "interrupt", lockoutMs: 4_000 }), ]), }), ]); /** * Returns an audited utility full-block contract for an exact WoW 3.3.5a * selected rank. Other ranks and source profiles remain on their current path. */ export function enemyUtilitySpellBlockSourceContract( spellId: number | undefined, sourceProfile: EnemyNpcSpellSourceProfile | undefined, ): EnemyUtilitySpellBlockSourceContract | undefined { if (sourceProfile !== "wow335") return undefined; const rank = UTILITY_SPELL_BLOCK_RANKS.find((candidate) => candidate.spellId === spellId); if (!rank) return undefined; const spellDefense = enemyNpcSourceSpellBlockMetadata(rank.spellId, "wow335"); if (!spellDefense) return undefined; return Object.freeze({ spellId: rank.spellId, sourceProfile: "wow335", abilityId: rank.abilityId, ...(rank.sourceAbilityId ? { sourceAbilityId: rank.sourceAbilityId } : {}), classId: rank.classId, abilityName: rank.abilityName, spellDefense, dbcEffects: rank.dbcEffects, projectedEffects: rank.projectedEffects, }); } function matchesUtilityBlockMetadata( metadata: EnemyNpcSpellDefenseMetadata | undefined, contract: EnemyUtilitySpellBlockSourceContract, reconstructSourceProof = false, ): boolean { if (!metadata) return false; const expected = contract.spellDefense; if (metadata.damageClass !== expected.damageClass || metadata.schoolMask !== expected.schoolMask || metadata.attributes0 !== expected.attributes0 || metadata.attributes3 !== expected.attributes3 || metadata.directDamageFromEffects !== expected.directDamageFromEffects || metadata.sourceProfile !== expected.sourceProfile) return false; const knownCustom = expected.customAttributeEvidence; if (knownCustom) { if (metadata.customAttributes !== undefined && ((metadata.customAttributes & knownCustom.knownMask) >>> 0) !== knownCustom.value) return false; const evidence = metadata.customAttributeEvidence; if (evidence && ((evidence.knownMask & knownCustom.knownMask) !== knownCustom.knownMask || ((evidence.value & knownCustom.knownMask) >>> 0) !== knownCustom.value)) return false; } const withSourceProof = reconstructSourceProof && metadata.customAttributes === undefined && metadata.customAttributeEvidence === undefined && knownCustom ? { ...metadata, customAttributeEvidence: knownCustom } : metadata; return classifyEnemyNpcSpellBlockPhase(withSourceProof) === "hit-table"; } function stableSignature(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableSignature).join(",")}]`; if (value && typeof value === "object") { const entries = Object.entries(value as Record) .sort(([left], [right]) => left.localeCompare(right)); return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableSignature(entry)}`).join(",")}}`; } return JSON.stringify(value) ?? "undefined"; } /** * Binds the spell-block contract to the actual selected playable catalog row. * Matching a spell ID alone cannot authorize another class, source, wrapper, * or modified effect list to borrow this source proof. */ export function enemyUtilitySpellBlockAbilityContract( ability: AbilityDefinition | undefined, ): EnemyUtilitySpellBlockSourceContract | undefined { if (!ability) return undefined; const contract = enemyUtilitySpellBlockSourceContract( ability.dbcSpellId, ability.wowSpellDefense?.sourceProfile, ); if (!contract || (ability.id !== contract.abilityId && ability.id !== contract.sourceAbilityId) || ability.classId !== contract.classId || ability.name !== contract.abilityName || ability.source !== "wow335-progression" || ability.dbcSpellId !== contract.spellId || !matchesUtilityBlockMetadata(ability.wowSpellDefense, contract, true) || stableSignature(ability.effects) !== stableSignature(contract.projectedEffects)) return undefined; return contract; } /** * Resolves whether one already-computed hit-table result suppresses the * utility effects for its exact cast recipient. It does not roll a hit or * block, and it does not widen the ordinary NPC defender rules. */ export function enemyUtilitySpellBlockDisposition(input: { readonly spellId: number | undefined; readonly sourceProfile: EnemyNpcSpellSourceProfile | undefined; readonly spellDefense: EnemyNpcSpellDefenseMetadata | undefined; readonly casterKind: EnemyUtilitySpellCasterKind; readonly targetKind: EnemyUtilitySpellTargetKind; readonly outcome: EnemyUtilitySpellHitOutcome | undefined; }): EnemyUtilitySpellBlockDisposition { const contract = enemyUtilitySpellBlockSourceContract(input.spellId, input.sourceProfile); if (!contract || input.casterKind !== "player" && input.casterKind !== "party" || input.targetKind !== "ordinary-npc" || !matchesUtilityBlockMetadata(input.spellDefense, contract)) return "fallback"; if (input.outcome === "block") return "blocked"; if (input.outcome === "miss" || input.outcome === "dodge" || input.outcome === "parry" || input.outcome === "resist") return "avoided"; if (input.outcome === "hit" || input.outcome === "crit") return "apply"; return "fallback"; } /** Leaves unsupported calls on their existing path; a resolved miss or full block has no utility effects. */ export function enemyUtilityEffectsAfterBlock( effects: readonly T[], disposition: EnemyUtilitySpellBlockDisposition, ): readonly T[] { return disposition === "blocked" || disposition === "avoided" ? [] : effects; }