Update Healer Man server

This commit is contained in:
phenom committed 2026-09-30 09:57:13 -04:00
1 parent 35dfdaed61
commit 4fb7a0b9c8
161 files changed
+24618 -529

No files matched your search

+468 -2
View File
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";
import { useCombatStore } from "./combatStore";
import { useCombatStore, type EnemyActiveCast } from "./combatStore";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
clearPartyRuntimeRegistry,
@@ -23,8 +23,9 @@ import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import { EMPTY_ITEM_STATS } from "./itemStats";
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
import { subscribeCombatEvents, type StructuredCombatEvent } from "./combatEvents";
import { abilitiesForClass } from "./abilityCatalog";
import { abilitiesForClass, abilityAtLevel } from "./abilityCatalog";
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
import { channelPulseAt, channelPulseCount } from "./combatChannelAmounts";
import { baseClassFor } from "../app/characterCatalog";
function registerEngagedMob(id = "pack:a", maxHealth = 200): void {
@@ -36,6 +37,60 @@ function registerEngagedMob(id = "pack:a", maxHealth = 200): void {
}
}
function stableUnit(seed: string): number {
let hash = 2166136261;
for (const character of seed) {
hash ^= character.charCodeAt(0);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0) / 0x1_0000_0000;
}
function partyHealCriticalAt(sourceId: string, abilityId: string, targetId: string, chance: number): number {
for (let now = 10_000; now < 1_000_000; now += 1) {
if (stableUnit(`${sourceId}:${abilityId}:${targetId}:${now}:heal`) < chance) return now;
}
throw new Error("Could not find a seeded companion healing crit.");
}
function createFrostbitePartyMage(seed: string) {
const mage = createConfiguredPartyMember({
name: "Frostbite Mage",
classId: "mage",
specialization: "Frost",
specializationId: "mage:frost",
talentTreeId: "mage-frost",
talentRanks: { "talent-38": 3 },
role: "damage",
}, 80, 0, seed);
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2);
usePartyStore.setState({ members: [{
...mage,
gearStats: EMPTY_ITEM_STATS,
resource: 10_000,
maxResource: 10_000,
resourcePools: { ...mage.resourcePools, mana: 10_000 },
}] });
registerPartyRuntimePosition(mage.id, [0, 0, 0]);
useCombatStore.getState().synchronizeActors();
return mage;
}
function registerFrostbiteTarget(id: string): void {
useCombatStore.getState().registerMob(id, {
name: id,
level: 1,
maxHealth: 100_000,
xpReward: 0,
});
setMobPosition(id, [2, 0, 0]);
useCombatStore.getState().damageMob(id, 1, 100);
for (const member of usePartyStore.getState().members) {
updatePartyRuntimeTargetVisibility(member.id, id, true);
}
usePartyStore.setState({ command: "attack", activeMobId: id });
}
describe("party combat runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
@@ -75,6 +130,361 @@ describe("party combat runtime", () => {
expect(effects.find(effect => effect.targetId === first.targetId)?.nextTickAt).toBe(first.nextTickAt);
});
it("refreshes a companion Priest's Shadow Word: Pain from its own completed Mind Blast", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const priest = createConfiguredPartyMember({
name: "Shadow Priest",
classId: "priest",
specialization: "Shadow",
specializationId: "priest:shadow",
talentTreeId: "priest-shadow",
role: "damage",
}, 80, 0, "pain-and-suffering-companion");
const priestAbilities = abilitiesForClass("priest");
const shadowWordPainBase = priestAbilities.find((ability) => ability.name === "Shadow Word: Pain")!;
const shadowWordPain = abilityAtLevel(shadowWordPainBase, 80);
const shadowWordPainDot = shadowWordPain.effects.find((effect) => effect.kind === "dot")!;
const mindBlast = abilityAtLevel(priestAbilities.find((ability) => ability.name === "Mind Blast")!, 80);
const targetId = "pain-and-suffering-companion-target";
usePartyStore.getState().initializeParty("pain-and-suffering-companion", 80,
useCombatStore.getState().health, "damage");
usePartyStore.setState({ members: [{
...priest,
talentRanks: { "talent-1909": 3 },
gearStats: EMPTY_ITEM_STATS,
activeCast: {
abilityId: mindBlast.id,
school: "shadow",
mode: "cast",
startedAt: 10_000,
completesAt: 10_000 + mindBlast.castTimeMs,
originalDurationMs: mindBlast.castTimeMs,
pushbackHits: 0,
origin: [0, 0, 0],
targetId,
},
activeCastTargetId: targetId,
status: "attacking",
}] });
registerPartyRuntimePosition(priest.id, [0, 0, 0]);
useCombatStore.getState().synchronizeActors();
registerEngagedMob(targetId, 100_000);
expect(useCombatStore.getState().applyTimedActorEffect(
priest.id, targetId, shadowWordPain, shadowWordPainDot, 1_000,
)).toBe(true);
const before = useCombatStore.getState().effects.find((effect) => (
effect.kind === "dot" && effect.sourceActorId === priest.id && effect.targetId === targetId
))!;
advanceDungeonPartyCombat(mindBlast.castTimeMs + 10_000);
const after = useCombatStore.getState().effects.find((effect) => (
effect.kind === "dot" && effect.sourceActorId === priest.id && effect.targetId === targetId
))!;
expect(after.remainingTicks).toBe(shadowWordPainDot.ticks);
expect(after.nextTickAt).toBe(10_000 + mindBlast.castTimeMs + after.intervalMs);
expect(after.fixedAmount).toBe(before.fixedAmount);
expect(after.intervalMs).toBe(before.intervalMs);
});
it("lets a party Mage Frostbite freeze after landed direct Frostbolt damage", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const seed = "party-mage-frostbite-direct";
const mage = createFrostbitePartyMage(seed);
const frostbolt = abilityAtLevel(abilitiesForClass("mage").find((ability) => ability.name === "Frostbolt")!, 80);
const targetId = "party-mage-frostbite-direct-target";
const impactAt = Array.from({ length: 100_000 }, (_, index) => 10_000 + index)
.find((now) => stableUnit(`${mage.id}:${frostbolt.id}:${targetId}:${now}:wow335-mage-frostbite`) < 0.15);
if (impactAt === undefined) throw new Error("Could not find a seeded party Frostbite proc time.");
usePartyStore.setState({ members: [{
...usePartyStore.getState().members[0]!,
activeCast: {
abilityId: frostbolt.id,
school: "frost",
mode: "cast",
startedAt: impactAt - frostbolt.castTimeMs,
completesAt: impactAt,
originalDurationMs: frostbolt.castTimeMs,
pushbackHits: 0,
origin: [0, 0, 0],
targetId,
},
activeCastTargetId: targetId,
status: "attacking",
}] });
useCombatStore.getState().synchronizeActors();
registerFrostbiteTarget(targetId);
const beforeHealth = useCombatStore.getState().mobs[targetId]!.health;
advanceDungeonPartyCombat(impactAt - frostbolt.castTimeMs);
advanceDungeonPartyCombat(impactAt);
expect(useCombatStore.getState().mobs[targetId]!.health).toBeLessThan(beforeHealth);
expect(useCombatStore.getState().mobs[targetId]!.statuses).toContainEqual(expect.objectContaining({
kind: "freeze",
sourceAbilityId: frostbolt.id,
sourceActorId: mage.id,
endsAt: impactAt + 4_000,
}));
});
it("lets a party Mage Frostbite proc on a landed Blizzard channel pulse", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const seed = "party-mage-frostbite-blizzard";
const mage = createFrostbitePartyMage(seed);
const blizzard = abilityAtLevel(abilitiesForClass("mage").find((ability) => ability.name === "Blizzard")!, 80);
const targetId = "party-mage-frostbite-blizzard-target";
const durationMs = blizzard.castTimeMs;
expect(blizzard.castMode).toBe("channel");
expect(durationMs).toBeGreaterThan(0);
let startedAt = 0;
let procAt = 0;
for (let start = 10_000; start < 20_000 && procAt === 0; start += 1) {
const pulses = Array.from({ length: channelPulseCount(blizzard) }, (_, index) => (
channelPulseAt(blizzard, start, durationMs, index)
));
const proc = pulses.find((pulseAt) => (
stableUnit(`${mage.id}:${blizzard.id}:${targetId}:${pulseAt}:wow335-mage-frostbite`) < 0.15
));
if (proc !== undefined) {
startedAt = start;
procAt = proc;
}
}
if (procAt === 0) throw new Error("Could not find a seeded party Blizzard Frostbite pulse.");
usePartyStore.setState({ members: [{
...usePartyStore.getState().members[0]!,
activeCast: {
abilityId: blizzard.id,
school: "frost",
mode: "channel",
startedAt,
completesAt: startedAt + durationMs,
originalDurationMs: durationMs,
pushbackHits: 0,
origin: [0, 0, 0],
targetId,
channelTicksCompleted: 0,
},
activeCastTargetId: targetId,
status: "attacking",
}] });
useCombatStore.getState().synchronizeActors();
registerFrostbiteTarget(targetId);
const beforeHealth = useCombatStore.getState().mobs[targetId]!.health;
advanceDungeonPartyCombat(startedAt);
advanceDungeonPartyCombat(procAt);
expect(useCombatStore.getState().mobs[targetId]!.health).toBeLessThan(beforeHealth);
expect(useCombatStore.getState().mobs[targetId]!.statuses).toContainEqual(expect.objectContaining({
kind: "freeze",
sourceAbilityId: blizzard.id,
sourceActorId: mage.id,
endsAt: procAt + 4_000,
}));
});
it("lets a party Mage proc its learned Eruption passive on Fireball", () => {
useCombatStore.getState().initializeCharacter({ classId: "rom-mage", level: 80 });
const seed = "party-rom-mage-eruption";
const mage = createConfiguredPartyMember({
name: "Eruption Mage",
classId: "rom-mage",
secondaryClassId: "rom-druid",
specialization: "Primary",
role: "damage",
}, 80, 0, seed);
const fireball = abilitiesForClass("rom-mage").find((ability) => ability.name === "Fireball")!;
const targetId = "party-rom-mage-eruption-target";
const impactAt = Array.from({ length: 100_000 }, (_, index) => 10_000 + index)
.find((now) => stableUnit(`${mage.id}:${fireball.id}:${targetId}:${now}:rom-eruption`) < 0.1);
if (impactAt === undefined) throw new Error("Could not find a seeded party Eruption proc time.");
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2, "rom");
usePartyStore.getState().setCommand("attack");
usePartyStore.setState({ members: [{
...mage,
resource: 10_000,
maxResource: 10_000,
resourcePools: { ...mage.resourcePools, mana: 10_000 },
activeCast: {
abilityId: fireball.id,
school: "fire",
mode: "cast",
startedAt: impactAt,
completesAt: impactAt,
originalDurationMs: 0,
pushbackHits: 0,
origin: [0, 0, 0],
targetId,
},
activeCastTargetId: targetId,
status: "attacking",
}] });
registerPartyRuntimePosition(mage.id, [0, 0, 0]);
registerFrostbiteTarget(targetId);
updatePartyRuntimeTargetVisibility(mage.id, targetId, true);
useCombatStore.getState().synchronizeActors();
advanceDungeonPartyCombat(impactAt);
expect(useCombatStore.getState().mobs[targetId]!.health).toBeLessThan(99_999);
expect(useCombatStore.getState().auras.some((aura) => aura.targetId === mage.id
&& aura.definition.id === "rom-mage-druid-control-flame-buff")).toBe(true);
});
it("procs a party Paladin's source-owned Vindication from its completed Holy spell", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const seed = "vindication-paladin-party";
const paladin = createConfiguredPartyMember({
name: "Vindication Paladin",
classId: "paladin",
specialization: "Retribution",
specializationId: "paladin:retribution",
talentTreeId: "paladin-retribution",
talentRanks: { "talent-1633": 3 },
role: "damage",
}, 80, 0, seed);
const exorcism = abilityAtLevel(abilitiesForClass("paladin").find((ability) => ability.name === "Exorcism")!, 80);
const targetId = "vindication-paladin-party-target";
const completionAt = Array.from({ length: 100_000 }, (_, index) => 10_000 + index)
.find((now) => stableUnit(`${paladin.id}:${exorcism.id}:${targetId}:${now}:wow335-paladin-vindication`) < 0.65);
if (completionAt === undefined) throw new Error("Could not find a seeded party Vindication proc time.");
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2);
usePartyStore.setState({ members: [{
...paladin,
gearStats: EMPTY_ITEM_STATS,
activeCast: {
abilityId: exorcism.id,
school: "holy",
mode: "cast",
startedAt: completionAt - exorcism.castTimeMs,
completesAt: completionAt,
originalDurationMs: exorcism.castTimeMs,
pushbackHits: 0,
origin: [0, 0, 0],
targetId,
},
activeCastTargetId: targetId,
status: "attacking",
}] });
registerPartyRuntimePosition(paladin.id, [0, 0, 0]);
useCombatStore.getState().synchronizeActors();
registerEngagedMob(targetId, 100_000);
advanceDungeonPartyCombat(completionAt);
expect(useCombatStore.getState().mobs[targetId].auras).toEqual(expect.arrayContaining([
expect.objectContaining({
id: "wow335:paladin:vindication",
sourceActorId: paladin.id,
sourceAbilityId: exorcism.id,
endsAt: completionAt + 10_000,
magnitude: 357,
}),
]));
});
it("grants a party Rogue Improved Kick only after it interrupts an active cast", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const seed = "improved-kick-party-runtime";
const rogue = createConfiguredPartyMember({
name: "Combat Rogue",
classId: "rogue",
specialization: "Combat",
specializationId: "rogue:combat",
talentTreeId: "rogue-combat",
talentRanks: { "talent-206": 2 },
role: "damage",
}, 80, 0, seed);
const kick = abilitiesForClass("rogue").find((ability) => ability.name === "Kick");
if (!kick) throw new Error("Missing generated Rogue Kick");
usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "damage", 2);
usePartyStore.setState({ members: [{
...rogue,
resource: 0,
resourcePools: { ...rogue.resourcePools, energy: 0 },
activeCast: {
abilityId: kick.id,
school: "physical",
mode: "cast",
startedAt: 900,
completesAt: 900,
originalDurationMs: 0,
pushbackHits: 0,
origin: [0, 0, 0],
targetId: "improved-kick-party-target",
},
activeCastTargetId: "improved-kick-party-target",
status: "attacking",
}] });
registerPartyRuntimePosition(rogue.id, [0, 0, 0]);
useCombatStore.getState().synchronizeActors();
const targetId = "improved-kick-party-target";
useCombatStore.getState().registerMob(targetId, { name: targetId, maxHealth: 100_000, xpReward: 0 });
setMobPosition(targetId, [2, 0, 0]);
useCombatStore.getState().damageMob(targetId, 1, 100);
updatePartyRuntimeTargetVisibility(rogue.id, targetId, true);
const hostileCast: EnemyActiveCast = {
attackId: "party-test-shadow-bolt",
name: "Shadow Bolt",
spellId: 686,
school: "shadow",
interruptible: true,
targetActorId: null,
sourcePosition: [2, 0, 0],
targetPosition: [0, 0, 0],
sourceYaw: 0,
startedAt: 500,
completesAt: 2_500,
};
useCombatStore.setState((state) => ({
mobs: { ...state.mobs, [targetId]: { ...state.mobs[targetId]!, activeCast: hostileCast } },
}));
advanceDungeonPartyCombat(1_000);
expect(useCombatStore.getState().mobs[targetId]?.activeCast).toBeNull();
expect(useCombatStore.getState().auras).toContainEqual(expect.objectContaining({
sourceId: rogue.id,
targetId: rogue.id,
appliedAt: 1_000,
expiresAt: 7_000,
definition: expect.objectContaining({
utilityTags: ["wow335-rogue-improved-kick"],
modifiers: [
{ kind: "stat", stat: "melee-haste", operation: "flat", value: 35 },
{ kind: "stat", stat: "ranged-haste", operation: "flat", value: 35 },
],
}),
}));
expect(usePartyStore.getState().members[0]?.resourcePools.energy).toBe(40);
useCombatStore.getState().selectMob(targetId);
usePartyStore.setState((state) => ({
members: state.members.map((member) => ({
...member,
nextActionAt: 10_000,
gearStats: { ...member.gearStats, hasteRating: 0 },
})),
}));
useCombatStore.getState().synchronizeActors();
const autoAttackTimes: number[] = [];
const unsubscribe = subscribeCombatPresentation((message) => {
if (message.kind === "event"
&& message.event.sourceActorId === rogue.id
&& message.event.abilityId === "party-auto-attack"
&& message.event.phase === "impact") {
autoAttackTimes.push(message.event.occurredAt);
}
});
advanceDungeonPartyCombat(2_200);
advanceDungeonPartyCombat(3_400);
unsubscribe();
expect(autoAttackTimes).toEqual([2_200, 3_400]);
});
it("precasts Earth Shield on the tank while the Restoration Shaman is at full health", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const tank = createConfiguredPartyMember({
@@ -354,6 +764,62 @@ describe("party combat runtime", () => {
.toBeGreaterThan(startingHealth);
});
it("applies a Shaman owner's Elemental Oath to party-caster healing crits", () => {
useCombatStore.getState().initializeCharacter({ classId: "shaman", level: 80 });
useCombatStore.setState((state) => ({
talentRanks: { ...state.talentRanks, "talent-2049": 3 },
}));
const tank = createConfiguredPartyMember({
name: "Tank", classId: "warrior", specialization: "Protection",
specializationId: "warrior:protection", talentTreeId: "warrior-protection", role: "tank",
}, 80, 0, "elemental-oath-party-heal");
const healer = createConfiguredPartyMember({
name: "Healer", classId: "priest", specialization: "Holy",
specializationId: "priest:holy", talentTreeId: "priest-holy", role: "healer",
}, 80, 1, "elemental-oath-party-heal");
const greaterHeal = abilitiesForClass("priest").find((ability) => ability.name === "Greater Heal")!;
const startingHealth = Math.round(tank.maxHealth * 0.3);
usePartyStore.getState().initializeParty("elemental-oath-party-heal", 80,
useCombatStore.getState().health, "damage");
usePartyStore.setState({
members: [
{ ...tank, health: startingHealth, gearStats: EMPTY_ITEM_STATS },
{
...healer,
gearStats: EMPTY_ITEM_STATS,
resourceType: "mana",
resourceName: "Mana",
resource: 10_000,
maxResource: 10_000,
resourcePools: { mana: 10_000 },
cooldowns: Object.fromEntries(abilitiesForClass("priest")
.filter((ability) => ability.id !== greaterHeal.id)
.map((ability) => [ability.id, 60_000])),
},
],
});
registerPartyRuntimePosition(tank.id, [0, 0, 0]);
registerPartyRuntimePosition(healer.id, [0, 0, 0]);
useCombatStore.getState().synchronizeActors();
registerEngagedMob("elemental-oath-party-heal-boss", 100_000);
const impactAt = partyHealCriticalAt(healer.id, greaterHeal.id, tank.id, 0.03);
const castStartAt = impactAt - greaterHeal.castTimeMs;
advanceDungeonPartyCombat(castStartAt);
const activeCast = usePartyStore.getState().members.find((member) => member.id === healer.id)?.activeCast;
expect(activeCast?.abilityId).toBe(greaterHeal.id);
expect(activeCast?.completesAt).toBe(impactAt);
advanceDungeonPartyCombat(impactAt);
expect(useCombatStore.getState().combatEvents).toContainEqual(expect.objectContaining({
kind: "healing",
sourceActorId: healer.id,
targetActorId: tank.id,
abilityId: greaterHeal.id,
critical: true,
}));
});
it("keeps companion Penance active for three healing pulses", () => {
useCombatStore.getState().initializeCharacter({ classId: "mage", role: "damage", level: 80 });
const tank = createConfiguredPartyMember({