Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
@@ -1,10 +1,11 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, updatePartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { characterCombatDefender, PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
import { abilityTalentNodeId, preloadAbilityCatalogsForClasses } from "./abilityCatalog";
|
||||
import { applyAura } from "./combatAuras";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
|
||||
|
||||
function abilityId(name: string): string {
|
||||
@@ -97,6 +98,81 @@ describe("WoW 3.3.5 Paladin source-backed runtime mechanics", () => {
|
||||
expect(useCombatStore.getState().controlledUntil).toBe(14_450);
|
||||
});
|
||||
|
||||
it("makes Aura Mastery immune to incoming interrupts while a spell is casting", () => {
|
||||
const mobId = "paladin-aura-mastery-interrupt";
|
||||
useCombatStore.getState().registerMob(mobId, {
|
||||
name: "Interrupt attacker",
|
||||
maxHealth: 10_000,
|
||||
attacks: [{
|
||||
id: "interrupt",
|
||||
name: "Interrupt",
|
||||
delivery: "area",
|
||||
target: "primary",
|
||||
school: "shadow",
|
||||
animation: "cast",
|
||||
castTimeMs: 0,
|
||||
range: 30,
|
||||
cooldownMs: 10_000,
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [10_000, 10_000],
|
||||
damageMultiplier: 0,
|
||||
effect: { kind: "control", mechanic: "interrupt", durationMs: 3_000 },
|
||||
}],
|
||||
});
|
||||
setMobPosition(mobId, [2, 0, 0]);
|
||||
useCombatStore.getState().selectMob(mobId);
|
||||
useCombatStore.getState().damageMob(mobId, 1, 1_000);
|
||||
|
||||
const activate = useCombatStore.getState().castAbility(unlockTalentAbility("Aura Mastery"), undefined, 1_000);
|
||||
expect(activate).toMatchObject({ ok: true });
|
||||
const playerAura = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.controlImmunities?.includes("interrupt")
|
||||
));
|
||||
expect(playerAura).toBeDefined();
|
||||
|
||||
const member = usePartyStore.getState().members[0]!;
|
||||
updatePartyRuntimePosition(member.id, 4, 0, 0);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
const holyLight = useCombatStore.getState().castAbility(abilityId("Holy Light"), {
|
||||
targetActorId: member.id,
|
||||
now: 2_500,
|
||||
});
|
||||
expect(holyLight).toMatchObject({ ok: true });
|
||||
expect(useCombatStore.getState().activeCast).not.toBeNull();
|
||||
|
||||
useCombatStore.getState().advanceMobCombat(2_600);
|
||||
expect(useCombatStore.getState().controlMechanic).toBeNull();
|
||||
expect(useCombatStore.getState().activeCast).not.toBeNull();
|
||||
});
|
||||
|
||||
it("doubles the active Paladin aura on each Aura Mastery recipient for the generated spell duration", () => {
|
||||
const armor = () => characterCombatDefender(useCombatStore.getState()).armor ?? 0;
|
||||
const baseArmor = armor();
|
||||
expect(useCombatStore.getState().castAbility(abilityId("Devotion Aura"), undefined, 1_000))
|
||||
.toMatchObject({ ok: true });
|
||||
expect(armor() - baseArmor).toBe(1_205);
|
||||
|
||||
const mastery = useCombatStore.getState().castAbility(
|
||||
unlockTalentAbility("Aura Mastery"),
|
||||
undefined,
|
||||
2_500,
|
||||
);
|
||||
expect(mastery).toMatchObject({ ok: true });
|
||||
|
||||
const partyMember = usePartyStore.getState().members[0]!;
|
||||
const masteryAuras = useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.definition.utilityTags?.includes("wow335-aura-mastery-aura-effectiveness")
|
||||
));
|
||||
for (const targetId of [PLAYER_AURA_ENTITY_ID, partyMember.id]) {
|
||||
expect(masteryAuras.some((aura) => aura.targetId === targetId && aura.expiresAt === 12_500)).toBe(true);
|
||||
}
|
||||
expect(armor() - baseArmor).toBe(2_410);
|
||||
|
||||
useCombatStore.getState().tick(0.1, 12_501);
|
||||
expect(armor() - baseArmor).toBe(1_205);
|
||||
});
|
||||
|
||||
it("lets Redemption target and revive a defeated party member at the rank's source health", () => {
|
||||
const member = usePartyStore.getState().members[0]!;
|
||||
usePartyStore.setState((party) => ({
|
||||
@@ -162,6 +238,173 @@ describe("WoW 3.3.5 Paladin source-backed runtime mechanics", () => {
|
||||
expect(result).toMatchObject({ ok: false, reason: "condition-not-met" });
|
||||
});
|
||||
|
||||
it("adds Redoubt's generated shield block chance only while a shield is equipped", () => {
|
||||
setTalentRank("Redoubt", 2);
|
||||
const withoutShield = characterCombatDefender(useCombatStore.getState()).blockPercent ?? 0;
|
||||
const shieldId = "paladin-redoubt-block-test";
|
||||
const shield = {
|
||||
instanceId: shieldId,
|
||||
itemId: 1,
|
||||
itemKey: shieldId,
|
||||
name: "Redoubt Test Shield",
|
||||
quality: "uncommon" as const,
|
||||
inventorySlot: "off-hand",
|
||||
armorType: "shield",
|
||||
weaponType: null,
|
||||
itemLevel: 1,
|
||||
sourceItemLevel: 1,
|
||||
stats: { ...EMPTY_ITEM_STATS, blockValue: 40 },
|
||||
quantity: 1,
|
||||
sourceBossKey: "test",
|
||||
sourceBossName: "Test",
|
||||
acquiredAt: 1,
|
||||
};
|
||||
useCombatStore.setState((state) => ({
|
||||
inventory: [shield],
|
||||
equipment: { player: { "off-hand": shieldId } },
|
||||
gearStats: { ...state.gearStats, blockValue: 40 },
|
||||
}));
|
||||
const withShield = characterCombatDefender(useCombatStore.getState()).blockPercent ?? 0;
|
||||
|
||||
expect(withShield - withoutShield).toBe(12);
|
||||
});
|
||||
|
||||
it("procs Redoubt from a shield block with the generated rank values and duration", () => {
|
||||
setTalentRank("Redoubt", 3);
|
||||
const shieldId = "paladin-redoubt-proc-test";
|
||||
const shield = {
|
||||
instanceId: shieldId,
|
||||
itemId: 1,
|
||||
itemKey: shieldId,
|
||||
name: "Redoubt Proc Shield",
|
||||
quality: "uncommon" as const,
|
||||
inventorySlot: "off-hand",
|
||||
armorType: "shield",
|
||||
weaponType: null,
|
||||
itemLevel: 1,
|
||||
sourceItemLevel: 1,
|
||||
stats: { ...EMPTY_ITEM_STATS, blockRating: 1_000_000, blockValue: 40 },
|
||||
quantity: 1,
|
||||
sourceBossKey: "test",
|
||||
sourceBossName: "Test",
|
||||
acquiredAt: 1,
|
||||
};
|
||||
useCombatStore.setState((state) => ({
|
||||
inventory: [shield],
|
||||
equipment: { player: { "off-hand": shieldId } },
|
||||
gearStats: { ...state.gearStats, blockRating: 1_000_000, blockValue: 40 },
|
||||
}));
|
||||
const mobId = "paladin-redoubt-proc-attacker";
|
||||
useCombatStore.getState().registerMob(mobId, {
|
||||
name: "Redoubt attacker",
|
||||
level: 80,
|
||||
maxHealth: 100_000,
|
||||
attacks: [{
|
||||
id: "redoubt-proc-melee",
|
||||
name: "Melee attack",
|
||||
delivery: "melee",
|
||||
target: "primary",
|
||||
school: "physical",
|
||||
animation: "attack",
|
||||
castTimeMs: 0,
|
||||
range: 8,
|
||||
cooldownMs: 500,
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [500, 500],
|
||||
damageMultiplier: 0.1,
|
||||
}],
|
||||
});
|
||||
setMobPosition(mobId, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(mobId, 1, 1_000);
|
||||
|
||||
let redoubt = undefined as ReturnType<typeof useCombatStore.getState>["auras"][number] | undefined;
|
||||
for (let now = 1_450; now <= 30_000; now += 250) {
|
||||
useCombatStore.getState().advanceMobCombat(now);
|
||||
redoubt = useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.utilityTags?.includes("wow335-paladin-redoubt")
|
||||
));
|
||||
if (redoubt) break;
|
||||
}
|
||||
|
||||
expect(useCombatStore.getState().combatEvents.some((event) => (
|
||||
event.kind === "damage" && event.targetActorId === PLAYER_AURA_ENTITY_ID && event.outcome === "block"
|
||||
))).toBe(true);
|
||||
expect(redoubt).toBeDefined();
|
||||
expect(redoubt?.expiresAt).toBe(redoubt!.appliedAt + 10_000);
|
||||
expect(redoubt?.definition.modifiers).toContainEqual({
|
||||
kind: "damage", direction: "taken", operation: "percent", value: -0.06,
|
||||
});
|
||||
expect(redoubt?.definition.modifiers).toContainEqual({
|
||||
kind: "stat", stat: "strength", operation: "percent", value: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
it("spends Holy Shield's eight source-defined charges on successful blocks", () => {
|
||||
const shieldId = "paladin-holy-shield-charge-test";
|
||||
const shield = {
|
||||
instanceId: shieldId,
|
||||
itemId: 1,
|
||||
itemKey: shieldId,
|
||||
name: "Test Shield",
|
||||
quality: "uncommon" as const,
|
||||
inventorySlot: "off-hand",
|
||||
armorType: "shield",
|
||||
weaponType: null,
|
||||
itemLevel: 1,
|
||||
sourceItemLevel: 1,
|
||||
stats: { ...EMPTY_ITEM_STATS, blockRating: 1_000_000, blockValue: 40 },
|
||||
quantity: 1,
|
||||
sourceBossKey: "test",
|
||||
sourceBossName: "Test",
|
||||
acquiredAt: 1,
|
||||
};
|
||||
useCombatStore.setState((state) => ({
|
||||
inventory: [shield],
|
||||
equipment: { player: { "off-hand": shieldId } },
|
||||
gearStats: { ...state.gearStats, blockRating: 1_000_000, blockValue: 40 },
|
||||
}));
|
||||
|
||||
const targetId = "paladin-holy-shield-charge-target";
|
||||
useCombatStore.getState().registerMob(targetId, {
|
||||
name: "Melee attacker",
|
||||
level: 80,
|
||||
maxHealth: 100_000,
|
||||
attacks: [{
|
||||
id: "holy-shield-charge-melee",
|
||||
name: "Melee attack",
|
||||
delivery: "melee",
|
||||
target: "primary",
|
||||
school: "physical",
|
||||
animation: "attack",
|
||||
castTimeMs: 0,
|
||||
range: 8,
|
||||
cooldownMs: 500,
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [500, 500],
|
||||
damageMultiplier: 0.1,
|
||||
}],
|
||||
});
|
||||
setMobPosition(targetId, [2, 0, 0]);
|
||||
useCombatStore.getState().damageMob(targetId, 1, 1_000);
|
||||
|
||||
const holyShieldId = unlockTalentAbility("Holy Shield");
|
||||
const cast = useCombatStore.getState().castAbility(holyShieldId, undefined, 1_000);
|
||||
expect(cast, JSON.stringify(cast)).toMatchObject({ ok: true });
|
||||
const findHolyShieldProc = () => useCombatStore.getState().auras.find((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& aura.definition.procs?.some((proc) => proc.id.endsWith(":holy-shield"))
|
||||
));
|
||||
expect(findHolyShieldProc()?.procStates[0]?.remainingCharges).toBe(8);
|
||||
|
||||
for (let now = 1_450; now <= 10_000; now += 250) {
|
||||
useCombatStore.getState().advanceMobCombat(now);
|
||||
if (findHolyShieldProc()?.procStates[0]?.remainingCharges === 0) break;
|
||||
}
|
||||
|
||||
expect(findHolyShieldProc()?.procStates[0]?.remainingCharges).toBe(0);
|
||||
});
|
||||
|
||||
it("procs three 8-second Reckoning charges from melee hits and adds one landed autoattack per charge", () => {
|
||||
setTalentRank("Reckoning", 5);
|
||||
const mobId = "paladin-reckoning-trigger";
|
||||
|
||||
Reference in new issue
Block a user