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