Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,195 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { abilitiesForCharacter, abilityAtLevel, type AbilityAmountScaling } from "./abilityCatalog";
|
||||
import { createConfiguredPartyMember } from "./partyStore";
|
||||
import { resolvedEquipmentStats } from "./heirloomEquipment";
|
||||
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
|
||||
import { applyRomNativeStatModifiers } from "./romNativeStats";
|
||||
import { resolveRomNativeTrainingWeaponProfile } from "./romNativeDamage";
|
||||
import { nativeDamageCasterAmount, nativeDisarmedMainHandAutoAttackProfile, nativePeriodicDamageCasterSnapshot,
|
||||
romNativeAmountBinding } from "./romNativeCombatAmount";
|
||||
import { romNativePeriodicDamageBinding } from "./romNativePeriodicDamage";
|
||||
import { deriveCharacterStats } from "./wotlkStats";
|
||||
|
||||
describe("native RoM unarmed Disarm profile", () => {
|
||||
it("uses the source Level*2 main-hand base and 30-tenths cadence for every active-five player and party NPC", () => {
|
||||
const level = 30;
|
||||
for (const [index, identity] of ROM_FIVE_IDENTITIES.entries()) {
|
||||
const playerGear = resolvedEquipmentStats({}, [], {
|
||||
ownerId: "native-disarm-player",
|
||||
ownerKind: "player",
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
specializationId: null,
|
||||
role: identity.role,
|
||||
level,
|
||||
});
|
||||
const player = deriveCharacterStats(identity.classId, level, playerGear, identity.raceId, {},
|
||||
identity.secondaryClassId, "male", "rom").romNative;
|
||||
const member = createConfiguredPartyMember({
|
||||
...identity,
|
||||
statRuleset: "rom",
|
||||
specialization: "Primary",
|
||||
talentTreeId: "",
|
||||
}, level, index, "native-disarm-active-five");
|
||||
const partyNpc = deriveCharacterStats(member.classId, member.level, member.gearStats, member.raceId, {},
|
||||
member.secondaryClassId, member.gender, member.statRuleset).romNative;
|
||||
|
||||
for (const [owner, stats] of [["player", player], ["party-npc", partyNpc]] as const) {
|
||||
const profile = nativeDisarmedMainHandAutoAttackProfile(stats);
|
||||
expect(profile, `${owner}:${identity.classId}`).toBeDefined();
|
||||
if (!profile || !stats) continue;
|
||||
const training = resolveRomNativeTrainingWeaponProfile(stats, { trainingGearProfileMatches: true });
|
||||
expect(training.status, `${owner}:${identity.classId}`).toBe("supported");
|
||||
if (training.status !== "supported") continue;
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
slot: "main-hand",
|
||||
sourceDamageChannel: "Main_DMG",
|
||||
attackSpeed: 30,
|
||||
intervalMs: 3_000,
|
||||
sourceItemIds: [],
|
||||
sourceSemantics: "runewaker-roleattr-unarmed-main-hand",
|
||||
unmodeledRateChannels: [
|
||||
"Main_DMG_Per", "DMG_AllWeapon", "DMG_Unarmed", "AtkSpeed", "Main_ATKSpeed_Per",
|
||||
"AttackSpeed_AllWeapon", "AttackSpeed_Unarmed",
|
||||
],
|
||||
});
|
||||
expect(profile.damage).toBeCloseTo(Math.max(1,
|
||||
(training.value.residualFlatDamage + level * 2) * stats.rateFactors.physicalDamage));
|
||||
expect(profile.physicalAttack).toBe(stats.physicalAttack);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("retains the native nonweapon DMG remainder and tracked DMG_Per factor", () => {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-knight")!;
|
||||
const level = 30;
|
||||
const gear = resolvedEquipmentStats({}, [], {
|
||||
ownerId: "native-disarm-rate",
|
||||
ownerKind: "player",
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
specializationId: null,
|
||||
role: identity.role,
|
||||
level,
|
||||
});
|
||||
const native = deriveCharacterStats(identity.classId, level, gear, identity.raceId, {},
|
||||
identity.secondaryClassId, "male", "rom").romNative!;
|
||||
const modified = applyRomNativeStatModifiers(native, {
|
||||
channels: { physicalDamage: 100 },
|
||||
rates: { physicalDamage: 1.5 },
|
||||
});
|
||||
const training = resolveRomNativeTrainingWeaponProfile(modified, { trainingGearProfileMatches: true });
|
||||
const profile = nativeDisarmedMainHandAutoAttackProfile(modified);
|
||||
|
||||
expect(training.status).toBe("supported");
|
||||
expect(profile).toBeDefined();
|
||||
if (training.status !== "supported" || !profile) return;
|
||||
expect(training.value.residualFlatDamage).toBeGreaterThan(0);
|
||||
expect(profile.damage).toBeCloseTo(Math.max(1,
|
||||
(training.value.residualFlatDamage + level * 2) * modified.rateFactors.physicalDamage));
|
||||
expect(profile.damage).toBeGreaterThan(nativeDisarmedMainHandAutoAttackProfile(native)!.damage);
|
||||
});
|
||||
|
||||
it("fails closed when native training gear provenance is not verified", () => {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-knight")!;
|
||||
const gear = resolvedEquipmentStats({}, [], {
|
||||
ownerId: "native-disarm-custom",
|
||||
ownerKind: "player",
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
specializationId: null,
|
||||
role: identity.role,
|
||||
level: 30,
|
||||
});
|
||||
const customGear = {
|
||||
...gear,
|
||||
romNative: { ...gear.romNative, physicalDamage: (gear.romNative?.physicalDamage ?? 0) + 1 },
|
||||
};
|
||||
const custom = deriveCharacterStats(identity.classId, 30, customGear, identity.raceId, {},
|
||||
identity.secondaryClassId, "male", "rom").romNative;
|
||||
|
||||
expect(nativeDisarmedMainHandAutoAttackProfile(undefined)).toBeUndefined();
|
||||
expect(nativeDisarmedMainHandAutoAttackProfile(custom)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("substitutes source unarmed inputs only in native ability branches that read Main_DMG/Main_ATKSpeed", () => {
|
||||
const basicAttacks = ["Holy Strike", "Rising Tide", "Shadowstab", "Shot", "Flame"];
|
||||
let mainHandConsumers = 0;
|
||||
let unchangedRangedConsumers = 0;
|
||||
const mainHandConsumerClasses: string[] = [];
|
||||
for (const [index, identity] of ROM_FIVE_IDENTITIES.entries()) {
|
||||
const level = 30;
|
||||
const gear = resolvedEquipmentStats({}, [], { ownerId: "native-disarm-direct", ownerKind: "player",
|
||||
classId: identity.classId, secondaryClassId: identity.secondaryClassId,
|
||||
specializationId: null, role: identity.role, level });
|
||||
const stats = deriveCharacterStats(identity.classId, level, gear, identity.raceId, {},
|
||||
identity.secondaryClassId, "male", "rom").romNative!;
|
||||
const ability = abilityAtLevel(abilitiesForCharacter(identity.classId, identity.secondaryClassId)
|
||||
.find((candidate) => candidate.name === basicAttacks[index])!, level);
|
||||
const effect = ability.effects.find((candidate) => candidate.kind === "damage");
|
||||
expect(effect?.kind, identity.classId).toBe("damage");
|
||||
if (effect?.kind !== "damage" || !effect.romNativeSource) continue;
|
||||
const binding = romNativeAmountBinding(effect.romNativeSource);
|
||||
const profile = nativeDisarmedMainHandAutoAttackProfile(stats);
|
||||
expect(binding, identity.classId).not.toBeNull();
|
||||
expect(profile, identity.classId).toBeDefined();
|
||||
if (!binding || !profile) continue;
|
||||
const consumesMainHand = binding.component.calculationType === 0 || binding.component.calculationType === 9
|
||||
|| (binding.component.fixedValue !== 0 && binding.component.fixedType === 2);
|
||||
const trainingAmount = nativeDamageCasterAmount(effect, level, stats);
|
||||
const unarmedAmount = nativeDamageCasterAmount(effect, level, stats, undefined, profile);
|
||||
expect(trainingAmount, identity.classId).not.toBeNull();
|
||||
expect(unarmedAmount, identity.classId).not.toBeNull();
|
||||
if (consumesMainHand) {
|
||||
mainHandConsumers += 1;
|
||||
mainHandConsumerClasses.push(identity.classId);
|
||||
expect(unarmedAmount, identity.classId).not.toBe(trainingAmount);
|
||||
expect(nativeDamageCasterAmount(effect, level, stats, undefined,
|
||||
{ ...profile, damage: profile.damage + 1 }), identity.classId).toBeNull();
|
||||
} else {
|
||||
unchangedRangedConsumers += 1;
|
||||
expect(unarmedAmount, identity.classId).toBe(trainingAmount);
|
||||
}
|
||||
}
|
||||
expect(mainHandConsumers).toBeGreaterThanOrEqual(2);
|
||||
expect(mainHandConsumerClasses).toEqual(expect.arrayContaining(["rom-knight", "rom-rogue"]));
|
||||
expect(unchangedRangedConsumers).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("uses unarmed Main_DMG and attack speed for native physical DotMagic rank correction", () => {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-rogue")!;
|
||||
const level = 30;
|
||||
const gear = resolvedEquipmentStats({}, [], { ownerId: "native-disarm-dot", ownerKind: "player",
|
||||
classId: identity.classId, secondaryClassId: identity.secondaryClassId,
|
||||
specializationId: null, role: identity.role, level });
|
||||
const caster = deriveCharacterStats(identity.classId, level, gear, identity.raceId, {},
|
||||
identity.secondaryClassId, "male", "rom").romNative!;
|
||||
const target = {
|
||||
attributes: { wisdom: caster.attributes.wisdom },
|
||||
health: { max: 10_000 },
|
||||
physicalAttack: caster.physicalAttack,
|
||||
physicalDamage: caster.physicalDamage,
|
||||
physicalDefense: caster.physicalDefense,
|
||||
magicalAttack: caster.magicalAttack,
|
||||
magicalDamage: caster.magicalDamage,
|
||||
magicalDefense: caster.magicalDefense,
|
||||
healing: { ...caster.healing, targetHealAbsorbRate: 0, targetHealAbsorb: 0 },
|
||||
sourceClassId: caster.sourceInputs.classId,
|
||||
};
|
||||
const source = romNativePeriodicDamageBinding({ skillId: 490306, componentId: 620313, kind: "dot" });
|
||||
expect(source?.component).toMatchObject({ effectType: 1, dotType: 0, isFixDotPower: false, dotSkillLevelArg: 45 });
|
||||
const profile = nativeDisarmedMainHandAutoAttackProfile(caster);
|
||||
expect(profile).toBeDefined();
|
||||
if (!profile) return;
|
||||
const scaling = { romNativeSource: { kind: "dot", skillId: 490306, componentId: 620313 } } as AbilityAmountScaling;
|
||||
const trainingSnapshot = nativePeriodicDamageCasterSnapshot(scaling, level, caster, target, level);
|
||||
const unarmedSnapshot = nativePeriodicDamageCasterSnapshot(scaling, level, caster, target,
|
||||
level, undefined, undefined, profile);
|
||||
expect(trainingSnapshot).toBeDefined();
|
||||
expect(unarmedSnapshot).toBeDefined();
|
||||
expect(unarmedSnapshot?.magicLevelAdjustment).not.toBe(trainingSnapshot?.magicLevelAdjustment);
|
||||
expect(nativePeriodicDamageCasterSnapshot(scaling, level, caster, target, level,
|
||||
undefined, undefined, { ...profile, attackSpeed: profile.attackSpeed + 1 })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user