Files
healer-man/src/game/combatAuraRuntime.test.ts
T
2026-09-17 09:13:15 -04:00

298 lines
12 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { applyAura, type AuraDefinition } from "./combatAuras";
import { useCombatStore } from "./combatStore";
import { abilityById } from "./abilityCatalog";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
function registerTarget(id = "target", distance = 2): void {
useCombatStore.getState().registerMob(id, { name: id, maxHealth: 1_000, xpReward: 0 });
setMobPosition(id, [distance, 0, 0]);
expect(useCombatStore.getState().selectMob(id)).toBe(true);
}
describe("named aura combat runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
usePartyStore.getState().initializeParty("aura-runtime", 80, 2_000);
usePartyStore.getState().selectMember(null);
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
});
it("applies and expires an imported named player buff", () => {
const now = 10_000;
const baseHealth = useCombatStore.getState().health;
const baseMaxHealth = useCombatStore.getState().maxHealth;
const result = useCombatStore.getState().castAbility(
"wow335-priest-power-word-fortitude",
undefined,
now,
);
expect(result.ok).toBe(true);
const fortitude = useCombatStore.getState().auras.find((aura) => (
aura.definition.name.startsWith("Power Word: Fortitude (DBC")
));
expect(fortitude).toMatchObject({
sourceId: PLAYER_AGGRO_ID,
targetId: PLAYER_AGGRO_ID,
expiresAt: now + 1_800_000,
definition: {
disposition: "buff",
dispelCategory: "magic",
icon: useCombatStore.getState().abilities.find((ability) => (
ability.id === "wow335-priest-power-word-fortitude"
))?.icon,
},
});
expect(useCombatStore.getState().maxHealth).toBeGreaterThan(baseMaxHealth);
expect(useCombatStore.getState().health - baseHealth).toBe(
useCombatStore.getState().maxHealth - baseMaxHealth,
);
useCombatStore.getState().tick(0, now + 1_800_000);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === fortitude?.definition.id)).toBe(false);
expect(useCombatStore.getState()).toMatchObject({ health: baseHealth, maxHealth: baseMaxHealth });
});
it("grants learned long-term stat buffs to the full party on load", () => {
useCombatStore.getState().initializeCharacter({
classId: "priest",
specializationId: "priest:holy",
level: 8,
inventory: [],
equipment: {},
});
usePartyStore.getState().initializeParty("default-party-buffs", 8, useCombatStore.getState().health);
useCombatStore.getState().synchronizeActors();
const playerMaxHealth = useCombatStore.getState().maxHealth;
const memberMaxHealth = new Map(usePartyStore.getState().members.map((member) => [member.id, member.maxHealth]));
expect(playerMaxHealth).toBe(407);
useCombatStore.getState().applyDefaultPartyBuffs(20_000);
const state = useCombatStore.getState();
const targetIds = [PLAYER_AGGRO_ID, ...usePartyStore.getState().members.map((member) => member.id)];
for (const targetId of targetIds) {
expect(state.auras).toContainEqual(expect.objectContaining({
targetId,
definition: expect.objectContaining({ exclusiveGroup: "default-party-buff:fortitude" }),
}));
}
expect(state.maxHealth).toBe(437);
for (const member of usePartyStore.getState().members) {
expect(member.maxHealth).toBeGreaterThan(memberMaxHealth.get(member.id)!);
}
const buffedPlayerMaxHealth = state.maxHealth;
expect(state.auras.filter((aura) => (
aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.exclusiveGroup === "default-party-buff:fortitude"
))).toHaveLength(1);
expect(state.castAbility("wow335-priest-power-word-fortitude", undefined, 21_000).ok).toBe(true);
expect(useCombatStore.getState().maxHealth).toBe(buffedPlayerMaxHealth);
expect(useCombatStore.getState().auras.filter((aura) => (
aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.exclusiveGroup === "default-party-buff:fortitude"
))).toHaveLength(1);
});
it("enters the dungeon with spec maintenance and permanent pets already prepared", () => {
const now = 30_000;
useCombatStore.getState().initializeCharacter({
classId: "hunter",
specializationId: "hunter:beast-mastery",
role: "damage",
level: 80,
inventory: [],
equipment: {},
});
const warlock = createConfiguredPartyMember({
name: "Demo",
classId: "warlock",
specialization: "Demonology",
specializationId: "warlock:demonology",
talentTreeId: "warlock-demonology",
talentRanks: runtimePartyTalentRanksForLevel("warlock", "Demonology", 80),
role: "damage",
}, 80, 0, "precombat-preparation");
usePartyStore.setState({ members: [warlock], playerRole: "damage" });
useCombatStore.getState().synchronizeActors();
const playerResource = useCombatStore.getState().resource;
const companionResource = warlock.resource;
useCombatStore.getState().applyPrecombatPreparation(now);
const prepared = useCombatStore.getState();
expect(prepared.auras).toContainEqual(expect.objectContaining({
targetId: PLAYER_AGGRO_ID,
definition: expect.objectContaining({ exclusiveGroup: "hunter:aspect" }),
}));
expect(prepared.auras).toContainEqual(expect.objectContaining({
targetId: warlock.id,
definition: expect.objectContaining({ exclusiveGroup: "warlock:armor" }),
}));
expect(prepared.summons.some((summon) => summon.ownerActorId === PLAYER_AGGRO_ID
&& abilityById(summon.sourceAbilityId)?.name === "Call Pet")).toBe(true);
expect(prepared.summons.some((summon) => summon.ownerActorId === warlock.id
&& abilityById(summon.sourceAbilityId)?.name === "Summon Felguard")).toBe(true);
expect(prepared.resource).toBe(playerResource);
expect(prepared.globalCooldownEndsAt).toBe(0);
expect(usePartyStore.getState().members[0]).toMatchObject({
resource: companionResource,
globalCooldownEndsAt: 0,
cooldowns: {},
});
const summonIds = prepared.summons.map((summon) => summon.id);
useCombatStore.getState().applyPrecombatPreparation(now + 1_000);
expect(useCombatStore.getState().summons.map((summon) => summon.id)).toEqual(summonIds);
});
it("consumes a named absorb before player health", () => {
const now = Date.now();
expect(useCombatStore.getState().castAbility(
"wow335-priest-power-word-shield",
undefined,
now,
).ok).toBe(true);
const beforeHealth = useCombatStore.getState().health;
const beforeAbsorb = useCombatStore.getState().auras
.flatMap((aura) => aura.absorbs)
.reduce((total, absorb) => total + absorb.remaining, 0);
expect(beforeAbsorb).toBeGreaterThan(0);
expect(useCombatStore.getState().auras.find((aura) => aura.absorbs.length)?.definition.icon).toBe(
useCombatStore.getState().abilities.find((ability) => (
ability.id === "wow335-priest-power-word-shield"
))?.icon,
);
const healthDamage = useCombatStore.getState().damagePlayer(20, "shadow", 80);
const afterAbsorb = useCombatStore.getState().auras
.flatMap((aura) => aura.absorbs)
.reduce((total, absorb) => total + absorb.remaining, 0);
expect(healthDamage).toBe(0);
expect(useCombatStore.getState().health).toBe(beforeHealth);
expect(afterAbsorb).toBeLessThan(beforeAbsorb);
});
it("cleanses a matching debuff without removing an unrelated category", () => {
useCombatStore.getState().initializeCharacter({ classId: "paladin", level: 80 });
const magic: AuraDefinition = {
id: "test-magic-debuff",
name: "Magic Debuff",
disposition: "debuff",
durationMs: 30_000,
dispelCategory: "magic",
};
const curse: AuraDefinition = {
id: "test-curse-debuff",
name: "Curse Debuff",
disposition: "debuff",
durationMs: 30_000,
dispelCategory: "curse",
};
let auras = applyAura([], { definition: magic, sourceId: "enemy", targetId: PLAYER_AGGRO_ID, now: 1_000 }).auras;
auras = applyAura(auras, { definition: curse, sourceId: "enemy", targetId: PLAYER_AGGRO_ID, now: 1_001 }).auras;
useCombatStore.setState({ auras });
expect(useCombatStore.getState().castAbility("wow335-paladin-cleanse", undefined, 2_000).ok).toBe(true);
expect(useCombatStore.getState().auras.map((aura) => aura.definition.id)).toEqual(["test-curse-debuff"]);
});
it("interrupts an active enemy cast and applies its school lockout", () => {
useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 80 });
registerTarget("caster");
useCombatStore.setState((state) => ({
mobs: {
...state.mobs,
caster: {
...state.mobs.caster,
activeCast: {
attackId: "fireball",
name: "Fireball",
school: "fire",
interruptible: true,
targetActorId: PLAYER_AGGRO_ID,
sourcePosition: [0, 0, 0],
targetPosition: [1, 0, 0],
sourceYaw: Math.PI / 2,
startedAt: 1_000,
completesAt: 6_000,
},
},
},
}));
expect(useCombatStore.getState().castAbility("rogue-kick", undefined, 2_000).ok).toBe(true);
expect(useCombatStore.getState().mobs.caster.activeCast).toBeNull();
expect(useCombatStore.getState().mobs.caster.schoolLockedUntil.fire).toBeGreaterThanOrEqual(7_000);
});
it("executes a guaranteed aura proc as the real triggered spell", () => {
useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 80 });
registerTarget("proc-target");
const procAura: AuraDefinition = {
id: "test-triggered-fear",
name: "Triggered Fear",
disposition: "buff",
durationMs: null,
procs: [{
id: "test-triggered-fear-proc",
triggers: "cast",
chance: 1,
action: { kind: "custom", id: "wow335-trigger-spell", data: { spellId: 5782 } },
}],
};
useCombatStore.setState({
auras: applyAura([], {
definition: procAura,
sourceId: PLAYER_AGGRO_ID,
targetId: PLAYER_AGGRO_ID,
now: 1_000,
}).auras,
});
expect(useCombatStore.getState().castAbility("warlock-corruption", undefined, 2_000).ok).toBe(true);
expect(useCombatStore.getState().mobs["proc-target"].statuses).toContainEqual({
kind: "fear",
sourceAbilityId: "warlock-fear",
endsAt: 8_000,
});
});
it("purges the named and legacy views of a real enemy magic buff", () => {
useCombatStore.getState().initializeCharacter({ classId: "shaman", level: 80 });
registerTarget("buffed-enemy");
const enemyBuff: AuraDefinition = {
id: "enemy:buffed-enemy:arcane-empowerment",
name: "Arcane Empowerment",
disposition: "buff",
durationMs: 30_000,
dispelCategory: "magic",
};
useCombatStore.setState((state) => ({
auras: applyAura(state.auras, {
definition: enemyBuff,
sourceId: "buffed-enemy",
targetId: "buffed-enemy",
now: 1_000,
}).auras,
mobs: {
...state.mobs,
"buffed-enemy": {
...state.mobs["buffed-enemy"],
auras: [{ id: "arcane-empowerment", sourceAbilityId: "enemy-buff", endsAt: 31_000 }],
},
},
}));
expect(useCombatStore.getState().castAbility("wow335-shaman-purge", undefined, 2_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === enemyBuff.id)).toBe(false);
expect(useCombatStore.getState().mobs["buffed-enemy"].auras).toEqual([]);
});
});