Update Healer Man server

This commit is contained in:
phenom committed 2026-09-28 22:46:45 -04:00
1 parent 9d7e0e2f9e
commit 35dfdaed61
94 files changed
+17034 -418

No files matched your search

+307
View File
@@ -0,0 +1,307 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { abilityById } from "./abilityCatalog";
import { clearMobRuntimeRegistry, getMobRuntimeTransform, setMobPosition } from "./mobRuntimeRegistry";
import { useCombatStore } from "./combatStore";
import { LOOT_CATALOG } from "./lootCatalog";
import { abilityRequiresOutOfCombat } from "./abilityRules";
import { romEffectsForCast } from "./romCombatMechanics";
import { PLAYER_AGGRO_ID } from "./aggro";
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
import { PLAYER_EQUIPMENT_OWNER_ID } from "./equipment";
import {
ROM_ROGUE_DAGGER_MASTERY_TAG,
ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID,
ROM_ROGUE_TREASURE_HUNTER_TAG,
romRogueDaggerDamageMultiplier,
romRogueShadowWalkerAdjustment,
romRogueTreasureHunterLootChance,
} from "./romRogueMechanics";
function effectAuras(abilityId: string) {
return abilityById(abilityId)!.effects.filter((effect) => effect.kind === "apply-aura");
}
function testWeapon(weaponType: string, instanceId: string, stats = { ...useCombatStore.getState().gearStats }) {
return {
instanceId,
itemId: 1,
itemKey: instanceId,
name: `Test ${weaponType}`,
quality: "uncommon" as const,
inventorySlot: weaponType === "crossbow" ? "two-hand" : "one-hand",
armorType: null,
weaponType,
itemLevel: 1,
sourceItemLevel: 1,
stats,
quantity: 1,
sourceBossKey: "test",
sourceBossName: "Test",
acquiredAt: 1,
};
}
describe("RoM Rogue source mechanics", () => {
afterEach(() => resetCombatSpatialProvider());
beforeEach(() => {
resetCombatSpatialProvider();
clearMobRuntimeRegistry();
useCombatStore.getState().initializeCharacter({ classId: "rom-rogue", raceId: "rom-human", level: 80 });
useCombatStore.setState({ playerPosition: [0, 0, 0] });
registerCombatSpatialProvider({
actor: (id) => {
if (id === PLAYER_AGGRO_ID) return { id, position: useCombatStore.getState().playerPosition, yaw: 0 };
const transform = getMobRuntimeTransform(id);
return transform ? { id, ...transform } : null;
},
hasLineOfSight: () => true,
projectGroundPoint: (point) => point,
});
});
it("keeps Dagger Mastery, Treasure Hunter, and Off Hand Mastery as scoped passive tags", () => {
const daggerMastery = effectAuras("rom-rogue-primary-3")[0];
const treasureHunter = effectAuras("rom-rogue-primary-10")[0];
expect(daggerMastery?.kind === "apply-aura" && daggerMastery.aura.utilityTags)
.toContain(ROM_ROGUE_DAGGER_MASTERY_TAG);
expect(treasureHunter?.kind === "apply-aura" && treasureHunter.aura.utilityTags)
.toContain(ROM_ROGUE_TREASURE_HUNTER_TAG);
expect(abilityById("rom-rogue-primary-12")!.effects).toEqual([]);
});
it("applies Dagger Mastery only to the Rogue player's physical melee hits with a main-hand dagger", () => {
const context = {
classId: "rom-rogue",
sourceActorId: "player",
playerActorId: "player",
attackKind: "melee",
school: "physical",
mainHandDagger: true,
passiveTags: [ROM_ROGUE_DAGGER_MASTERY_TAG],
};
expect(romRogueDaggerDamageMultiplier(context)).toBe(1.1);
expect(romRogueDaggerDamageMultiplier({ ...context, mainHandDagger: false })).toBe(1);
expect(romRogueDaggerDamageMultiplier({ ...context, classId: "rom-scout" })).toBe(1);
expect(romRogueDaggerDamageMultiplier({ ...context, attackKind: "spell" })).toBe(1);
expect(romRogueDaggerDamageMultiplier({ ...context, passiveTags: [] })).toBe(1);
});
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);
expect(romRogueTreasureHunterLootChance(undefined, true)).toBeCloseTo(0.05);
expect(romRogueTreasureHunterLootChance(0.98, true)).toBe(1);
});
it("limits Shadow Walker's in-combat, cost, and cooldown rider to Rogue/Priest Sneak Attack", () => {
const sneakAttack = abilityById("rom-rogue-primary-9")!;
expect(romRogueShadowWalkerAdjustment(
"rom-rogue", "rom-priest", sneakAttack.source, sneakAttack.dbcSpellId,
[ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID],
)).toEqual({ allowsCombatUse: true, energyCostReduction: 10, cooldownIncreaseMs: 10_000 });
expect(romRogueShadowWalkerAdjustment(
"rom-rogue", "rom-scout", sneakAttack.source, sneakAttack.dbcSpellId,
[ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID],
)).toEqual({ allowsCombatUse: false, energyCostReduction: 0, cooldownIncreaseMs: 0 });
expect(romRogueShadowWalkerAdjustment(
"rom-rogue", "rom-priest", sneakAttack.source, sneakAttack.dbcSpellId, [],
)).toEqual({ allowsCombatUse: false, energyCostReduction: 0, cooldownIncreaseMs: 0 });
expect(romRogueShadowWalkerAdjustment(
"rom-rogue", "rom-priest", sneakAttack.source, 490306,
[ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID],
)).toEqual({ allowsCombatUse: false, energyCostReduction: 0, cooldownIncreaseMs: 0 });
});
it("adds one bonus boss loot roll only while Treasure Hunter is active", () => {
const bossKey = LOOT_CATALOG.bosses[0]!.bossKey;
const lootCount = (passiveActive: boolean) => {
useCombatStore.getState().initializeCharacter({ classId: "rom-rogue", raceId: "rom-human", level: 80 });
if (!passiveActive) {
useCombatStore.setState((state) => ({
auras: state.auras.filter((aura) => !aura.definition.utilityTags?.includes(ROM_ROGUE_TREASURE_HUNTER_TAG)),
}));
}
useCombatStore.getState().registerMob("rogue-treasure-target", {
name: "Treasure Target", boss: true, maxHealth: 10, lootSourceId: bossKey,
});
const random = vi.spyOn(Math, "random").mockReturnValue(0);
try {
useCombatStore.getState().damageMob("rogue-treasure-target", 10, 1_000);
} finally {
random.mockRestore();
}
return useCombatStore.getState().mobs["rogue-treasure-target"]!.pendingLoot.length;
};
const ordinaryDrops = lootCount(false);
const treasureHunterDrops = lootCount(true);
expect(treasureHunterDrops).toBe(ordinaryDrops + 1);
});
it("keeps Soul Stab's unconditional first hit and gates only the second hit on Weakened", () => {
const ability = abilityById("rom-rogue-rom-warlock-elite-25")!;
const context = {
sealStacks: 0,
bleedCount: 0,
charged: false,
mainHandDagger: false,
casterMaxHealth: 2_000,
targetHealthPercent: 1,
};
const ordinary = romEffectsForCast(ability, context).filter((effect) => effect.kind === "damage");
const weakened = romEffectsForCast(ability, { ...context, targetTags: ["rom-weakened"] })
.filter((effect) => effect.kind === "damage");
expect(ordinary).toHaveLength(1);
expect(weakened).toHaveLength(2);
});
it("marks Blind Spot and Sneak Attack as rear attacks and preserves Sneak Attack's combat gate", () => {
const blindSpot = abilityById("rom-rogue-primary-4")!;
const sneakAttack = abilityById("rom-rogue-primary-9")!;
expect(blindSpot.targeting?.requiresBehindTarget).toBe(true);
expect(sneakAttack.targeting?.requiresBehindTarget).toBe(true);
expect(abilityRequiresOutOfCombat(sneakAttack)).toBe(true);
});
it("rejects Blind Spot from the front and allows Sneak Attack from behind out of combat", () => {
const dagger = testWeapon("dagger", "rogue-position-dagger");
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", raceId: "rom-human", level: 80,
inventory: [dagger], equipment: { player: { "main-hand": dagger.instanceId } },
});
useCombatStore.getState().registerMob("rogue-rear-target", { name: "Target", maxHealth: 100_000 });
setMobPosition("rogue-rear-target", [0, 0, 0]);
useCombatStore.getState().selectMob("rogue-rear-target");
useCombatStore.setState((state) => ({
playerPosition: [0, 0, 2],
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 },
}));
expect(useCombatStore.getState().castAbility("rom-rogue-primary-4", undefined, 1_000))
.toMatchObject({ ok: false, reason: "must-be-behind" });
useCombatStore.setState((state) => ({ playerPosition: [0, 0, -2], resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 } }));
expect(useCombatStore.getState().castAbility("rom-rogue-primary-9", undefined, 2_000).ok).toBe(true);
});
it("rejects Sneak Attack while an enemy is already engaged", () => {
const dagger = testWeapon("dagger", "rogue-combat-dagger");
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", raceId: "rom-human", level: 80,
inventory: [dagger], equipment: { player: { "main-hand": dagger.instanceId } },
});
useCombatStore.getState().registerMob("rogue-engaged-target", { name: "Target", maxHealth: 100_000 });
setMobPosition("rogue-engaged-target", [0, 0, 0]);
useCombatStore.getState().selectMob("rogue-engaged-target");
useCombatStore.setState((state) => ({
playerPosition: [0, 0, -2],
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 },
mobs: { ...state.mobs, "rogue-engaged-target": { ...state.mobs["rogue-engaged-target"]!, engaged: true } },
}));
expect(useCombatStore.getState().castAbility("rom-rogue-primary-9", undefined, 1_000))
.toMatchObject({ ok: false, reason: "condition-not-met" });
});
it("applies Shadow Walker to combat Sneak Attack with the exact Energy and cooldown rider", () => {
const dagger = testWeapon("dagger", "rogue-shadow-walker-dagger");
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-priest", raceId: "rom-human", level: 80,
inventory: [dagger], equipment: { player: { "main-hand": dagger.instanceId } },
});
expect(useCombatStore.getState().abilities.some((candidate) => (
candidate.dbcSpellId === ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID && candidate.passive
))).toBe(true);
useCombatStore.getState().registerMob("rogue-shadow-walker-target", { name: "Target", maxHealth: 100_000 });
setMobPosition("rogue-shadow-walker-target", [0, 0, 0]);
useCombatStore.getState().selectMob("rogue-shadow-walker-target");
useCombatStore.setState((state) => ({
playerPosition: [0, 0, -2],
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 },
mobs: { ...state.mobs, "rogue-shadow-walker-target": { ...state.mobs["rogue-shadow-walker-target"]!, engaged: true } },
}));
expect(useCombatStore.getState().castAbility("rom-rogue-primary-9", undefined, 1_000).ok).toBe(true);
expect(useCombatStore.getState().resourcePools.energy).toBe(80);
expect(useCombatStore.getState().cooldowns["rom-rogue-primary-9"]).toBe(11_000);
});
it("enforces Energy Conservation's strict below-75% health threshold", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-warlock", raceId: "rom-human", level: 80,
});
const maximum = useCombatStore.getState().maxHealth;
const thresholdHealth = Math.ceil(maximum * 0.75);
useCombatStore.setState((state) => ({
health: thresholdHealth,
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 },
}));
expect(useCombatStore.getState().castAbility("rom-rogue-rom-warlock-elite-20", undefined, 1_000))
.toMatchObject({ ok: false, reason: "condition-not-met" });
useCombatStore.setState((state) => ({
health: thresholdHealth - 1,
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 },
}));
expect(useCombatStore.getState().castAbility("rom-rogue-rom-warlock-elite-20", undefined, 1_000).ok).toBe(true);
});
it("requires the Rogue/Knight Lion's Protection aura for Power of the Lion", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-knight", raceId: "rom-human", level: 80,
});
useCombatStore.setState((state) => ({
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100, mana: 100 },
}));
expect(useCombatStore.getState().castAbility("rom-rogue-rom-knight-elite-40", undefined, 1_000))
.toMatchObject({ ok: false, reason: "condition-not-met" });
expect(useCombatStore.getState().castAbility("rom-rogue-rom-knight-elite-25", undefined, 1_000).ok).toBe(true);
expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-491552")))
.toBe(true);
expect(useCombatStore.getState().castAbility("rom-rogue-rom-knight-elite-40", undefined, 2_600).ok).toBe(true);
});
it("increases Shadowstab damage only with Dagger Mastery and a main-hand dagger", () => {
const baselineStats = { ...useCombatStore.getState().gearStats };
const hitWith = (weaponType: string, passiveActive: boolean, instanceId: string) => {
const weapon = testWeapon(weaponType, instanceId, baselineStats);
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", raceId: "rom-human", level: 80,
inventory: [weapon], equipment: { player: { "main-hand": weapon.instanceId } },
});
const equipped = resolvedEquipmentLoadout(useCombatStore.getState().equipment, useCombatStore.getState().inventory, {
ownerId: PLAYER_EQUIPMENT_OWNER_ID, ownerKind: "player", classId: "rom-rogue",
specializationId: null, role: "damage", level: 80,
});
expect(equipped["main-hand"]?.weaponType).toBe(weaponType);
if (!passiveActive) {
useCombatStore.setState((state) => ({
auras: state.auras.filter((aura) => !aura.definition.utilityTags?.includes(ROM_ROGUE_DAGGER_MASTERY_TAG)),
}));
}
useCombatStore.getState().registerMob("rogue-dagger-mastery-target", { name: "Target", maxHealth: 100_000 });
setMobPosition("rogue-dagger-mastery-target", [0, 0, 3]);
useCombatStore.getState().selectMob("rogue-dagger-mastery-target");
useCombatStore.setState((state) => ({
resource: 100,
resourcePools: { ...state.resourcePools, energy: 100 },
}));
const before = useCombatStore.getState().mobs["rogue-dagger-mastery-target"]!.health;
const cast = useCombatStore.getState().castAbility("rom-rogue-general-0", undefined, 1_000);
if (!cast.ok) throw new Error(JSON.stringify(cast));
return before - useCombatStore.getState().mobs["rogue-dagger-mastery-target"]!.health;
};
const baseDaggerDamage = hitWith("dagger", false, "rogue-mastery-dagger-without-passive");
const daggerDamage = hitWith("dagger", true, "rogue-mastery-dagger-with-passive");
const baseNonDaggerDamage = hitWith("crossbow", false, "rogue-mastery-crossbow-without-passive");
const nonDaggerDamage = hitWith("crossbow", true, "rogue-mastery-crossbow-with-passive");
expect(daggerDamage).toBeGreaterThan(baseDaggerDamage);
expect(daggerDamage / baseDaggerDamage).toBeCloseTo(1.1, 1);
expect(nonDaggerDamage).toBe(baseNonDaggerDamage);
});
});