Update Healer Man server

This commit is contained in:
phenom committed 2026-10-02 10:38:12 -04:00
1 parent 4c768fbec6
commit b16821e92e
330 files changed
+35406 -1760

No files matched your search

+242 -60
View File
@@ -8,22 +8,30 @@ import { romEffectsForCast } from "./romCombatMechanics";
import { PLAYER_AGGRO_ID } from "./aggro";
import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial";
import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks";
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
import { PLAYER_EQUIPMENT_OWNER_ID } from "./equipment";
import { combatAttackTable } from "./combatResults";
import { usePartyStore } from "./partyStore";
import {
ROM_ROGUE_COERCE_DAMAGE_MULTIPLIER,
ROM_ROGUE_COERCE_ENERGY_COST_REDUCTION,
ROM_ROGUE_COERCE_LOW_BLOW_SPELL_ID,
ROM_ROGUE_COERCE_PASSIVE_SPELL_ID,
ROM_ROGUE_COERCE_TAG,
ROM_ROGUE_DAGGER_MASTERY_TAG,
ROM_ROGUE_INSTANT_SHADOW_READY_TAG,
ROM_ROGUE_OFF_HAND_MASTERY_TAG,
ROM_ROGUE_SHADOW_WALKER_PASSIVE_SPELL_ID,
ROM_ROGUE_TREASURE_HUNTER_TAG,
romRogueCoerceAbilityAdjustment,
romRogueCoerceLowBlowDamageMultiplier,
romRogueDaggerDamageMultiplier,
romRogueEnchantedThrowShouldProc,
romRogueShadowWalkerAdjustment,
romRogueTreasureHunterLootChance,
} from "./romRogueMechanics";
import {
ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG,
ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG,
ROM_ROGUE_PRIEST_SINISTER_METHODS_COOLDOWN_KEY,
ROM_ROGUE_PRIEST_SINISTER_METHODS_TAG,
romRogueMageDayOfRainBaseMissPercent,
@@ -127,7 +135,90 @@ describe("RoM Rogue source mechanics", () => {
.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([]);
expect(effectAuras("rom-rogue-primary-12")[0]).toEqual(expect.objectContaining({
kind: "apply-aura",
aura: expect.objectContaining({ utilityTags: [ROM_ROGUE_OFF_HAND_MASTERY_TAG] }),
}));
});
it("translates Coerce to a learned Rogue/Scout marker for current Low Blow behavior", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-scout", raceId: "rom-human", level: 80,
});
const passive = abilityById("rom-rogue-rom-scout-elite-45")!;
const [marker] = effectAuras(passive.id);
expect(marker?.kind === "apply-aura" && marker.aura.utilityTags).toContain(ROM_ROGUE_COERCE_TAG);
expect(marker?.kind === "apply-aura" && marker.aura.modifiers).toBeUndefined();
});
it("applies Coerce's sourced 20.2% Low Blow multiplier only to learned Rogue/Scout Low Blow", () => {
const context = {
classId: "rom-rogue",
abilitySource: "runewaker",
abilitySpellId: ROM_ROGUE_COERCE_LOW_BLOW_SPELL_ID,
passiveTags: [ROM_ROGUE_COERCE_TAG],
} as const;
expect(romRogueCoerceLowBlowDamageMultiplier(context)).toBe(ROM_ROGUE_COERCE_DAMAGE_MULTIPLIER);
expect(romRogueCoerceLowBlowDamageMultiplier({ ...context, abilitySpellId: 490324 })).toBe(1);
expect(romRogueCoerceLowBlowDamageMultiplier({ ...context, passiveTags: [] })).toBe(1);
});
it("reduces only learned Rogue/Scout Low Blow's Energy cost by the local sourced amount", () => {
const passiveSpellIds = [ROM_ROGUE_COERCE_PASSIVE_SPELL_ID];
expect(romRogueCoerceAbilityAdjustment(
"rom-rogue", "rom-scout", "runewaker", ROM_ROGUE_COERCE_LOW_BLOW_SPELL_ID, passiveSpellIds,
)).toMatchObject({ energyCostReduction: ROM_ROGUE_COERCE_ENERGY_COST_REDUCTION, cooldownIncreaseMs: 0 });
expect(romRogueCoerceAbilityAdjustment(
"rom-rogue", "rom-scout", "runewaker", 490324, passiveSpellIds,
).energyCostReduction).toBe(0);
expect(romRogueCoerceAbilityAdjustment(
"rom-rogue", "rom-priest", "runewaker", ROM_ROGUE_COERCE_LOW_BLOW_SPELL_ID, passiveSpellIds,
).energyCostReduction).toBe(0);
expect(romRogueCoerceAbilityAdjustment(
"rom-rogue", "rom-scout", "runewaker", ROM_ROGUE_COERCE_LOW_BLOW_SPELL_ID, [],
).energyCostReduction).toBe(0);
});
it("applies Coerce's reduced Low Blow Energy cost during a Rogue/Scout player cast", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-scout", raceId: "rom-human", level: 80,
});
const targetId = "rogue-coerce-target";
useCombatStore.getState().registerMob(targetId, {
name: "Coerce target", maxHealth: 100_000, level: 80, xpReward: 0,
});
setMobPosition(targetId, [0, 0, 2]);
useCombatStore.getState().selectMob(targetId);
useCombatStore.setState((state) => ({
gearStats: { ...state.gearStats, hitRating: 1_000_000 },
resourcePools: { ...state.resourcePools, energy: state.maxResourcePools.energy ?? 100 },
}));
const beforeEnergy = useCombatStore.getState().resourcePools.energy!;
expect(useCombatStore.getState().castAbility("rom-rogue-primary-1", { targetActorId: targetId, now: 1_000 }))
.toMatchObject({ ok: true });
expect(beforeEnergy - useCombatStore.getState().resourcePools.energy!).toBe(25);
});
it("scales Low Blow's damage effects by the current Coerce value for player and party cast contexts", () => {
const lowBlow = abilityById("rom-rogue-primary-1")!;
const context = {
sealStacks: 0,
bleedCount: 0,
charged: false,
mainHandDagger: false,
casterMaxHealth: 1_000,
targetHealthPercent: 1,
passiveTags: [ROM_ROGUE_COERCE_TAG],
} as const;
const base = romEffectsForCast(lowBlow, { ...context, passiveTags: [] });
const coerced = romEffectsForCast(lowBlow, context);
const baseDamage = base.filter((effect) => effect.kind === "damage");
const coercedDamage = coerced.filter((effect) => effect.kind === "damage");
expect(coercedDamage).toHaveLength(baseDamage.length);
expect(coercedDamage.map((effect) => effect.kind === "damage" && effect.coefficient))
.toEqual(baseDamage.map((effect) => effect.kind === "damage" && effect.coefficient * ROM_ROGUE_COERCE_DAMAGE_MULTIPLIER));
expect(romEffectsForCast(lowBlow, { ...context, passiveTags: [] }).filter((effect) => effect.kind === "damage"))
.toEqual(baseDamage);
});
it("learns Sinister Methods as a proc marker without an always-on critical aura", () => {
@@ -301,54 +392,143 @@ 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"
));
it("adds the CoA Fire packet from each landed Chaos Dagger hand and mitigates it as Fire", () => {
const weaponStats = { ...useCombatStore.getState().gearStats, hitRating: 1_000_000 };
const mainHandDamage = { damageMin: 100, damageMax: 100, damageSchool: "physical" as const, speedMs: 2_000 };
const offHandDamage = { damageMin: 40, damageMax: 40, damageSchool: "physical" as const, speedMs: 2_000 };
const mainHand = testWeapon("dagger", "chaos-dagger-player-main", { ...weaponStats, meleeWeapon: mainHandDamage });
const offHand = {
...testWeapon("dagger", "chaos-dagger-player-off", { ...weaponStats, meleeWeapon: offHandDamage }),
inventorySlot: "off-hand" as const,
};
clearMobRuntimeRegistry();
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 80,
inventory: [mainHand, offHand],
equipment: { player: { "main-hand": mainHand.instanceId, "off-hand": offHand.instanceId } },
});
const manaBefore = useCombatStore.getState().resourcePools.mana;
useCombatStore.getState().registerMob("chaos-dagger-target", {
name: "Chaos Dagger target", maxHealth: 1_000_000, level: 80,
schoolImmunities: ["fire"],
});
setMobPosition("chaos-dagger-target", [0, 0, 3]);
useCombatStore.setState({
autoAttackTargetId: "chaos-dagger-target",
nextAutoAttackAt: 1_500,
nextOffhandAutoAttackAt: 1_500,
});
useCombatStore.getState().tick(0.25, 1_500);
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);
const hits = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage" && event.targetActorId === "chaos-dagger-target"
));
const fireHits = hits.filter((event) => event.school === "fire");
expect(fireHits.map((event) => event.rawAmount).sort((a, b) => a - b)).toEqual([34, 85]);
expect(fireHits.every((event) => event.effectiveAmount === 0)).toBe(true);
expect(hits.some((event) => event.school === "physical" && event.effectiveAmount > 0)).toBe(true);
expect(useCombatStore.getState().resourcePools.mana).toBe(manaBefore);
});
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,
it.each([
["unlearned below level 15", 14, "rom-mage", 80],
["wrong secondary class", 80, "rom-warrior", 80],
["missed attack", 80, "rom-mage", 255],
] as const)("does not add Chaos Dagger Fire damage for %s", (_label, level, secondaryClassId, targetLevel) => {
clearMobRuntimeRegistry();
const gear = { ...useCombatStore.getState().gearStats, hitRating: targetLevel === 255 ? 0 : 1_000_000 };
const mainHand = testWeapon("dagger", `chaos-dagger-gated-main-${level}-${secondaryClassId}-${targetLevel}`, {
...gear,
meleeWeapon: { damageMin: 100, damageMax: 100, damageSchool: "physical" as const, speedMs: 2_000 },
});
const offHand = {
...testWeapon("dagger", `chaos-dagger-gated-off-${level}-${secondaryClassId}-${targetLevel}`, {
...gear,
meleeWeapon: { damageMin: 40, damageMax: 40, damageSchool: "physical" as const, speedMs: 2_000 },
}),
inventorySlot: "off-hand" as const,
};
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId, raceId: "rom-human", level,
inventory: [mainHand, offHand],
equipment: { player: { "main-hand": mainHand.instanceId, "off-hand": offHand.instanceId } },
});
useCombatStore.getState().registerMob("chaos-dagger-gated-target", {
name: "Target", maxHealth: 1_000_000, level: targetLevel,
});
setMobPosition("chaos-dagger-gated-target", [0, 0, 3]);
useCombatStore.setState({
autoAttackTargetId: "chaos-dagger-gated-target",
nextAutoAttackAt: 1_500,
nextOffhandAutoAttackAt: 1_500,
});
for (let now = 1_500; now <= 20_000; now += 250) {
useCombatStore.getState().tick(0.25, now);
if (now === 1_500 && targetLevel === 255) break;
}
const damage = useCombatStore.getState().combatEvents.filter((event) => (
event.kind === "damage" && event.targetActorId === "chaos-dagger-gated-target"
));
expect(damage.some((event) => event.school === "fire")).toBe(false);
});
it("applies Off Hand Mastery damage only to the RoM Rogue player's equipped off-hand swing", () => {
const simulateOffhand = (masteryActive: boolean) => {
const weaponStats = {
...useCombatStore.getState().gearStats,
hitRating: 1_000_000,
meleeWeapon: { damageMin: 2_000, damageMax: 2_000, damageSchool: "physical" as const, speedMs: 2_000 },
};
const mainHand = testWeapon("dagger", `rom-offhand-main-${masteryActive}`, weaponStats);
const offHand = {
...testWeapon("dagger", `rom-offhand-second-${masteryActive}`, weaponStats),
inventorySlot: "off-hand" as const,
};
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue",
raceId: "rom-human",
level: 80,
inventory: [mainHand, offHand],
equipment: { player: { "main-hand": mainHand.instanceId, "off-hand": offHand.instanceId } },
});
expect(useCombatStore.getState().auras.some((aura) => (
aura.definition.utilityTags?.includes(ROM_ROGUE_OFF_HAND_MASTERY_TAG)
))).toBe(true);
useCombatStore.getState().registerMob("rom-offhand-mastery-target", {
name: "Off-hand Mastery Target", maxHealth: 10_000_000, level: 80, xpReward: 0,
});
setMobPosition("rom-offhand-mastery-target", [0, 0, 3]);
useCombatStore.setState((state) => ({
...(masteryActive ? {} : { auras: state.auras.filter((aura) => (
!aura.definition.utilityTags?.includes(ROM_ROGUE_OFF_HAND_MASTERY_TAG)
)) }),
autoAttackTargetId: "rom-offhand-mastery-target",
nextAutoAttackAt: 1_500,
nextOffhandAutoAttackAt: 1_500,
}));
const presentations: CombatPresentationEvent[] = [];
const unsubscribe = subscribeCombatPresentation((message) => {
if (message.kind === "event") presentations.push(message.event);
});
for (let now = 1_500; now <= 20_000; now += 250) {
useCombatStore.getState().tick(0.25, now);
if (presentations.some((event) => event.abilityId === "player-auto-attack-offhand")) break;
}
unsubscribe();
const combat = useCombatStore.getState();
return {
offhandSwingPresented: presentations.some((event) => event.abilityId === "player-auto-attack-offhand"),
totalDamage: combat.combatEvents
.filter((event) => event.kind === "damage" && event.targetActorId === "rom-offhand-mastery-target")
.reduce((total, event) => total + event.effectiveAmount, 0),
};
};
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);
const withoutMastery = simulateOffhand(false);
const withMastery = simulateOffhand(true);
expect(withoutMastery.offhandSwingPresented).toBe(true);
expect(withMastery.offhandSwingPresented).toBe(true);
expect(withMastery.totalDamage).toBeGreaterThan(withoutMastery.totalDamage);
});
it("adds Treasure Hunter only to the supported bonus loot chance and clamps at one", () => {
@@ -391,13 +571,13 @@ describe("RoM Rogue source mechanics", () => {
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", () => {
it("applies Day of Rain's exact 50% miss rate only while Chaos Dagger is learned", () => {
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,
"rom-rogue", "rom-mage", dayOfRain.dbcSpellId, [ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG], 6,
);
expect(beforeBuff).toBeNull();
expect(withBuff).toBe(56);
@@ -410,12 +590,12 @@ describe("RoM Rogue source mechanics", () => {
rules: { baseMissChancePercent: withBuff! },
}).missPercent).toBe(50);
expect(romRogueMageDayOfRainBaseMissPercent(
"rom-rogue", "rom-warrior", dayOfRain.dbcSpellId, [ROM_ROGUE_MAGE_ENCHANTED_THROW_TAG], 6,
"rom-rogue", "rom-warrior", dayOfRain.dbcSpellId, [ROM_ROGUE_MAGE_CHAOS_DAGGER_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 => {
it("uses the learned Chaos Dagger hit penalty in Day of Rain's resolved combat result", () => {
const outcomeFor = (now: number, chaosDaggerLearned: boolean): string | undefined => {
clearMobRuntimeRegistry();
useCombatStore.getState().initializeCharacter({
classId: "rom-rogue", secondaryClassId: "rom-mage", raceId: "rom-human", level: 30,
@@ -423,8 +603,10 @@ describe("RoM Rogue source mechanics", () => {
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);
if (!chaosDaggerLearned) {
useCombatStore.setState((state) => ({
auras: state.auras.filter((aura) => !aura.definition.utilityTags?.includes(ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG)),
}));
}
expect(useCombatStore.getState().castAbility("rom-rogue-rom-mage-elite-30", undefined, now).ok).toBe(true);
return useCombatStore.getState().combatEvents.find((event) => (
@@ -444,17 +626,17 @@ describe("RoM Rogue source mechanics", () => {
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);
it("marks the learned Chaos Dagger passive for Day of Rain's hit-rate rider", () => {
const chaosDagger = abilityById("rom-rogue-rom-mage-elite-15")!;
expect(chaosDagger.passive).toBe(true);
expect(chaosDagger.costs).toEqual([]);
expect(chaosDagger.description).toContain("lists 25 MP");
expect(chaosDagger.description).toContain("no Mana charge is modeled");
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)
aura.definition.utilityTags?.includes(ROM_ROGUE_MAGE_CHAOS_DAGGER_TAG)
))).toBe(true);
});