Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
@@ -0,0 +1,213 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user