Files
healer-man/src/game/enemyBlockAuraRuntime.integration.test.ts
2026-10-06 22:25:34 -04:00

204 lines
7.0 KiB
TypeScript

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<string, CombatPosition>();
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<Record<string, CombatPosition>>): 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);
});
});