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

+392 -6
View File
@@ -72,6 +72,35 @@ function castHostile(name: string, targetId: string, now: number) {
return result;
}
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 painAndSufferingMindBlastStart(targetId: string, rank: number, shouldProc: boolean, earliest: number): number {
const mindBlast = priestAbility("Mind Blast");
const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!;
const chance = rank > 0 ? (talent.rankEffects[rank - 1]?.procChance ?? 0) / 100 : 0;
for (let start = earliest; start < earliest + 12_000; start += 100) {
const completesAt = start + mindBlast.castTimeMs;
const proc = stableUnit(
`${PLAYER_AGGRO_ID}:${mindBlast.id}:${targetId}:${completesAt}:wow335-priest-pain-and-suffering`,
) < chance;
if (proc === shouldProc) return start;
}
throw new Error(`Could not find a seeded Pain and Suffering ${shouldProc ? "proc" : "failure"}.`);
}
function shadowWordPainEffect(targetId: string, sourceActorId = PLAYER_AGGRO_ID) {
return useCombatStore.getState().effects.find((effect) => effect.kind === "dot"
&& effect.sourceAbilityId === priestAbility("Shadow Word: Pain").id
&& effect.sourceActorId === sourceActorId && effect.targetId === targetId);
}
describe("WoW Priest mechanics", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
@@ -587,6 +616,128 @@ describe("WoW Priest mechanics", () => {
expect(useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("wow335-priest-shadow-weaving"))?.stacks).toBe(5);
});
it("uses generated Pain and Suffering proc chances to refresh Shadow Word: Pain", () => {
const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!;
expect(talent.rankEffects.map((rank) => rank.procChance)).toEqual([33, 66, 100]);
const cases = [
{ rank: 0, shouldProc: false },
{ rank: 1, shouldProc: false },
{ rank: 1, shouldProc: true },
{ rank: 2, shouldProc: false },
{ rank: 2, shouldProc: true },
{ rank: 3, shouldProc: true },
];
const shadowWordPain = abilityAtLevel(priestAbility("Shadow Word: Pain"), 80);
const dotDefinition = shadowWordPain.effects.find((effect) => effect.kind === "dot")!;
expect(dotDefinition.kind).toBe("dot");
cases.forEach(({ rank, shouldProc }, index) => {
const targetId = `priest-pain-and-suffering-rank-${rank}-${index}`;
useCombatStore.getState().registerMob(targetId, {
name: "Pain and Suffering target", maxHealth: 1_000_000, xpReward: 0,
});
setMobPosition(targetId, [4, 0, 0]);
setPriestTalentRanks(rank ? { [talent.id]: rank } : {});
const appliedAt = 1_000 + index * 25_000;
expect(castHostile("Shadow Word: Pain", targetId, appliedAt).ok).toBe(true);
const before = shadowWordPainEffect(targetId)!;
expect(before).toBeDefined();
const expirationBefore = before.nextTickAt + (before.remainingTicks - 1) * before.intervalMs;
const mindBlast = priestAbility("Mind Blast");
const castAt = painAndSufferingMindBlastStart(targetId, rank, shouldProc, appliedAt + 2_000);
expect(castHostile("Mind Blast", targetId, castAt).ok).toBe(true);
const completedCast = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "cast" && event.abilityId === mindBlast.id && event.targetActorId === targetId
)).at(-1)!;
const after = shadowWordPainEffect(targetId)!;
const expirationAfter = after.nextTickAt + (after.remainingTicks - 1) * after.intervalMs;
if (shouldProc) {
expect(after.remainingTicks).toBe(dotDefinition.ticks);
expect(after.nextTickAt).toBe(completedCast.occurredAt + after.intervalMs);
expect(expirationAfter).toBe(completedCast.occurredAt + dotDefinition.ticks * dotDefinition.intervalMs);
} else {
expect(expirationAfter).toBe(expirationBefore);
}
});
});
it("refreshes only its own Shadow Word: Pain on the eligible cast target", () => {
const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!;
setPriestTalentRanks({ [talent.id]: 3 });
for (const targetId of ["priest-pain-scope-primary", "priest-pain-scope-other"]) {
useCombatStore.getState().registerMob(targetId, {
name: "Pain scope target", maxHealth: 1_000_000, xpReward: 0,
});
setMobPosition(targetId, [4, 0, 0]);
}
expect(castHostile("Shadow Word: Pain", "priest-pain-scope-primary", 1_000).ok).toBe(true);
expect(castHostile("Shadow Word: Pain", "priest-pain-scope-other", 5_000).ok).toBe(true);
const ownedPrimary = shadowWordPainEffect("priest-pain-scope-primary")!;
const ownedOther = shadowWordPainEffect("priest-pain-scope-other")!;
const foreignPrimary = { ...ownedPrimary, id: "foreign-priest-shadow-word-pain", sourceActorId: "other-priest" };
useCombatStore.setState((state) => ({ effects: [...state.effects, foreignPrimary] }));
const endOtherBefore = ownedOther.nextTickAt + (ownedOther.remainingTicks - 1) * ownedOther.intervalMs;
const endForeignBefore = foreignPrimary.nextTickAt + (foreignPrimary.remainingTicks - 1) * foreignPrimary.intervalMs;
expect(castHostile("Mind Blast", "priest-pain-scope-primary", 10_000).ok).toBe(true);
const ownAfter = shadowWordPainEffect("priest-pain-scope-primary")!;
const otherAfter = shadowWordPainEffect("priest-pain-scope-other")!;
const foreignAfter = useCombatStore.getState().effects.find((effect) => effect.id === foreignPrimary.id)!;
expect(ownAfter.remainingTicks).toBe(6);
expect(otherAfter.nextTickAt + (otherAfter.remainingTicks - 1) * otherAfter.intervalMs).toBe(endOtherBefore);
expect(foreignAfter.nextTickAt + (foreignAfter.remainingTicks - 1) * foreignAfter.intervalMs).toBe(endForeignBefore);
});
it("does not refresh for unrelated casts or when the target has no Shadow Word: Pain", () => {
const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!;
setPriestTalentRanks({ [talent.id]: 3 });
for (const targetId of ["priest-pain-unrelated", "priest-pain-absent"]) {
useCombatStore.getState().registerMob(targetId, {
name: "Pain trigger guard target", maxHealth: 1_000_000, xpReward: 0,
});
setMobPosition(targetId, [4, 0, 0]);
}
expect(castHostile("Shadow Word: Pain", "priest-pain-unrelated", 1_000).ok).toBe(true);
const dot = shadowWordPainEffect("priest-pain-unrelated")!;
const endBefore = dot.nextTickAt + (dot.remainingTicks - 1) * dot.intervalMs;
expect(castHostile("Smite", "priest-pain-unrelated", 5_000).ok).toBe(true);
const afterSmite = shadowWordPainEffect("priest-pain-unrelated")!;
expect(afterSmite.nextTickAt + (afterSmite.remainingTicks - 1) * afterSmite.intervalMs).toBe(endBefore);
expect(castHostile("Mind Blast", "priest-pain-absent", 10_000).ok).toBe(true);
expect(shadowWordPainEffect("priest-pain-absent")).toBeUndefined();
});
it.each(["Mind Flay", "Mind Sear", "Mind Blast"])("refreshes Shadow Word: Pain after a %s cast", (castName) => {
const talent = TALENT_NODES.find((node) => node.id === "talent-1909")!;
const castAbilityNodeId = abilityTalentNodeId(priestAbility(castName));
setPriestTalentRanks({
[talent.id]: 3,
...(castAbilityNodeId ? { [castAbilityNodeId]: 1 } : {}),
});
const targetId = `priest-pain-and-suffering-${castName.toLowerCase().replaceAll(" ", "-")}`;
useCombatStore.getState().registerMob(targetId, {
name: "Pain and Suffering eligible cast target", maxHealth: 1_000_000, xpReward: 0,
});
setMobPosition(targetId, [4, 0, 0]);
expect(castHostile("Shadow Word: Pain", targetId, 1_000).ok).toBe(true);
useCombatStore.getState().tick(9, 10_000);
const before = shadowWordPainEffect(targetId)!;
expect(before.remainingTicks).toBeLessThan(6);
const castResult = castHostile(castName, targetId, 11_000);
expect(castResult.ok, JSON.stringify(castResult)).toBe(true);
const cast = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "cast" && event.abilityId === priestAbility(castName).id && event.targetActorId === targetId
)).at(-1)!;
const after = shadowWordPainEffect(targetId)!;
expect(after.remainingTicks).toBe(6);
expect(after.nextTickAt).toBe(cast.occurredAt + after.intervalMs);
expect(after.tickAmounts).toEqual(before.tickAmounts);
expect(after.basePoints).toBe(before.basePoints);
expect(after.intervalMs).toBe(before.intervalMs);
expect(after.damageSchool).toBe(before.damageSchool);
expect(after.id).toBe(before.id);
});
it("procs Holy Concentration on a critical heal and extends nearby Renew by at most six seconds", () => {
const talent = TALENT_NODES.find((node) => node.name === "Holy Concentration")!;
setPriestTalentRanks({ [talent.id]: 3 }, 100);
@@ -649,18 +800,24 @@ describe("WoW Priest mechanics", () => {
useCombatStore.getState().damagePlayer(500);
const healthBefore = useCombatStore.getState().health;
const clickAmount = useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1);
expect(clickAmount).toBeGreaterThan(0);
expect(clickAmount).toBe(1_540 * 3);
expect(useCombatStore.getState().health).toBe(healthBefore);
expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({
kind: "hot",
sourceAbilityId: ability.id,
remainingTicks: 6,
intervalMs: 1_000,
fixedAmount: expect.any(Number),
nextTickAt: completion! + 1_001,
remainingTicks: 3,
intervalMs: 2_000,
coefficient: 0,
basePoints: 1_540,
nextTickAt: completion! + 2_001,
}));
useCombatStore.getState().tick(6, completion! + 6_001);
useCombatStore.getState().tick(1, completion! + 1_001);
expect(useCombatStore.getState().health).toBe(healthBefore);
expect(useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === ability.id)?.remainingTicks).toBe(3);
useCombatStore.getState().tick(1, completion! + 2_001);
expect(useCombatStore.getState().health).toBeGreaterThan(healthBefore);
expect(useCombatStore.getState().effects.find((effect) => effect.sourceAbilityId === ability.id)?.remainingTicks).toBe(2);
useCombatStore.getState().tick(4, completion! + 6_001);
expect(useCombatStore.getState().summons.find((summon) => summon.id === well!.id)?.clickCharges).toBe(9);
for (let click = 1; click < 10; click += 1) {
@@ -669,4 +826,233 @@ describe("WoW Priest mechanics", () => {
expect(useCombatStore.getState().summons.some((summon) => summon.id === well!.id)).toBe(false);
expect(useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 20)).toBe(0);
});
it("uses the sourced per-tick spell-power coefficient for Lightwell ranks with a bonus row", () => {
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 75 });
useCombatStore.setState((state) => ({ gearStats: { ...state.gearStats, spellPower: 300 } }));
usePartyStore.getState().initializeParty("wow-priest-lightwell-coefficient", 75, useCombatStore.getState().health);
useCombatStore.getState().synchronizeActors();
unlock("Lightwell");
const ability = priestAbility("Lightwell");
expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true);
const completion = useCombatStore.getState().activeCast?.completesAt;
expect(completion).toBeDefined();
useCombatStore.getState().tick(1, completion!);
const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id);
expect(well?.creatureId).toBe(31893);
expect(useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1)).toBe(1_405 * 3);
expect(useCombatStore.getState().effects).toContainEqual(expect.objectContaining({
sourceAbilityId: ability.id,
basePoints: 1_305,
coefficient: 0.3333,
remainingTicks: 3,
intervalMs: 2_000,
}));
});
it("cancels a Lightwell HoT after cumulative effective damage reaches 30% of recipient max health", () => {
unlock("Lightwell");
const ability = priestAbility("Lightwell");
expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true);
const completion = useCombatStore.getState().activeCast?.completesAt;
expect(completion).toBeDefined();
useCombatStore.getState().tick(1, completion!);
const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id);
expect(well).toBeDefined();
const maxHealth = useCombatStore.getState().maxHealth;
useCombatStore.setState({ health: maxHealth, shield: 0 });
useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1);
const threshold = maxHealth * 0.3;
const firstDamage = useCombatStore.getState().damagePlayer(Math.floor(maxHealth * 0.2), "shadow", undefined, completion! + 2);
const afterFirstHit = useCombatStore.getState().effects.find((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id);
expect(firstDamage).toBeGreaterThan(0);
expect(firstDamage).toBeLessThan(threshold);
expect(afterFirstHit?.lightwellDamageCancellation).toEqual({ threshold, accumulatedDamage: firstDamage });
const secondDamage = useCombatStore.getState().damagePlayer(Math.floor(maxHealth * 0.2), "shadow", undefined, completion! + 3);
expect(firstDamage + secondDamage).toBeGreaterThanOrEqual(threshold);
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id)).toBe(false);
useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 4);
expect(useCombatStore.getState().effects.find((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id)?.lightwellDamageCancellation).toEqual({
threshold,
accumulatedDamage: 0,
});
});
it("does not count fully absorbed damage and starts a fresh threshold after the HoT expires", () => {
unlock("Lightwell");
const ability = priestAbility("Lightwell");
expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true);
const completion = useCombatStore.getState().activeCast?.completesAt;
expect(completion).toBeDefined();
useCombatStore.getState().tick(1, completion!);
const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id);
expect(well).toBeDefined();
useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1);
useCombatStore.setState({ shield: 2_000 });
expect(useCombatStore.getState().damagePlayer(1_000, "shadow", undefined, completion! + 2)).toBe(0);
expect(useCombatStore.getState().effects.find((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id)?.lightwellDamageCancellation?.accumulatedDamage).toBe(0);
useCombatStore.getState().tick(1, completion! + 6_001);
expect(useCombatStore.getState().effects.some((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id)).toBe(false);
useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 6_002);
expect(useCombatStore.getState().effects.find((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id)?.lightwellDamageCancellation?.accumulatedDamage).toBe(0);
});
it("tracks Lightwell damage separately for each recipient and on resolved enemy hits", () => {
unlock("Lightwell");
const ability = priestAbility("Lightwell");
expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true);
const completion = useCombatStore.getState().activeCast?.completesAt;
expect(completion).toBeDefined();
useCombatStore.getState().tick(1, completion!);
const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id);
expect(well).toBeDefined();
const member = usePartyStore.getState().members[0]!;
useCombatStore.getState().clickLightwell(well!.id, PLAYER_AURA_ENTITY_ID, completion! + 1);
useCombatStore.getState().clickLightwell(well!.id, member.id, completion! + 1);
useCombatStore.getState().registerMob("priest-lightwell-hit", {
name: "Lightwell damage test",
maxHealth: 100_000,
xpReward: 0,
attacks: [{
...DEFAULT_ENEMY_ATTACK,
id: "priest-lightwell-shadow-cast",
name: "Shadow cast",
delivery: "projectile",
animation: "cast",
school: "shadow",
range: 30,
castTimeMs: 0,
}],
});
setMobPosition("priest-lightwell-hit", [8, 0, 0]);
useCombatStore.getState().damageMob("priest-lightwell-hit", 1, completion! + 1);
const beforeHit = useCombatStore.getState().health;
useCombatStore.getState().advanceMobCombat(completion! + 451, [0, 0, 0]);
const appliedDamage = beforeHit - useCombatStore.getState().health;
const effects = useCombatStore.getState().effects.filter((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id);
expect(appliedDamage).toBeGreaterThan(0);
expect(effects.find((effect) => effect.targetId === null)?.lightwellDamageCancellation?.accumulatedDamage)
.toBe(appliedDamage);
expect(effects.find((effect) => effect.targetId === member.id)?.lightwellDamageCancellation?.accumulatedDamage)
.toBe(0);
});
it("counts resolved enemy attacks against a party member toward that member's Lightwell cancellation", () => {
unlock("Lightwell");
const ability = priestAbility("Lightwell");
expect(useCombatStore.getState().castAbility(ability.id, [0, 0, 0], 1_000).ok).toBe(true);
const completion = useCombatStore.getState().activeCast?.completesAt;
expect(completion).toBeDefined();
useCombatStore.getState().tick(1, completion!);
const well = useCombatStore.getState().summons.find((summon) => summon.sourceAbilityId === ability.id);
expect(well).toBeDefined();
const member = usePartyStore.getState().members[0]!;
setMemberHealth(member.id, 100_000, 100_000);
useCombatStore.getState().clickLightwell(well!.id, member.id, completion! + 1);
useCombatStore.getState().registerMob("priest-lightwell-party-hit", {
name: "Lightwell party damage test",
maxHealth: 100_000,
xpReward: 0,
attacks: [{
...DEFAULT_ENEMY_ATTACK,
id: "priest-lightwell-party-strike",
name: "Party strike",
range: 30,
cooldownMs: 1_900,
}],
});
setMobPosition("priest-lightwell-party-hit", [4, 0, 0]);
useCombatStore.getState().damageMob("priest-lightwell-party-hit", 1, completion! + 1, {
actorId: member.id,
role: member.role,
});
useCombatStore.getState().advanceMobCombat(completion! + 451, [0, 0, 0]);
const damagedMember = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!;
const effectiveDamage = 100_000 - damagedMember.health;
const memberHot = useCombatStore.getState().effects.find((effect) => effect.kind === "hot"
&& effect.sourceAbilityId === ability.id && effect.targetId === member.id);
expect(effectiveDamage).toBeGreaterThan(0);
expect(memberHot?.lightwellDamageCancellation?.accumulatedDamage).toBe(effectiveDamage);
});
it("applies Spell Warding to incoming magic damage by rank and leaves physical damage unchanged", () => {
const incomingDamageAtRank = (rank: number, school: "physical" | "shadow"): number => {
setPriestTalentRanks(rank > 0 ? { "talent-411": rank } : {});
useCombatStore.setState({ health: 1_000_000, maxHealth: 1_000_000, shield: 0 });
const before = useCombatStore.getState().health;
useCombatStore.getState().damagePlayer(10_000, school, 80, 1_000 + rank);
return before - useCombatStore.getState().health;
};
const magicDamage = [0, 1, 2, 3].map((rank) => incomingDamageAtRank(rank, "shadow"));
expect(magicDamage[0]).toBeGreaterThan(magicDamage[1]!);
expect(magicDamage[1]).toBeGreaterThan(magicDamage[2]!);
expect(magicDamage[2]).toBeGreaterThan(magicDamage[3]!);
expect(magicDamage[3]).toBeCloseTo(magicDamage[0]! * 0.97, -1);
const physicalDamage = [0, 1, 2, 3].map((rank) => incomingDamageAtRank(rank, "physical"));
expect(physicalDamage).toEqual([physicalDamage[0], physicalDamage[0], physicalDamage[0], physicalDamage[0]]);
});
it("applies Spell Warding to resolved enemy spell attacks without reducing enemy melee hits", () => {
const resolvedEnemyDamageAtRank = (rank: number, school: "physical" | "shadow"): number => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 });
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
usePartyStore.getState().initializeParty("wow-priest-spell-warding", 80, useCombatStore.getState().health);
usePartyStore.getState().selectMember(null);
useCombatStore.getState().synchronizeActors();
setPriestTalentRanks(rank > 0 ? { "talent-411": rank } : {});
const mobId = "priest-spell-warding-attacker";
const attackId = "priest-spell-warding-attack";
useCombatStore.getState().registerMob(mobId, {
name: "Spell Warding attacker",
maxHealth: 100_000,
xpReward: 0,
attacks: [{
...DEFAULT_ENEMY_ATTACK,
id: attackId,
delivery: school === "physical" ? "melee" : "projectile",
animation: school === "physical" ? "attack" : "cast",
school,
range: 30,
castTimeMs: 0,
}],
});
setMobPosition(mobId, [8, 0, 0]);
useCombatStore.getState().damageMob(mobId, 1, 1_000);
const before = useCombatStore.getState().health;
useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0]);
return before - useCombatStore.getState().health;
};
const baseSpellHit = resolvedEnemyDamageAtRank(0, "shadow");
const wardedSpellHit = resolvedEnemyDamageAtRank(3, "shadow");
expect(baseSpellHit).toBeGreaterThan(0);
expect(wardedSpellHit).toBeLessThan(baseSpellHit);
const baseMeleeHit = resolvedEnemyDamageAtRank(0, "physical");
const wardedMeleeHit = resolvedEnemyDamageAtRank(3, "physical");
expect(baseMeleeHit).toBeGreaterThan(0);
expect(wardedMeleeHit).toBe(baseMeleeHit);
});
});