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

+344
View File
@@ -7,16 +7,75 @@ import { abilityRequiresOutOfCombat } from "./abilityRules";
import { romEffectsForCast } from "./romCombatMechanics";
import { PLAYER_AGGRO_ID } from "./aggro";
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
import { PLAYER_EQUIPMENT_OWNER_ID } from "./equipment";
import { combatAttackTable } from "./combatResults";
import { usePartyStore } from "./partyStore";
import {
ROM_ROGUE_DAGGER_MASTERY_TAG,
ROM_ROGUE_INSTANT_SHADOW_READY_TAG,
ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID,
ROM_ROGUE_TREASURE_HUNTER_TAG,
romRogueDaggerDamageMultiplier,
romRogueEnchantedThrowShouldProc,
romRogueShadowWalkerAdjustment,
romRogueTreasureHunterLootChance,
} from "./romRogueMechanics";
import {
ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG,
ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY,
ROM_ROGUE_PRIEST_SINISTER_METHODS_TAG,
romRogueMageDayOfRainBaseMissPercent,
romScoutRogueProcActionSpec,
} from "./romScoutRogueOverrides";
const SINISTER_METHODS_BUFF_ID = "rom-pair-sinister-methods-critical";
function prepareSinisterMethodsTarget(
abilityId: "rom-rogue-primary-1" | "rom-rogue-primary-4" | "rom-rogue-primary-2",
mobLevel = 80,
criticalStrikeRating = 0,
) {
const behindTarget = abilityId === "rom-rogue-primary-4";
const dagger = behindTarget ? testWeapon("dagger", "rogue-sinister-methods-dagger") : null;
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue",
secondaryClassId: "rom-priest",
raceId: "rom-human",
level: 80,
...(dagger ? { inventory: [dagger], equipment: { player: { "main-hand": dagger.instanceId } } } : {}),
});
useCombatStore.setState((state) => ({
playerPosition: behindTarget ? [0, 0, -2] : [0, 0, 0],
gearStats: {
...state.gearStats,
hitRating: mobLevel >= 200 ? 0 : 1_000_000,
criticalStrikeRating,
},
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy ?? 100 },
}));
const targetId = "rogue-sinister-methods-target";
useCombatStore.getState().registerMob(targetId, {
name: "Sinister Methods target",
maxHealth: 100_000,
level: mobLevel,
xpReward: 0,
});
setMobPosition(targetId, behindTarget ? [0, 0, 0] : [0, 0, 2]);
useCombatStore.getState().selectMob(targetId);
return targetId;
}
function sinisterMethodsBuff() {
return useCombatStore.getState().auras.find((aura) => (
aura.targetId === PLAYER_AGGRO_ID && aura.definition.id === SINISTER_METHODS_BUFF_ID
));
}
function castRogueAbilityAtTarget(abilityId: string, targetActorId: string, now: number) {
return useCombatStore.getState().castAbility(abilityId, { targetActorId, now });
}
function effectAuras(abilityId: string) {
return abilityById(abilityId)!.effects.filter((effect) => effect.kind === "apply-aura");
@@ -71,6 +130,160 @@ describe("RoM Rogue source mechanics", () => {
expect(abilityById("rom-rogue-primary-12")!.effects).toEqual([]);
});
it("learns Sinister Methods as a proc marker without an always-on critical aura", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-priest", raceId: "rom-human", level: 80,
});
const passive = abilityById("rom-rogue-rom-priest-elite-45")!;
const [marker] = effectAuras(passive.id);
expect(marker?.kind === "apply-aura" && marker.aura.utilityTags)
.toContain(ROM_ROGUE_PRIEST_SINISTER_METHODS_TAG);
expect(marker?.kind === "apply-aura" && marker.aura.procs?.[0]?.triggers).toEqual(["hit", "crit"]);
expect(marker?.kind === "apply-aura" && marker.aura.modifiers).toBeUndefined();
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === SINISTER_METHODS_BUFF_ID)).toBe(false);
useCombatStore.getState().initializeCharacter({ classId: "rom-rogue", raceId: "rom-human", level: 80 });
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.utilityTags?.includes(ROM_ROGUE_PRIEST_SINISTER_METHODS_TAG)
))).toBe(false);
});
it.each([
["Low Blow", "rom-rogue-primary-1"],
["Blind Spot", "rom-rogue-primary-4"],
] as const)("procs Sinister Methods after a landed %s", (_name, abilityId) => {
const targetId = prepareSinisterMethodsTarget(abilityId);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === SINISTER_METHODS_BUFF_ID)).toBe(false);
const castResult = castRogueAbilityAtTarget(abilityId, targetId, 1_000);
expect(castResult, JSON.stringify(castResult)).toMatchObject({ ok: true });
const hit = useCombatStore.getState().combatEvents.find((event) => (
event.kind === "damage" && event.abilityId === abilityId
));
expect(hit?.outcome).not.toBe("miss");
expect(sinisterMethodsBuff()).toMatchObject({
appliedAt: 1_000,
expiresAt: 11_000,
definition: {
modifiers: [{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: 20 }],
},
});
expect(useCombatStore.getState().cooldowns[ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY]).toBe(21_000);
});
it("also procs Sinister Methods on a landed critical hit", () => {
const targetId = prepareSinisterMethodsTarget("rom-rogue-primary-1", 80, 1_000_000);
const castResult = castRogueAbilityAtTarget("rom-rogue-primary-1", targetId, 1_000);
expect(castResult, JSON.stringify(castResult)).toMatchObject({ ok: true });
expect(useCombatStore.getState().combatEvents.find((event) => (
event.kind === "damage" && event.abilityId === "rom-rogue-primary-1"
))?.outcome).toBe("crit");
expect(sinisterMethodsBuff()?.appliedAt).toBe(1_000);
});
it("does not proc or start its cooldown on a miss or another ability", () => {
const targetId = prepareSinisterMethodsTarget("rom-rogue-primary-1", 200);
expect(castRogueAbilityAtTarget("rom-rogue-primary-1", targetId, 1_000).ok).toBe(true);
expect(useCombatStore.getState().combatEvents.find((event) => (
event.kind === "damage" && event.abilityId === "rom-rogue-primary-1"
))?.outcome).toBe("miss");
expect(sinisterMethodsBuff()).toBeUndefined();
expect(useCombatStore.getState().cooldowns[ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY]).toBeUndefined();
const otherTargetId = prepareSinisterMethodsTarget("rom-rogue-primary-2");
expect(castRogueAbilityAtTarget("rom-rogue-primary-2", otherTargetId, 3_000).ok).toBe(true);
expect(sinisterMethodsBuff()).toBeUndefined();
expect(useCombatStore.getState().cooldowns[ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY]).toBeUndefined();
useCombatStore.setState((state) => ({
gearStats: { ...state.gearStats, hitRating: 1_000_000 },
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy ?? 100 },
}));
expect(castRogueAbilityAtTarget("rom-rogue-primary-1", otherTargetId, 5_000).ok).toBe(true);
expect(sinisterMethodsBuff()?.appliedAt).toBe(5_000);
});
it("honors Sinister Methods duration and 20-second cooldown expiry", () => {
const targetId = prepareSinisterMethodsTarget("rom-rogue-primary-1");
expect(castRogueAbilityAtTarget("rom-rogue-primary-1", targetId, 1_000).ok).toBe(true);
expect(sinisterMethodsBuff()?.expiresAt).toBe(11_000);
useCombatStore.setState((state) => ({
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy ?? 100 },
}));
expect(castRogueAbilityAtTarget("rom-rogue-primary-4", targetId, 12_000).ok).toBe(true);
expect(sinisterMethodsBuff()).toMatchObject({ appliedAt: 1_000, expiresAt: 11_000 });
expect(useCombatStore.getState().cooldowns[ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY]).toBe(21_000);
useCombatStore.setState((state) => ({
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy ?? 100 },
}));
expect(castRogueAbilityAtTarget("rom-rogue-primary-4", targetId, 21_000).ok).toBe(true);
expect(sinisterMethodsBuff()).toMatchObject({ appliedAt: 21_000, expiresAt: 31_000 });
expect(useCombatStore.getState().cooldowns[ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY]).toBe(41_000);
});
it("arms Instant Shadow on a successful Rogue/Mage dodge and consumes it for its three-second concealment", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 80,
});
const ability = abilityById("rom-rogue-rom-mage-elite-60")!;
useCombatStore.getState().resetSettings();
usePartyStore.getState().initializeParty("rom-instant-shadow-tests", 1, useCombatStore.getState().health);
resetCombatSpatialProvider();
const beforeDodge = useCombatStore.getState().castAbility(ability.id, undefined, 1_000);
expect(beforeDodge).toMatchObject({ ok: false, reason: "condition-not-met" });
useCombatStore.getState().registerMob("rogue-instant-shadow-attacker", {
name: "Instant Shadow attacker", maxHealth: 100_000, xpReward: 0, attacks: [DEFAULT_ENEMY_ATTACK],
});
setMobPosition("rogue-instant-shadow-attacker", [0, 0, 1]);
useCombatStore.setState((state) => ({
gearStats: { ...state.gearStats, dodgeRating: 1_000_000 },
resource: state.maxResource,
resourcePools: {
...state.resourcePools,
energy: state.maxResourcePools.energy,
mana: state.maxResourcePools.mana,
},
}));
useCombatStore.getState().damageMob("rogue-instant-shadow-attacker", 1, 1_100);
let dodgedAt: number | null = null;
for (let now = 1_450; now < 20_000; now += 100) {
useCombatStore.getState().advanceMobCombat(now);
if (useCombatStore.getState().lastCombatResult?.outcome === "dodge") {
dodgedAt = now;
break;
}
}
expect(dodgedAt, JSON.stringify({ health: useCombatStore.getState().health,
lastCombatResult: useCombatStore.getState().lastCombatResult,
mob: useCombatStore.getState().mobs["rogue-instant-shadow-attacker"] })).not.toBeNull();
expect(useCombatStore.getState().auras.some((aura) => (
aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.utilityTags?.includes(ROM_ROGUE_INSTANT_SHADOW_READY_TAG)
))).toBe(true);
const beforeCast = useCombatStore.getState();
const manaCost = Math.round((beforeCast.maxResourcePools.mana ?? 0) * 0.06);
const castAt = dodgedAt! + 100;
expect(useCombatStore.getState().castAbility(ability.id, undefined, castAt))
.toMatchObject({ ok: true, abilityId: ability.id });
const afterCast = useCombatStore.getState();
expect(afterCast.resourcePools.energy).toBe((beforeCast.resourcePools.energy ?? 0) - 15);
expect(afterCast.resourcePools.mana).toBe((beforeCast.resourcePools.mana ?? 0) - manaCost);
expect(afterCast.cooldowns[ability.id]).toBe(castAt + ability.cooldownMs);
expect(afterCast.auras.some((aura) => (
aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.utilityTags?.includes(ROM_ROGUE_INSTANT_SHADOW_READY_TAG)
))).toBe(false);
expect(afterCast.auras.some((aura) => (
aura.targetId === PLAYER_AGGRO_ID
&& aura.definition.utilityTags?.includes("stealth")
&& aura.expiresAt === castAt + 3_000
))).toBe(true);
});
it("applies Dagger Mastery only to the Rogue player's physical melee hits with a main-hand dagger", () => {
const context = {
classId: "rom-rogue",
@@ -88,6 +301,56 @@ describe("RoM Rogue source mechanics", () => {
expect(romRogueDaggerDamageMultiplier({ ...context, passiveTags: [] })).toBe(1);
});
it("adds the referenced Throw payload to landed Rogue/Mage normal attacks while Enchanted Throw is active", () => {
const simulateAutoAttacks = (enchanted: boolean) => {
clearMobRuntimeRegistry();
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 30,
});
useCombatStore.getState().registerMob("enchanted-throw-target", { name: "Target", maxHealth: 1_000_000 });
setMobPosition("enchanted-throw-target", [0, 0, 3]);
if (enchanted) {
expect(useCombatStore.getState().castAbility("rom-rogue-rom-mage-elite-15", undefined, 1_000).ok).toBe(true);
}
useCombatStore.setState({ autoAttackTargetId: "enchanted-throw-target", nextAutoAttackAt: 1_500 });
for (let now = 1_500; now <= 20_000; now += 250) {
useCombatStore.getState().tick(0.25, now);
if (useCombatStore.getState().combatEvents.some((event) => (
event.kind === "damage" && event.abilityId === "rom-rogue-general-1"
))) break;
}
return useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage" && event.targetActorId === "enchanted-throw-target"
));
};
const enchantedHits = simulateAutoAttacks(true);
expect(enchantedHits.some((event) => event.abilityId === "rom-rogue-general-1"
&& event.school === "physical" && event.effectiveAmount > 0)).toBe(true);
const ordinaryHits = simulateAutoAttacks(false);
expect(ordinaryHits.some((event) => event.abilityId === undefined)).toBe(true);
expect(ordinaryHits.some((event) => event.abilityId === "rom-rogue-general-1")).toBe(false);
});
it("only qualifies landed Rogue/Mage player normal attacks for Enchanted Throw", () => {
const context = {
classId: "rom-rogue",
secondaryClassId: "rom-mage",
sourceActorId: "player",
playerActorId: "player",
attackKind: "melee",
isAutoAttack: true,
attackLanded: true,
enchantedThrowActive: true,
};
expect(romRogueEnchantedThrowShouldProc(context)).toBe(true);
expect(romRogueEnchantedThrowShouldProc({ ...context, attackLanded: false })).toBe(false);
expect(romRogueEnchantedThrowShouldProc({ ...context, enchantedThrowActive: false })).toBe(false);
expect(romRogueEnchantedThrowShouldProc({ ...context, isAutoAttack: false })).toBe(false);
expect(romRogueEnchantedThrowShouldProc({ ...context, secondaryClassId: "rom-warrior" })).toBe(false);
});
it("adds Treasure Hunter only to the supported bonus loot chance and clamps at one", () => {
expect(romRogueTreasureHunterLootChance(0.2, false)).toBe(0.2);
expect(romRogueTreasureHunterLootChance(0.2, true)).toBeCloseTo(0.25);
@@ -114,6 +377,87 @@ describe("RoM Rogue source mechanics", () => {
)).toEqual({ allowsCombatUse: false, energyCostReduction: 0, cooldownIncreaseMs: 0 });
});
it("gives Rogue/Mage Throw its source-backed one-second Day of Rain cooldown reduction", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 30,
});
useCombatStore.getState().registerMob("rogue-day-of-rain-target", { name: "Target", maxHealth: 100_000 });
setMobPosition("rogue-day-of-rain-target", [0, 0, 1]);
useCombatStore.getState().selectMob("rogue-day-of-rain-target");
expect(useCombatStore.getState().castAbility("rom-rogue-rom-mage-elite-30", undefined, 1_000).ok).toBe(true);
expect(useCombatStore.getState().cooldowns["rom-rogue-rom-mage-elite-30"]).toBe(26_000);
expect(useCombatStore.getState().castAbility("rom-rogue-general-1", undefined, 2_600).ok).toBe(true);
expect(useCombatStore.getState().cooldowns["rom-rogue-rom-mage-elite-30"]).toBe(25_000);
});
it("applies Day of Rain's exact 50% miss rate only while Enchanted Throw is active", () => {
const dayOfRain = abilityById("rom-rogue-rom-mage-elite-30")!;
const beforeBuff = romRogueMageDayOfRainBaseMissPercent(
"rom-rogue", "rom-mage", dayOfRain.dbcSpellId, [], 6,
);
const withBuff = romRogueMageDayOfRainBaseMissPercent(
"rom-rogue", "rom-mage", dayOfRain.dbcSpellId, [ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG], 6,
);
expect(beforeBuff).toBeNull();
expect(withBuff).toBe(56);
expect(combatAttackTable({
amount: 100,
attackKind: "ranged",
damageSchool: "physical",
attacker: { level: 80, hitPercent: 6, criticalChancePercent: 0 },
defender: { level: 80, dodgePercent: 0, parryPercent: 0, blockPercent: 0, criticalAvoidancePercent: 0 },
rules: { baseMissChancePercent: withBuff! },
}).missPercent).toBe(50);
expect(romRogueMageDayOfRainBaseMissPercent(
"rom-rogue", "rom-warrior", dayOfRain.dbcSpellId, [ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG], 6,
)).toBeNull();
});
it("uses the Enchanted Throw hit penalty in Day of Rain's resolved combat result", () => {
const outcomeFor = (now: number, enchantedThrow: boolean): string | undefined => {
clearMobRuntimeRegistry();
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 30,
});
useCombatStore.getState().registerMob("rogue-day-rain-roll-target", { name: "Target", maxHealth: 100_000 });
setMobPosition("rogue-day-rain-roll-target", [0, 0, 1]);
useCombatStore.getState().selectMob("rogue-day-rain-roll-target");
if (enchantedThrow) {
expect(useCombatStore.getState().castAbility("rom-rogue-rom-mage-elite-15", undefined, 1_000).ok).toBe(true);
}
expect(useCombatStore.getState().castAbility("rom-rogue-rom-mage-elite-30", undefined, now).ok).toBe(true);
return useCombatStore.getState().combatEvents.find((event) => (
event.kind === "damage" && event.abilityId === "rom-rogue-rom-mage-elite-30"
))?.outcome;
};
let foundEnchantedOnlyMiss = false;
for (let now = 3_000; now < 3_100; now += 1) {
const ordinaryOutcome = outcomeFor(now, false);
const enchantedOutcome = outcomeFor(now, true);
if (ordinaryOutcome !== "miss" && ordinaryOutcome !== undefined && enchantedOutcome === "miss") {
foundEnchantedOnlyMiss = true;
break;
}
}
expect(foundEnchantedOnlyMiss).toBe(true);
});
it("marks the active Rogue/Mage Enchanted Throw buff for its Day of Rain hit-rate rider", () => {
const enchantedThrow = abilityById("rom-rogue-rom-mage-elite-15")!;
const enchantedAuras = enchantedThrow.effects.filter((effect) => effect.kind === "apply-aura");
expect(enchantedAuras.length).toBeGreaterThan(0);
expect(enchantedAuras.every((effect) => !effect.aura.procs?.length)).toBe(true);
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 15,
});
expect(useCombatStore.getState().castAbility("rom-rogue-rom-mage-elite-15", undefined, 1_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.utilityTags?.includes(ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG)
))).toBe(true);
});
it("adds one bonus boss loot roll only while Treasure Hunter is active", () => {
const bossKey = LOOT_CATALOG.bosses[0]!.bossKey;
const lootCount = (passiveActive: boolean) => {