import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { PLAYER_AGGRO_ID } from "./aggro"; import { applyAura } from "./combatAuras"; import { PLAYER_AURA_ENTITY_ID, stableCombatUnit, useCombatStore } from "./combatStore"; import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial"; import { enemyBlockAuraContract, enemyBlockAuraInputDefinition } from "./enemyBlockAuras"; import { clearMobRuntimeRegistry, registerMobRuntime } from "./mobRuntimeRegistry"; import type { CombatPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; const MOB_ID = "enemy-block-aura-runtime"; const PARTY_ID = "enemy-block-aura-party"; const BLOCK_SEED = seedWithOutcomeBelow("npc-block-aura-block", 0.03); const HIT_SEED = seedWithOutcomeBelow("npc-block-aura-hit", 0.9); const positions = new Map(); function seedWithOutcomeBelow(prefix: string, maximum: number): string { for (let index = 0; index < 10_000; index += 1) { const seed = `${prefix}:${index}`; if (stableCombatUnit(`${seed}:outcome`) < maximum) return seed; } throw new Error(`Could not find a deterministic combat seed below ${maximum}.`); } function setSpatialActors(entries: Readonly>): void { positions.clear(); for (const [id, position] of Object.entries(entries)) positions.set(id, position); registerCombatSpatialProvider({ actor: (id) => { const position = positions.get(id); return position ? { id, position, yaw: 0 } : null; }, hasLineOfSight: () => true, projectGroundPoint: (position) => position, }); } function addInputAura( targetId: string, spellId: 3419 | 47296 | 667801, now: number, ): void { const contract = enemyBlockAuraContract(spellId, "ascension"); if (!contract) throw new Error(`Missing source block aura ${spellId}.`); const definition = enemyBlockAuraInputDefinition(contract); const current = useCombatStore.getState().auras; useCombatStore.setState({ auras: applyAura(current, { definition, sourceId: targetId, targetId, now }).auras, }); } function registerEnemy( id: string, entry: number | null, position: CombatPosition, runewakerNpcId?: number, ): void { useCombatStore.getState().registerMob(id, { name: id, ...(entry === null ? {} : { serverEntry: entry }), ...(runewakerNpcId === undefined ? {} : { runewakerNpcId }), level: 30, maxHealth: 100_000, xpReward: 0, attacks: [], }); registerMobRuntime(id, position, 0); } function hit( targetId: string, actorId: string, now: number, seed: string, options: { readonly canBlock?: boolean; readonly amount?: number } = {}, ) { return useCombatStore.getState().damageMob( targetId, options.amount ?? 39, now, { actorId, role: "damage", abilityId: "enemy-block-aura-integration" }, "physical", { attackKind: "melee", seed, rules: { isAutoAttack: true, canMiss: false, canCrit: false, canDodge: false, canParry: false, ignoreArmor: true, ...(options.canBlock === undefined ? {} : { canBlock: options.canBlock }), }, }, ); } function addPartyAttacker(): string { usePartyStore.getState().initializeParty( PARTY_ID, 30, useCombatStore.getState().health, "damage", 2, "wow", ); const member = createConfiguredPartyMember({ name: "Block aura test attacker", classId: "warrior", statRuleset: "wow", role: "damage", specialization: "Integration attacker", talentTreeId: "", talentRanks: {}, }, 30, 0, PARTY_ID); usePartyStore.setState({ members: [member] }); useCombatStore.getState().synchronizeActors(); return member.id; } beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetCombatSpatialProvider(); positions.clear(); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 30, xp: 0 }); useCombatStore.getState().setPlayerPosition([0, 0, -2]); usePartyStore.getState().initializeParty( PARTY_ID, 30, useCombatStore.getState().health, "damage", 2, "wow", ); }); afterEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetCombatSpatialProvider(); }); describe("ordinary NPC block aura runtime inputs", () => { it("lets rear player and party auto-attacks block under aura 288 and doubles the source block value under aura 253", () => { registerEnemy(MOB_ID, 1666, [0, 0, 0]); setSpatialActors({ [PLAYER_AURA_ENTITY_ID]: [0, 0, -2] }); addInputAura(MOB_ID, 3419, 20_000); addInputAura(MOB_ID, 47296, 20_000); const rearWithoutDirectionAura = hit(MOB_ID, PLAYER_AGGRO_ID, 20_001, HIT_SEED); expect(rearWithoutDirectionAura.combatResult).toMatchObject({ outcome: "hit", attackTable: { blockPercent: 0 }, }); // The input-only fixture reads aura 288 from Deterrence without importing // its unrelated DBC sibling effects as a complete spell definition. addInputAura(MOB_ID, 667801, 20_002); const playerBlock = hit(MOB_ID, PLAYER_AGGRO_ID, 20_003, BLOCK_SEED); expect(playerBlock.combatResult).toMatchObject({ outcome: "block", blocked: true, attackTable: { blockPercent: 80 }, blockedAmount: 30, finalDamage: 9, criticalBlockChancePercent: 100, criticalBlocked: true, }); expect(playerBlock.combatResult?.criticalBlockRoll).toBeGreaterThanOrEqual(0); expect(playerBlock.combatResult?.criticalBlockRoll).toBeLessThan(100); const partyAttackerId = addPartyAttacker(); positions.set(partyAttackerId, [0, 0, -2]); const partyBlock = hit(MOB_ID, partyAttackerId, 20_004, BLOCK_SEED); expect(partyBlock.combatResult).toMatchObject({ outcome: "block", attackTable: { blockPercent: 80 }, blockedAmount: 30, criticalBlocked: true, }); const blockingDisabled = hit(MOB_ID, PLAYER_AGGRO_ID, 20_005, BLOCK_SEED, { canBlock: false }); expect(blockingDisabled.combatResult).toMatchObject({ outcome: "hit", attackTable: { blockPercent: 0 }, blocked: false, }); const afterDirectionAuraExpiry = hit(MOB_ID, PLAYER_AGGRO_ID, 25_002, HIT_SEED); expect(afterDirectionAuraExpiry.combatResult).toMatchObject({ outcome: "hit", attackTable: { blockPercent: 0 }, blocked: false, }); expect(afterDirectionAuraExpiry.combatResult?.criticalBlocked).toBeUndefined(); }); it("does not give native RoM defenders ordinary NPC aura block inputs", () => { const nativeId = "native-block-aura-defender"; registerEnemy(nativeId, 1666, [4, 0, 0], 101_456); setSpatialActors({ [PLAYER_AURA_ENTITY_ID]: [4, 0, 2] }); addInputAura(nativeId, 3419, 30_000); addInputAura(nativeId, 47296, 30_000); addInputAura(nativeId, 667801, 30_000); const native = hit(nativeId, PLAYER_AGGRO_ID, 30_001, BLOCK_SEED); expect(native.combatResult?.attackTable.blockPercent).toBe(0); expect(native.combatResult?.blocked).toBe(false); }); });