472 lines
22 KiB
TypeScript
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);
|
|
});
|
|
});
|