Files
healer-man/src/game/wow335DeathKnightMarkOfBlood.test.ts
T
2026-09-30 09:57:13 -04:00

269 lines
10 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import type { OnlineSessionPlayer } from "../app/onlineSessionTypes";
import { installCombatClock } from "./combatClock";
import { createDeathKnightRunes } from "./combatActors";
import { useCombatStore, PLAYER_AURA_ENTITY_ID, type CombatSummon } from "./combatStore";
import { usePartyStore } from "./partyStore";
import { configureOnlineSessionRuntime, onlinePlayerActorId } from "./onlineSessionRuntime";
import {
WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG,
WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID,
} from "./wow335DeathKnightMarkOfBloodRuntime";
let now = 1_000;
let restoreClock: (() => void) | undefined;
function markAbility() {
const ability = useCombatStore.getState().abilities.find((candidate) => candidate.dbcSpellId === 49005);
if (!ability) throw new Error("Missing generated Death Knight Mark of Blood spell.");
return ability;
}
function registerAttacker(id: string) {
useCombatStore.getState().registerMob(id, {
name: id,
maxHealth: 100_000,
xpReward: 0,
attacks: [{
id: `${id}-strike`,
name: "Strike",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 5,
cooldownMs: 1_000,
damageMultiplier: 0.01,
}],
});
setMobPosition(id, [2, 0, 0]);
}
function castMark() {
const ability = markAbility();
const result = useCombatStore.getState().castAbility(ability.id, { now });
expect(result.ok).toBe(true);
return ability;
}
function remoteVictim(health = 9_000): OnlineSessionPlayer {
return {
accountId: "mark-remote-victim",
username: "Mark victim",
character: {
id: "mark-remote-character",
name: "Mark victim",
classId: "warrior",
categoryId: "wow",
raceId: "human",
gender: "female",
level: 80,
},
role: "tank",
position: [0, 0, 0],
yaw: 0,
grounded: true,
health,
maxHealth: 10_000,
resource: 0,
maxResource: 100,
resourceName: "Rage",
selectedTargetId: null,
activeCast: null,
animationEvent: null,
equipment: [],
updatedAt: now,
};
}
describe("WoW Death Knight Mark of Blood combat integration", () => {
beforeEach(() => {
now = 1_000;
restoreClock = installCombatClock(() => now);
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 80, xp: 0 });
usePartyStore.getState().initializeParty(
"wow335-death-knight-mark-of-blood", 80, useCombatStore.getState().health, "tank", 5,
);
useCombatStore.setState((state) => ({
globalCooldownEndsAt: 0,
deathKnightRunes: createDeathKnightRunes(now),
talentRanks: { ...state.talentRanks, "talent-1949": 1 },
resource: 100,
resourcePools: { "runic-power": 100 },
}));
registerAttacker("mark-target");
useCombatStore.getState().selectMob("mark-target");
});
afterEach(() => {
restoreClock?.();
restoreClock = undefined;
configureOnlineSessionRuntime({ active: false, authority: true, localAccountId: null });
});
it("casts the generated talent spell with twenty charges and a twenty-second duration", () => {
castMark();
const mark = useCombatStore.getState().auras.find((aura) => aura.targetId === "mark-target"
&& aura.definition.utilityTags?.includes(WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG));
expect(mark).toMatchObject({
sourceId: PLAYER_AURA_ENTITY_ID,
stacks: 20,
expiresAt: now + 20_000,
});
expect(mark?.definition.maxStacks).toBe(20);
});
it("heals the player when the marked enemy lands an attack and consumes one charge", () => {
castMark();
const maximumHealth = useCombatStore.getState().maxHealth;
const healthBefore = maximumHealth - 5_000;
useCombatStore.setState({ health: healthBefore });
useCombatStore.getState().damageMob("mark-target", 1, now);
useCombatStore.getState().advanceMobCombat(now + 450, [0, 0, 0]);
const state = useCombatStore.getState();
const damage = [...state.combatEvents].reverse().find((event) => event.kind === "damage"
&& event.sourceActorId === "mark-target"
&& event.targetActorId === PLAYER_AURA_ENTITY_ID);
const healing = [...state.combatEvents].reverse().find((event) => event.kind === "healing"
&& event.abilityId === WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID
&& event.targetActorId === PLAYER_AURA_ENTITY_ID);
const mark = state.auras.find((aura) => aura.targetId === "mark-target"
&& aura.definition.utilityTags?.includes(WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG));
expect(damage?.effectiveAmount).toBeGreaterThan(0);
expect(healing).toMatchObject({ rawAmount: Math.round(maximumHealth * 0.04) });
expect(healing?.effectiveAmount).toBe(healing?.rawAmount);
expect(state.health).toBe(healthBefore - damage!.effectiveAmount + healing!.effectiveAmount);
expect(mark?.stacks).toBe(19);
});
it("heals a party companion when the marked enemy lands an attack on them", () => {
const target = usePartyStore.getState().members[0]!;
const maxHealth = target.maxHealth;
const healthBefore = maxHealth - 1_000;
usePartyStore.setState((state) => ({
members: state.members.map((member) => member.id === target.id
? { ...member, health: healthBefore }
: member),
}));
registerPartyRuntimePosition(target.id, [0, 0, 0]);
castMark();
useCombatStore.getState().damageMob("mark-target", 50_000, now, {
actorId: target.id,
role: target.role ?? "damage",
});
useCombatStore.getState().advanceMobCombat(now + 450, [0, 0, 0]);
const state = useCombatStore.getState();
const memberAfter = usePartyStore.getState().members.find((member) => member.id === target.id)!;
const damage = [...state.combatEvents].reverse().find((event) => event.kind === "damage"
&& event.sourceActorId === "mark-target"
&& event.targetActorId === target.id);
const healing = [...state.combatEvents].reverse().find((event) => event.kind === "healing"
&& event.abilityId === WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID
&& event.targetActorId === target.id);
const mark = state.auras.find((aura) => aura.targetId === "mark-target"
&& aura.definition.utilityTags?.includes(WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG));
expect(damage?.effectiveAmount).toBeGreaterThan(0);
expect(healing).toMatchObject({ rawAmount: Math.round(maxHealth * 0.04) });
expect(healing?.effectiveAmount).toBe(healing?.rawAmount);
expect(memberAfter.health).toBe(healthBefore - damage!.effectiveAmount + healing!.effectiveAmount);
expect(mark?.stacks).toBe(19);
});
it("heals a summoned companion from the marked enemy's damage event", () => {
const summon: CombatSummon = {
id: "mark-summon-victim",
sourceAbilityId: "test-summon",
creatureId: 0,
targetId: "mark-target",
expiresAt: now + 60_000,
kind: "summon",
name: "Companion",
ownerActorId: PLAYER_AURA_ENTITY_ID,
health: 9_000,
maxHealth: 10_000,
position: [0, 0, 0],
nextActionAt: 0,
pulseIntervalMs: 1_000,
attackDamage: 0,
abilityIds: [],
resources: {},
cooldowns: {},
globalCooldownEndsAt: 0,
cast: null,
controlledUntil: 0,
schoolLockedUntil: {},
};
useCombatStore.setState({ summons: [summon] });
castMark();
useCombatStore.getState().damageMob("mark-target", 50_000, now, {
actorId: summon.id,
role: "tank",
});
useCombatStore.getState().advanceMobCombat(now + 450, [0, 0, 0]);
const state = useCombatStore.getState();
const summonAfter = state.summons.find((candidate) => candidate.id === summon.id)!;
const damage = [...state.combatEvents].reverse().find((event) => event.kind === "damage"
&& event.sourceActorId === "mark-target"
&& event.targetActorId === summon.id);
const healing = [...state.combatEvents].reverse().find((event) => event.kind === "healing"
&& event.abilityId === WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID
&& event.targetActorId === summon.id);
const mark = state.auras.find((aura) => aura.targetId === "mark-target"
&& aura.definition.utilityTags?.includes(WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG));
expect(damage?.effectiveAmount).toBeGreaterThan(0);
expect(healing).toMatchObject({ rawAmount: 400 });
expect(summonAfter.health).toBe(summon.health - damage!.effectiveAmount + healing!.effectiveAmount);
expect(mark?.stacks).toBe(19);
});
it("heals an online player from the marked enemy's damage event", () => {
const victim = remoteVictim();
const victimId = onlinePlayerActorId(victim.accountId);
configureOnlineSessionRuntime({
active: true,
authority: true,
localAccountId: "mark-local",
players: [victim],
});
registerPartyRuntimePosition(victimId, victim.position);
useCombatStore.getState().synchronizeActors();
castMark();
useCombatStore.getState().damageMob("mark-target", 50_000, now, {
actorId: victimId,
role: victim.role ?? "damage",
});
useCombatStore.getState().advanceMobCombat(now + 450, [0, 0, 0]);
const state = useCombatStore.getState();
const victimAfter = state.actors[victimId]!;
const damage = [...state.combatEvents].reverse().find((event) => event.kind === "damage"
&& event.sourceActorId === "mark-target"
&& event.targetActorId === victimId);
const healing = [...state.combatEvents].reverse().find((event) => event.kind === "healing"
&& event.abilityId === WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_ABILITY_ID
&& event.targetActorId === victimId);
const mark = state.auras.find((aura) => aura.targetId === "mark-target"
&& aura.definition.utilityTags?.includes(WOW335_DEATH_KNIGHT_MARK_OF_BLOOD_TAG));
expect(damage?.effectiveAmount).toBeGreaterThan(0);
expect(healing).toMatchObject({ rawAmount: 400 });
expect(victimAfter.health).toBe(victim.health - damage!.effectiveAmount + healing!.effectiveAmount);
expect(mark?.stacks).toBe(19);
});
});