Files
healer-man/src/game/romNativeHealing.integration.test.ts
T
2026-10-04 13:13:35 -04:00

472 lines
22 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import {
auraAdjustedCharacterStats,
equipmentAdjustedCombatAmount,
PLAYER_AURA_ENTITY_ID,
useCombatStore,
} from "./combatStore";
import { clearMobRuntimeRegistry } from "./mobRuntimeRegistry";
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
import {
nativeHolyHealTargetAmount,
romNativeAmountBinding,
romNativeMagicLevel,
nativePeriodicHealSnapshot,
} from "./romNativeCombatAmount";
import { resolveRomNativeHolyHeal } from "./romNativeHealing";
import { ROM_PRIEST_HOLY_BLESSING_TAG, romPriestHolyBlessingWisdom } from "./romPriestHolyBlessingRuntime";
import { resolveHealingValue } from "./combatAuras";
import { EMPTY_ITEM_STATS } from "./itemStats";
import { abilityAtLevel, abilityById } from "./abilityCatalog";
import { deriveCharacterStats } from "./wotlkStats";
const LEVEL = 30;
const CAST_NOW = 10_000;
const NATIVE_CASTER_BONUS = Object.freeze({ magicalDamage: 240, healPowerRateFactor: 1.35 });
const NATIVE_RECIPIENT_BONUS = Object.freeze({ healAbsorbRate: 27, healAbsorb: 19 });
const HEAL_ABILITY_IDS = ["rom-priest-primary-2", "rom-priest-general-0"] as const;
const REGENERATE_ABILITY_ID = "rom-priest-general-2";
function nativeGear(gearStats: typeof EMPTY_ITEM_STATS, native: Readonly<Record<string, number>>) {
return {
...gearStats,
romNative: { ...gearStats.romNative, ...native },
};
}
function directHealAbility(abilityId: string) {
const catalog = abilityById(abilityId);
if (!catalog) throw new Error(`Missing RoM heal ability ${abilityId}.`);
const ability = abilityAtLevel(catalog, LEVEL);
const effect = ability.effects.find((candidate) => candidate.kind === "heal");
if (!effect || effect.kind !== "heal" || !effect.romNativeSource) {
throw new Error(`${ability.name} has no source-linked direct heal effect.`);
}
return { ability, effect };
}
function regenerateAbility() {
const catalog = abilityById(REGENERATE_ABILITY_ID);
if (!catalog) throw new Error("Missing RoM Priest Regenerate ability.");
const ability = abilityAtLevel(catalog, LEVEL);
const effect = ability.effects.find((candidate) => candidate.kind === "hot");
if (!effect || effect.kind !== "hot" || effect.romNativeSource?.kind !== "hot") {
throw new Error("Regenerate has no source-linked native HoT effect.");
}
return { ability, effect };
}
function expectedDirectSourceAmount(
abilityId: string,
effectiveCaster: NonNullable<ReturnType<typeof deriveCharacterStats>["romNative"]>,
) {
const { effect } = directHealAbility(abilityId);
const sourceDescriptor = effect.romNativeSource;
if (!sourceDescriptor) throw new Error(`${abilityId} has no native source descriptor.`);
const binding = romNativeAmountBinding(sourceDescriptor);
if (!binding) throw new Error(`No source binding for ${sourceDescriptor.skillId}:${sourceDescriptor.componentId}.`);
const { rank, component } = binding;
return resolveRomNativeHolyHeal({
source: {
componentId: component.componentId,
sourceSkillId: rank.sourceSkillId,
damagePower: component.damagePower,
damagePowerSkillLevelArg: component.damagePowerSkillLevelArg,
magicLevel: romNativeMagicLevel(LEVEL),
spellTimeSeconds: rank.spellTimeSeconds,
isIgnoreDmgEffect: rank.isIgnoreDmgEffect,
isFixDmgEffect: rank.isFixDmgEffect,
ignoreMagicAbsorb: component.ignoreMagicAbsorb,
randomRange: component.randomRange,
},
caster: effectiveCaster,
target: {
healing: { ...effectiveCaster.healing, targetHealAbsorbRate: 0, targetHealAbsorb: 0 },
},
randomUnit: 0.5,
}).amount;
}
function preparePartyTarget(seed: string) {
const identity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.role === "tank")!;
const base = createConfiguredPartyMember({
...identity,
gender: "male",
specialization: "Primary",
statRuleset: "rom",
}, LEVEL, 0, seed);
const target = {
...base,
gearStats: nativeGear(base.gearStats, NATIVE_RECIPIENT_BONUS),
health: 1,
status: "ready" as const,
activeCast: null,
activeCastTargetId: null,
nextActionAt: 0,
};
usePartyStore.setState({ members: [target] });
registerPartyRuntimePosition(target.id, [2, 0, 0]);
useCombatStore.getState().synchronizeActors();
return usePartyStore.getState().members[0]!;
}
function installCasterGear() {
const state = useCombatStore.getState();
const gearStats = nativeGear(state.gearStats, NATIVE_CASTER_BONUS);
useCombatStore.setState({ gearStats });
return gearStats;
}
function playerEffectiveStats(now: number) {
const state = useCombatStore.getState();
const base = deriveCharacterStats(
state.classId, state.level, state.gearStats, state.raceId, {}, state.secondaryClassId,
state.gender, state.statRuleset,
);
const effective = auraAdjustedCharacterStats(base, state.auras, PLAYER_AURA_ENTITY_ID, now).romNative;
if (!effective) throw new Error("Expected native RoM Priest stats.");
return { base: base.romNative!, effective, auras: state.auras };
}
function npcEffectiveStats(memberId: string, now: number) {
const state = useCombatStore.getState();
const member = usePartyStore.getState().members.find((candidate) => candidate.id === memberId);
if (!member) throw new Error(`Missing party member ${memberId}.`);
const base = deriveCharacterStats(
member.classId, member.level, member.gearStats, member.raceId, {}, member.secondaryClassId,
member.gender, member.statRuleset ?? "wow",
);
const effective = auraAdjustedCharacterStats(base, state.auras, memberId, now).romNative;
if (!effective) throw new Error("Expected native RoM party Priest stats.");
return { base: base.romNative!, effective, auras: state.auras };
}
function expectedRecipientAdjustedAmount(
abilityId: string,
effectiveCaster: NonNullable<ReturnType<typeof deriveCharacterStats>["romNative"]>,
effectiveTarget: NonNullable<ReturnType<typeof deriveCharacterStats>["romNative"]>,
auras: readonly import("./combatAuras").ActiveAura[],
sourceActorId: string,
targetActorId: string,
) {
const { effect } = directHealAbility(abilityId);
const source = expectedDirectSourceAmount(abilityId, effectiveCaster);
const targetAdjusted = nativeHolyHealTargetAmount(Math.round(source), effect, effectiveTarget);
const done = resolveHealingValue(targetAdjusted, auras.filter((aura) => aura.targetId === sourceActorId), "done");
const taken = resolveHealingValue(done, auras.filter((aura) => aura.targetId === targetActorId), "taken");
return Math.round(taken);
}
function assertBlessedCasterInputs(base: NonNullable<ReturnType<typeof deriveCharacterStats>["romNative"]>, effective: NonNullable<ReturnType<typeof deriveCharacterStats>["romNative"]>, auras: readonly { targetId: string; definition: { utilityTags?: readonly string[] } }[], targetAuraId: string) {
expect(auras.some((aura) => aura.targetId === targetAuraId
&& aura.definition.utilityTags?.includes(ROM_PRIEST_HOLY_BLESSING_TAG)),
`missing Holy Blessing on ${targetAuraId}; present target auras: ${JSON.stringify(auras.filter(aura => aura.targetId === targetAuraId).map(aura => aura.definition.utilityTags))}`).toBe(true);
expect(effective.attributes.wisdom).toBeCloseTo(romPriestHolyBlessingWisdom(base.attributes.wisdom), 8);
const noGear = deriveCharacterStats(
"rom-priest", LEVEL, EMPTY_ITEM_STATS, "rom-human", {}, "rom-knight", "male", "rom",
).romNative!;
expect(effective.magicalDamage).toBeGreaterThan(noGear.magicalDamage);
expect(effective.healing.healPowerRate).toBeGreaterThan(noGear.healing.healPowerRate);
}
describe("native RoM Priest direct-heal integration", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetPartyRuntimeClock();
usePartyStore.setState({ members: [] });
useCombatStore.getState().initializeCharacter({
classId: "rom-priest",
raceId: "rom-human",
gender: "male",
role: "healer",
level: LEVEL,
statRuleset: "rom",
});
usePartyStore.getState().initializeParty("rom-native-heal-direct", LEVEL,
useCombatStore.getState().health, "healer", 1, "rom");
installCasterGear();
});
it.each(HEAL_ABILITY_IDS)("resolves a real player cast of %s from native source and recipient inputs", (abilityId) => {
const target = preparePartyTarget(`rom-native-player-heal:${abilityId}`);
const { ability, effect } = directHealAbility(abilityId);
const { base, effective } = playerEffectiveStats(CAST_NOW);
const targetBase = deriveCharacterStats(
target.classId, target.level, target.gearStats, target.raceId, {}, target.secondaryClassId,
target.gender, target.statRuleset ?? "wow",
);
const targetEffective = auraAdjustedCharacterStats(
targetBase, useCombatStore.getState().auras, target.id, CAST_NOW,
).romNative!;
assertBlessedCasterInputs(base, effective, useCombatStore.getState().auras, PLAYER_AURA_ENTITY_ID);
expect(targetEffective.healing.targetHealAbsorbRate).toBe(NATIVE_RECIPIENT_BONUS.healAbsorbRate);
expect(targetEffective.healing.targetHealAbsorb).toBe(NATIVE_RECIPIENT_BONUS.healAbsorb);
const expectedRaw = expectedRecipientAdjustedAmount(abilityId, effective, targetEffective,
useCombatStore.getState().auras, PLAYER_AURA_ENTITY_ID, target.id);
const preCastHealth = target.health;
const started = useCombatStore.getState().castAbility(ability.id, {
targetActorId: target.id,
now: CAST_NOW,
});
expect(started.ok, JSON.stringify(started)).toBe(true);
if (useCombatStore.getState().activeCast?.abilityId === ability.id) {
useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt);
}
const healing = useCombatStore.getState().combatEvents.find((event) => event.kind === "healing"
&& event.sourceActorId === PLAYER_AGGRO_ID
&& event.targetActorId === target.id
&& event.abilityId === ability.id);
expect(healing).toBeDefined();
expect(healing?.rawAmount).toBe(Math.round(expectedRaw * (healing?.critical ? 1.5 : 1)));
expect(healing?.effectiveAmount).toBeGreaterThan(0);
expect(usePartyStore.getState().members.find((member) => member.id === target.id)!.health)
.toBe(preCastHealth + healing!.effectiveAmount);
expect(effect.romNativeSource?.kind).toBe("heal");
});
it.each(HEAL_ABILITY_IDS)("resolves a real companion cast of %s with its own blessing and recipient", (abilityId) => {
const priestIdentity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-priest")!;
const tankIdentity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.role === "tank")!;
const healerBase = createConfiguredPartyMember({
...priestIdentity,
gender: "male",
specialization: "Primary",
statRuleset: "rom",
}, LEVEL, 0, `rom-native-npc-heal:${abilityId}`);
const { ability } = directHealAbility(abilityId);
const healer = {
...healerBase,
gearStats: nativeGear(healerBase.gearStats, NATIVE_CASTER_BONUS),
status: "healing" as const,
activeCast: {
abilityId,
school: "holy" as const,
mode: "cast" as const,
startedAt: CAST_NOW - 1_000,
completesAt: CAST_NOW,
originalDurationMs: 1_000,
pushbackHits: 0,
origin: [0, 0, 0] as const,
targetId: "pending-target",
},
activeCastTargetId: "pending-target",
nextActionAt: CAST_NOW,
};
const targetBase = createConfiguredPartyMember({
...tankIdentity,
gender: "male",
specialization: "Primary",
statRuleset: "rom",
}, LEVEL, 1, `rom-native-npc-target:${abilityId}`);
const target = {
...targetBase,
gearStats: nativeGear(targetBase.gearStats, NATIVE_RECIPIENT_BONUS),
health: 1,
status: "ready" as const,
activeCast: null,
activeCastTargetId: null,
nextActionAt: 0,
};
const configuredHealer = {
...healer,
activeCast: { ...healer.activeCast, targetId: target.id },
activeCastTargetId: target.id,
};
const configuredTarget = target;
usePartyStore.setState({ members: [configuredHealer, configuredTarget] });
registerPartyRuntimePosition(configuredHealer.id, [0, 0, 0]);
registerPartyRuntimePosition(configuredTarget.id, [2, 0, 0]);
useCombatStore.getState().synchronizeActors();
const synchronizedMembers = usePartyStore.getState().members;
const synchronizedHealer = synchronizedMembers.find((member) => member.id === healer.id)!;
const synchronizedTarget = synchronizedMembers.find((member) => member.id === configuredTarget.id)!;
const firstCastState = useCombatStore.getState();
expect(firstCastState.auras.some((aura) => aura.targetId === healer.id
&& aura.definition.utilityTags?.includes(ROM_PRIEST_HOLY_BLESSING_TAG))).toBe(false);
advanceDungeonPartyCombat(CAST_NOW);
const finishedState = useCombatStore.getState();
const { base, effective } = npcEffectiveStats(synchronizedHealer.id, CAST_NOW);
const recipientBase = deriveCharacterStats(
synchronizedTarget.classId, synchronizedTarget.level, synchronizedTarget.gearStats,
synchronizedTarget.raceId, {}, synchronizedTarget.secondaryClassId, synchronizedTarget.gender, "rom",
);
const recipientEffective = auraAdjustedCharacterStats(
recipientBase, finishedState.auras, synchronizedTarget.id, CAST_NOW + 5_000,
).romNative!;
assertBlessedCasterInputs(base, effective, finishedState.auras, synchronizedHealer.id);
expect(recipientEffective.healing.targetHealAbsorbRate).toBe(NATIVE_RECIPIENT_BONUS.healAbsorbRate);
expect(recipientEffective.healing.targetHealAbsorb).toBe(NATIVE_RECIPIENT_BONUS.healAbsorb);
const expectedRaw = expectedRecipientAdjustedAmount(abilityId, effective, recipientEffective,
finishedState.auras, synchronizedHealer.id, synchronizedTarget.id);
const healing = finishedState.combatEvents.find((event) => event.kind === "healing"
&& event.sourceActorId === synchronizedHealer.id
&& event.targetActorId === synchronizedTarget.id
&& event.abilityId === abilityId);
expect(healing, `${abilityId} companion cast did not resolve`).toBeDefined();
expect(healing?.rawAmount).toBe(Math.round(expectedRaw * (healing?.critical ? 1.5 : 1)));
expect(healing?.effectiveAmount).toBeGreaterThan(0);
expect(ability.id).toBe(abilityId);
});
it("snapshots a player Regenerate tick with native target absorb and does not apply it again per tick", () => {
const target = preparePartyTarget("rom-native-player-regenerate");
const { ability, effect } = regenerateAbility();
const { effective } = playerEffectiveStats(CAST_NOW);
const targetBase = deriveCharacterStats(
target.classId, target.level, target.gearStats, target.raceId, {}, target.secondaryClassId,
target.gender, target.statRuleset ?? "wow",
);
const targetEffective = auraAdjustedCharacterStats(
targetBase, useCombatStore.getState().auras, target.id, CAST_NOW,
).romNative!;
const snapshot = nativePeriodicHealSnapshot(effect, LEVEL, effective, targetEffective, target.level);
expect(snapshot).toBeDefined();
expect(snapshot?.tickAmount).toBeGreaterThan(0);
const started = useCombatStore.getState().castAbility(ability.id, {
targetActorId: target.id,
now: CAST_NOW,
});
expect(started.ok, JSON.stringify(started)).toBe(true);
if (useCombatStore.getState().activeCast?.abilityId === ability.id) {
useCombatStore.getState().tick(0.1, useCombatStore.getState().activeCast!.completesAt);
}
const applied = useCombatStore.getState().effects.find((candidate) => candidate.kind === "hot"
&& candidate.sourceAbilityId === ability.id && candidate.targetId === target.id);
expect(applied).toMatchObject({ fixedAmount: snapshot!.tickAmount, remainingTicks: snapshot!.dotTimes });
const auras = useCombatStore.getState().auras;
const expectedTickRaw = Math.round(resolveHealingValue(
resolveHealingValue(snapshot!.tickAmount, auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID), "done"),
auras.filter((aura) => aura.targetId === target.id), "taken",
));
useCombatStore.getState().tick(0.1, applied!.nextTickAt);
const tick = useCombatStore.getState().combatEvents.find((event) => event.kind === "healing"
&& event.sourceActorId === PLAYER_AGGRO_ID && event.targetActorId === target.id
&& event.abilityId === ability.id);
expect(tick?.rawAmount).toBe(expectedTickRaw);
expect(tick?.rawAmount).toBeLessThan(Math.round(snapshot!.tickAmount
* (1 + targetEffective.healing.targetHealAbsorbRate / 100) + targetEffective.healing.targetHealAbsorb));
});
it("records self Regenerate ticks with the same effective healing as the health ledger", () => {
const { ability } = regenerateAbility();
useCombatStore.setState({ health: 1 });
useCombatStore.getState().synchronizeActors();
const started = useCombatStore.getState().castAbility(ability.id, {
now: CAST_NOW,
});
expect(started.ok, JSON.stringify(started)).toBe(true);
const applied = useCombatStore.getState().effects.find((candidate) => candidate.kind === "hot"
&& candidate.sourceAbilityId === ability.id && !candidate.targetId);
expect(applied).toBeDefined();
const healthBefore = useCombatStore.getState().health;
useCombatStore.getState().tick(0, applied!.nextTickAt);
const finished = useCombatStore.getState();
const tick = finished.combatEvents.find((event) => event.kind === "healing"
&& event.sourceActorId === PLAYER_AGGRO_ID && event.targetActorId === PLAYER_AGGRO_ID
&& event.abilityId === ability.id);
expect(tick?.effectiveAmount).toBeGreaterThan(0);
expect(tick?.effectiveAmount).toBe(finished.health - healthBefore);
expect(tick!.effectiveAmount + tick!.overheal).toBe(tick!.rawAmount);
});
it("snapshots a companion Regenerate tick from the completed player-build cast", () => {
const targetBase = createConfiguredPartyMember({
...ROM_FIVE_IDENTITIES.find((candidate) => candidate.role === "tank")!,
gender: "male", specialization: "Primary", statRuleset: "rom",
}, LEVEL, 1, "rom-native-npc-regenerate-target");
const target = {
...targetBase,
gearStats: nativeGear(targetBase.gearStats, NATIVE_RECIPIENT_BONUS),
health: 1,
status: "ready" as const,
activeCast: null,
activeCastTargetId: null,
nextActionAt: 0,
};
const healerBase = createConfiguredPartyMember({
...ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-priest")!,
gender: "male", specialization: "Primary", statRuleset: "rom",
}, LEVEL, 0, "rom-native-npc-regenerate-healer");
const { ability } = regenerateAbility();
const healer = {
...healerBase,
gearStats: nativeGear(healerBase.gearStats, NATIVE_CASTER_BONUS),
status: "healing" as const,
activeCast: {
abilityId: ability.id,
school: "holy" as const,
mode: "cast" as const,
startedAt: CAST_NOW - 1_000,
completesAt: CAST_NOW,
originalDurationMs: 1_000,
pushbackHits: 0,
origin: [0, 0, 0] as const,
targetId: target.id,
},
activeCastTargetId: target.id,
nextActionAt: CAST_NOW,
};
usePartyStore.setState({ members: [healer, target] });
registerPartyRuntimePosition(healer.id, [0, 0, 0]);
registerPartyRuntimePosition(target.id, [2, 0, 0]);
useCombatStore.getState().synchronizeActors();
const [synchronizedHealer, synchronizedTarget] = usePartyStore.getState().members;
advanceDungeonPartyCombat(CAST_NOW);
const finished = useCombatStore.getState();
const casterBase = deriveCharacterStats(
synchronizedHealer!.classId, synchronizedHealer!.level, synchronizedHealer!.gearStats,
synchronizedHealer!.raceId, {}, synchronizedHealer!.secondaryClassId,
synchronizedHealer!.gender, "rom",
);
const effectiveCaster = auraAdjustedCharacterStats(
casterBase, finished.auras, synchronizedHealer!.id, CAST_NOW,
).romNative!;
const recipientBase = deriveCharacterStats(
synchronizedTarget!.classId, synchronizedTarget!.level, synchronizedTarget!.gearStats,
synchronizedTarget!.raceId, {}, synchronizedTarget!.secondaryClassId,
synchronizedTarget!.gender, "rom",
);
const effectiveRecipient = auraAdjustedCharacterStats(
recipientBase, finished.auras, synchronizedTarget!.id, CAST_NOW,
).romNative!;
const { effect } = regenerateAbility();
const snapshot = nativePeriodicHealSnapshot(
effect, LEVEL, effectiveCaster, effectiveRecipient, synchronizedTarget!.level,
);
expect(snapshot).toBeDefined();
const applied = finished.effects.find((candidate) => candidate.kind === "hot"
&& candidate.sourceAbilityId === ability.id && candidate.targetId === synchronizedTarget!.id);
expect(applied).toMatchObject({ fixedAmount: snapshot!.tickAmount, remainingTicks: snapshot!.dotTimes });
});
it("keeps the same legacy amount when source metadata is present under the WoW ruleset", () => {
const { ability, effect } = directHealAbility("rom-priest-primary-2");
const gear = {
...EMPTY_ITEM_STATS,
spellPower: 725,
romNative: NATIVE_CASTER_BONUS,
};
const { romNativeSource: _ignored, ...withoutDescriptor } = effect;
const withMetadata = equipmentAdjustedCombatAmount(
effect, LEVEL, gear, "rom-priest", ability, 0, undefined, "rom-human", "male", "wow", null,
);
const legacyEffect = equipmentAdjustedCombatAmount(
withoutDescriptor, LEVEL, gear, "rom-priest", ability, 0, undefined, "rom-human", "male", "wow", null,
);
expect(withMetadata).toBe(legacyEffect);
});
});