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>) { 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["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["romNative"]>, effectiveTarget: NonNullable["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["romNative"]>, effective: NonNullable["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); }); });