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

214 lines
9.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
import { PLAYER_AURA_ENTITY_ID, useCombatStore, type CombatSummon } from "./combatStore";
import { setMobPosition } from "./mobRuntimeRegistry";
import { applyAura } from "./combatAuras";
import { usePartyStore } from "./partyStore";
import { registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import {
warlockSoulLinkDamageSplit,
warlockSoulLinkDemon,
warlockSoulLinkSharePercent,
type WarlockSoulLinkSummon,
} from "./wow335WarlockSoulLinkRuntime";
const demon = (overrides: Partial<WarlockSoulLinkSummon> = {}): WarlockSoulLinkSummon => ({
sourceAbilityId: "wow335-warlock-summon-felhunter",
ownerActorId: "warlock-player",
health: 300,
expiresAt: 10_000,
controlledUntil: 0,
position: [300, 0, 0],
...overrides,
});
function combatDemon(overrides: Partial<CombatSummon> = {}): CombatSummon {
return {
id: "soul-link-felhunter",
sourceAbilityId: "wow335-warlock-summon-felhunter",
creatureId: 417,
targetId: null,
expiresAt: Number.MAX_SAFE_INTEGER,
kind: "pet",
name: "Felhunter",
ownerActorId: PLAYER_AURA_ENTITY_ID,
health: 500,
maxHealth: 500,
position: [10, 0, 0],
nextActionAt: 0,
pulseIntervalMs: 0,
attackDamage: 0,
abilityIds: [],
resources: {},
cooldowns: {},
globalCooldownEndsAt: 0,
cast: null,
controlledUntil: 0,
schoolLockedUntil: {},
...overrides,
};
}
describe("WoW Warlock Soul Link mechanics", () => {
it("uses the populated generated talent tooltip and exposes Soul Link as an active self buff", () => {
const ability = WOW335_ABILITIES_BY_CLASS.warlock.find((candidate) => candidate.name === "Soul Link");
expect(ability?.target).toBe("self");
expect(ability?.description).toContain("6% of all damage taken by the caster");
expect(ability?.description).toContain("Cannot Soul Link a Summoned Imp");
expect(ability?.effects).toEqual(expect.arrayContaining([
expect.objectContaining({
kind: "apply-aura",
recipient: "caster",
aura: expect.objectContaining({
utilityTags: ["wow335-warlock-soul-link"],
durationMs: null,
}),
}),
]));
});
it("uses the generated Soul Link and Demonic Embrace rank descriptions", () => {
expect([0, 1, 2, 3].map((rank) => warlockSoulLinkSharePercent(1, rank)))
.toEqual([6, 8, 10, 12]);
expect(warlockSoulLinkSharePercent(0, 3)).toBe(0);
});
it("requires the caster's live, controlled, non-Imp demon within 30 yards", () => {
const linked = warlockSoulLinkDemon([demon()], "warlock-player", [0, 0, 0], 1_000);
expect(linked).not.toBeNull();
expect(warlockSoulLinkDemon(
[demon({ sourceAbilityId: "wow335-warlock-summon-imp" })],
"warlock-player",
[0, 0, 0],
1_000,
)).toBeNull();
expect(warlockSoulLinkDemon([demon({ ownerActorId: "another-warlock" })], "warlock-player", [0, 0, 0], 1_000))
.toBeNull();
expect(warlockSoulLinkDemon([demon({ health: 0 })], "warlock-player", [0, 0, 0], 1_000)).toBeNull();
expect(warlockSoulLinkDemon([demon({ expiresAt: 1_000 })], "warlock-player", [0, 0, 0], 1_000)).toBeNull();
expect(warlockSoulLinkDemon([demon({ controlledUntil: 1_001 })], "warlock-player", [0, 0, 0], 1_000))
.toBeNull();
expect(warlockSoulLinkDemon([demon({ position: [301, 0, 0] })], "warlock-player", [0, 0, 0], 1_000))
.toBeNull();
});
it("splits post-mitigation damage and rounds the unpreventable demon share", () => {
expect(warlockSoulLinkDamageSplit(101, 6)).toEqual({ casterDamage: 95, demonDamage: 6 });
expect(warlockSoulLinkDamageSplit(5, 12)).toEqual({ casterDamage: 4, demonDamage: 1 });
expect(warlockSoulLinkDamageSplit(0, 12)).toEqual({ casterDamage: 0, demonDamage: 0 });
});
it("transfers the generated Soul Link and Demonic Embrace share during incoming damage", () => {
useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 80, xp: 0 });
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1282": 1, "talent-1223": 3 },
summons: [combatDemon()],
playerPosition: [0, 0, 0],
}));
const soulLink = useCombatStore.getState().abilities.find((ability) => ability.name === "Soul Link");
expect(soulLink).toBeDefined();
expect(useCombatStore.getState().castAbility(soulLink!.id, undefined, 1_000).ok).toBe(true);
const demonBefore = useCombatStore.getState().summons[0]!.health;
const playerDamage = useCombatStore.getState().damagePlayer(100, "shadow", 80, 2_000);
const state = useCombatStore.getState();
const playerHit = [...state.combatEvents].reverse().find((event) => (
event.kind === "damage" && event.targetActorId === PLAYER_AURA_ENTITY_ID && event.occurredAt === 2_000
));
const demonHit = [...state.combatEvents].reverse().find((event) => (
event.kind === "damage" && event.targetActorId === "soul-link-felhunter" && event.occurredAt === 2_000
));
expect(playerHit).toBeDefined();
expect(demonHit).toMatchObject({ abilityId: "wow335-warlock-soul-link", absorbed: 0 });
const expectedShare = Math.round(playerHit!.rawAmount * 0.12);
expect(demonHit!.effectiveAmount).toBe(expectedShare);
expect(state.summons[0]!.health).toBe(demonBefore - expectedShare);
expect(playerDamage).toBe(playerHit!.rawAmount - expectedShare);
});
it("shares enemy melee damage with an active demon during combat", () => {
useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 80, xp: 0 });
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-1282": 1 },
summons: [combatDemon()],
playerPosition: [0, 0, 0],
}));
const soulLink = useCombatStore.getState().abilities.find((ability) => ability.name === "Soul Link");
expect(useCombatStore.getState().castAbility(soulLink!.id, undefined, 1_000).ok).toBe(true);
useCombatStore.getState().registerMob("soul-link-attacker", { name: "Attacker", maxHealth: 10_000, xpReward: 0 });
setMobPosition("soul-link-attacker", [2, 0, 0]);
useCombatStore.getState().damageMob("soul-link-attacker", 1, 1_000);
expect(useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0])).toBeGreaterThan(0);
const state = useCombatStore.getState();
const demonHit = [...state.combatEvents].reverse().find((event) => (
event.kind === "damage" && event.targetActorId === "soul-link-felhunter" && event.occurredAt === 1_450
));
expect(demonHit).toMatchObject({ abilityId: "wow335-warlock-soul-link", absorbed: 0 });
expect(demonHit!.effectiveAmount).toBeGreaterThan(0);
expect(state.summons[0]!.health).toBe(500 - demonHit!.effectiveAmount);
});
it("shares enemy damage with a companion Warlock's linked demon", () => {
useCombatStore.getState().initializeCharacter({ classId: "warlock", level: 80, xp: 0 });
usePartyStore.getState().initializeParty("soul-link-companion-test", 80, useCombatStore.getState().health);
const member = usePartyStore.getState().members[0]!;
usePartyStore.setState((state) => ({
members: state.members.map((candidate) => candidate.id === member.id
? { ...candidate, classId: "warlock", talentRanks: { ...candidate.talentRanks, "talent-1282": 1 } }
: candidate),
}));
registerPartyRuntimePosition(member.id, [0, 0, 0]);
const linkAbility = WOW335_ABILITIES_BY_CLASS.warlock.find((ability) => ability.name === "Soul Link")!;
const linkAura = linkAbility.effects.find((effect) => effect.kind === "apply-aura")!;
if (linkAura.kind !== "apply-aura") throw new Error("Soul Link must apply its caster aura");
const companionDemon = combatDemon({
id: "companion-soul-link-felhunter",
ownerActorId: member.id,
position: [10, 0, 0],
});
useCombatStore.setState((state) => ({
playerPosition: [20, 0, 0],
summons: [companionDemon],
auras: [...applyAura(state.auras, {
definition: linkAura.aura,
sourceId: member.id,
targetId: member.id,
now: 1_000,
}).auras],
}));
useCombatStore.getState().registerMob("companion-soul-link-attacker", {
name: "Attacker",
maxHealth: 10_000,
xpReward: 0,
});
setMobPosition("companion-soul-link-attacker", [2, 0, 0]);
useCombatStore.getState().damageMob(
"companion-soul-link-attacker",
1,
1_000,
{ actorId: member.id, role: "tank" },
);
let combatDamage = 0;
for (let now = 3_350; now < 20_000; now += 1_900) {
combatDamage += useCombatStore.getState().advanceMobCombat(now, [20, 0, 0]);
if (useCombatStore.getState().combatEvents.some((event) => (
event.kind === "damage"
&& event.targetActorId === "companion-soul-link-felhunter"
&& event.occurredAt === now
))) break;
}
expect(combatDamage).toBeGreaterThan(0);
const state = useCombatStore.getState();
const demonHit = [...state.combatEvents].reverse().find((event) => (
event.kind === "damage"
&& event.targetActorId === "companion-soul-link-felhunter"
&& event.occurredAt >= 3_350
));
expect(demonHit).toMatchObject({ abilityId: "wow335-warlock-soul-link", absorbed: 0 });
expect(demonHit!.effectiveAmount).toBeGreaterThan(0);
expect(state.summons[0]!.health).toBe(500 - demonHit!.effectiveAmount);
});
});