269 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|