import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, isAbilityUnlocked, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog"; import { PLAYER_AGGRO_ID } from "./aggro"; import { characterCombatDefender, useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; import { commitPartyAbilityUse } from "./partyAbilityAi"; import { applyAura as applyCombatAura } from "./combatAuras"; import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES } from "./romAbilityCatalog"; import { romPriestKnightAdditionalAbilities, romPriestKnightCastEffects } from "./romPriestKnightOverrides"; import { ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, ROM_KNIGHT_MASTER_OF_PARRY_TAG } from "./romKnightOverrides"; const PAIRED_SKILLS = [ ["rom-priest", "rom-knight", 15, 492355, "Enhanced Grace of Life", "passive"], ["rom-priest", "rom-knight", 20, 492361, "Life Link", "passive"], ["rom-priest", "rom-knight", 25, 491648, "Last Prayer", "elite"], ["rom-priest", "rom-knight", 30, 491366, "Divine Incarnation", "elite"], ["rom-priest", "rom-knight", 35, 492639, "Calm Heart", "elite"], ["rom-priest", "rom-knight", 40, 492934, "Sacrificial Cure", "elite"], ["rom-priest", "rom-knight", 45, 493044, "Holy Power", "elite"], ["rom-priest", "rom-knight", 50, 493271, "Light Fairy", "elite"], ["rom-priest", "rom-knight", 60, 499968, "Splendor Ripple", "passive"], ["rom-priest", "rom-knight", 70, 499957, "Light Guidance", "passive"], ["rom-knight", "rom-scout", 15, 491616, "Arrow of Vengeance", "elite"], ["rom-knight", "rom-scout", 20, 491610, "Enhanced Holy Strike", "passive"], ["rom-knight", "rom-scout", 25, 491613, "Holy Chains", "elite"], ["rom-knight", "rom-scout", 30, 491354, "Heavenly Arrow", "elite"], ["rom-knight", "rom-scout", 35, 490066, "Weaken Weapon Strength", "passive"], ["rom-knight", "rom-scout", 40, 492939, "Sacred Resistance", "elite"], ["rom-knight", "rom-scout", 45, 493046, "Sacred Protection", "elite"], ["rom-knight", "rom-scout", 50, 493273, "Disregard Danger", "elite"], ["rom-knight", "rom-scout", 60, 499926, "Evil Shackles", "passive"], ["rom-knight", "rom-scout", 70, 499923, "Blood Punishment", "passive"], ] as const; function pairedSkill(sourceId: number): AbilityDefinition { const found = ROM_ELITE_ABILITIES.find((ability) => ability.dbcSpellId === sourceId); if (!found) throw new Error(`Missing paired RoM source skill ${sourceId}`); return found; } function executable(effect: AbilityEffect): boolean { if (effect.kind === "scripted" || effect.kind === "unsupported") return false; if (effect.kind === "apply-aura") return Boolean( effect.control || effect.aura.modifiers?.length || effect.aura.absorbs?.length || effect.aura.procs?.length || effect.aura.utilityTags?.length || effect.aura.controlImmunities?.length || effect.aura.redirectDamagePercent, ); return true; } describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.setState({ playerPosition: [0, 0, 0] }); }); it("has exactly the twenty RuneWaker paired skills, including passive upgrades", () => { for (const [primary, secondary] of [["rom-priest", "rom-knight"], ["rom-knight", "rom-scout"]] as const) { const pair = ROM_ELITE_ABILITIES.filter((ability) => ability.classId === primary && ability.secondaryClassId === secondary); expect(pair, `${primary}/${secondary}`).toHaveLength(primary === "rom-priest" ? 11 : 10); const kit = abilitiesForCharacter(primary, secondary); for (const skill of pair) expect(kit).toContain(skill); } }); it.each(PAIRED_SKILLS)("maps %s/%s level %i source %i %s (%s) with the right gate", ( primary, secondary, unlockLevel, sourceId, name, group, ) => { const skill = pairedSkill(sourceId); expect(skill).toMatchObject({ classId: primary, secondaryClassId: secondary, unlockLevel, dbcSpellId: sourceId, name, romSkillGroup: group, passive: group === "passive", }); for (const level of [10, 30, 60, 79]) { expect(isAbilityUnlocked(skill, level), `${name} at level ${level}`) .toBe(level >= unlockLevel); } }); it.each(PAIRED_SKILLS)("gives %s/%s level %i source %i %s (%s) an executable mechanic", ( _primary, _secondary, _unlockLevel, sourceId, name, ) => { const skill = pairedSkill(sourceId); expect(skill.effects.some(executable), `${name} (${sourceId})`).toBe(true); }); it("retains direct and area Knight/Scout taunts", () => { expect(pairedSkill(491616).effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "taunt" }), ])); expect(pairedSkill(491354).effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "taunt", maxTargets: expect.any(Number) }), ])); }); it("retains Priest/Knight healing costs and offensive/defensive effects", () => { const sacrifice = pairedSkill(492934); expect(sacrifice.costs).toEqual(expect.arrayContaining([ expect.objectContaining({ resource: "health", amount: 70, percentage: true }), ])); expect(sacrifice.effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "heal", percentMaxHealth: expect.any(Number) }), ])); expect(pairedSkill(491366).effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "apply-aura", aura: expect.objectContaining({ modifiers: expect.arrayContaining([expect.objectContaining({ kind: "healing", direction: "done", value: 0.15 })]), }) }), ])); expect(pairedSkill(493271).effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "summon", creatureId: 102108 }), ])); }); it("retains Knight/Scout damage and Dexterity buff on Sacred Resistance", () => { const skill = pairedSkill(492939); expect(skill.effects.some((effect) => effect.kind === "damage" && effect.coefficient > 0)).toBe(true); expect(skill.effects.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "dexterity" && modifier.value > 0))).toBe(true); }); it("makes Calm Heart resist Fear rather than applying an empty buff", () => { expect(pairedSkill(492639).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.controlImmunities?.includes("fear"))).toBe(true); }); it("maps Sacred Protection to a real shield block bonus", () => { expect(pairedSkill(493046).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "block-chance" && modifier.value > 0))).toBe(true); }); it("redirects damage with Disregard Danger", () => { expect(pairedSkill(493273).effects.some((effect) => effect.kind === "apply-aura" && (effect.aura.redirectDamagePercent ?? 0) > 0)).toBe(true); }); it("applies only the advertised root from Holy Chains", () => { const controls = pairedSkill(491613).effects.flatMap((effect) => effect.kind === "apply-aura" && effect.control ? [effect.control.kind] : []); expect(controls).toEqual(["root"]); }); it("increases healing received through Blood Punishment", () => { expect(pairedSkill(499923).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-blood-punishment"))).toBe(true); expect(pairedSkill(499923).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "healing"))).toBe(false); }); it("keeps passive pair upgrades conditional on their parent casts", () => { for (const [sourceId, tag] of [ [492355, "rom-enhanced-grace-of-life"], [492361, "rom-life-link"], [499968, "rom-splendor-ripple"], [499957, "rom-light-guidance"], [491610, "rom-enhanced-holy-strike"], [490066, "rom-weaken-weapon-strength"], [499926, "rom-evil-shackles"], [499923, "rom-blood-punishment"], ] as const) { expect(pairedSkill(sourceId).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.utilityTags?.includes(tag)), `${sourceId} ${tag}`).toBe(true); } expect(pairedSkill(491648).effects.some((effect) => effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-last-prayer"))).toBe(true); }); it("applies passive upgrades to Grace, Urgent Heal, Holy Strike, and Heavenly Arrow casts", () => { const ability = (sourceId: number) => ROM_ALL_ABILITIES.find((row) => row.dbcSpellId === sourceId)!; const grace = ability(490290); const enhancedGrace = romPriestKnightCastEffects(grace, grace.effects, ["rom-enhanced-grace-of-life"], 1_000); expect(enhancedGrace.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "resource" && modifier.resource === "mana" && modifier.property === "maximum"))).toBe(true); const urgent = ability(491147); const linked = romPriestKnightCastEffects(urgent, urgent.effects, ["rom-life-link"], 1_000); expect(linked.filter((effect) => effect.kind === "heal" && effect.recipient === "caster")).toHaveLength(1); const holyStrike = ability(490148); const enhancedStrike = romPriestKnightCastEffects(holyStrike, holyStrike.effects, ["rom-enhanced-holy-strike"], 1_000); const originalDamage = holyStrike.effects.find((effect) => effect.kind === "damage"); const enhancedDamage = enhancedStrike.find((effect) => effect.kind === "damage"); expect(enhancedDamage?.kind === "damage" && originalDamage?.kind === "damage" && enhancedDamage.coefficient > originalDamage.coefficient).toBe(true); const arrow = ability(491354); const weakened = romPriestKnightCastEffects(arrow, arrow.effects, ["rom-weaken-weapon-strength"], 1_000); expect(weakened.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "damage" && modifier.direction === "dealt" && modifier.school === "physical" && modifier.value < 0))).toBe(true); }); it("halves Holy Chains duration against ordinary targets after Evil Shackles unlocks", () => { const chains = pairedSkill(491613); const rootDuration = (effects: readonly AbilityEffect[]) => effects .filter((effect): effect is Extract => effect.kind === "apply-aura") .find((effect) => effect.control?.kind === "root")?.aura.durationMs; const ordinary = romPriestKnightCastEffects(chains, chains.effects, ["rom-evil-shackles"], 1_000); const undead = romPriestKnightCastEffects(chains, chains.effects, ["rom-evil-shackles"], 1_000, ["rom-target-undead-demon"]); expect(rootDuration(ordinary)).toBe(rootDuration(chains.effects)! / 2); expect(rootDuration(undead)).toBe(rootDuration(chains.effects)); }); it("adds source 499967's extra Wave Armor heal only with Splendor Ripple", () => { const waveArmor = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490273)!; const ordinary = romPriestKnightCastEffects(waveArmor, waveArmor.effects, [], 1_000); const upgraded = romPriestKnightCastEffects(waveArmor, waveArmor.effects, ["rom-splendor-ripple"], 1_000); expect(ordinary.filter((effect) => effect.kind === "heal")).toHaveLength(0); expect(upgraded).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "heal", basePoints: 106.25, pointsPerLevel: 21.25 }), ])); }); it("adds current-CoA Blood Punishment modifiers to Blood Arrow's actor-owned aura", () => { const bloodArrow = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490434)!; const ordinary = romPriestKnightCastEffects(bloodArrow, bloodArrow.effects, [], 1_000); const upgraded = romPriestKnightCastEffects(bloodArrow, bloodArrow.effects, ["rom-blood-punishment"], 1_000); const arrowAura = (effects: readonly AbilityEffect[]) => effects.find((effect) => effect.kind === "apply-aura" && effect.recipient === "caster" && effect.aura.utilityTags?.includes("rom-self-health-drain")); const sourceAura = arrowAura(bloodArrow.effects); const plainAura = arrowAura(ordinary); const upgradedAura = arrowAura(upgraded); expect(sourceAura).toBeDefined(); expect(plainAura).toEqual(sourceAura); expect(upgradedAura?.kind).toBe("apply-aura"); if (upgradedAura?.kind !== "apply-aura") throw new Error("Blood Arrow active aura is missing"); expect(upgradedAura.aura.durationMs).toBe(sourceAura?.kind === "apply-aura" ? sourceAura.aura.durationMs : null); expect(upgradedAura.aura.modifiers).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "healing", direction: "taken", operation: "percent", value: 0.15 }), expect.objectContaining({ kind: "damage", direction: "dealt", school: "holy", operation: "percent", value: 0.05 }), ])); expect(upgraded.filter((effect) => effect.kind === "apply-aura")).toHaveLength( bloodArrow.effects.filter((effect) => effect.kind === "apply-aura").length, ); }); it("builds source 499966 Devotion Halo and grants conditional Light Guidance healing", () => { const [halo] = romPriestKnightAdditionalAbilities(ROM_ALL_ABILITIES); expect(halo).toMatchObject({ id: "rom-priest-rom-knight-elite-70-devotion-halo", dbcSpellId: 499966, classId: "rom-priest", secondaryClassId: "rom-knight", unlockLevel: 70, radius: 20, passive: false, }); expect(abilitiesForCharacter("rom-priest", "rom-knight").some((ability) => ability.dbcSpellId === 499966)).toBe(true); expect(halo!.effects.some((effect) => effect.kind === "apply-aura" && effect.scope === "party" && effect.aura.durationMs === 900_000 && effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "wisdom" && modifier.value === 0.05))).toBe(true); const ordinary = romPriestKnightCastEffects(halo!, halo!.effects, [], 1_000); const guided = romPriestKnightCastEffects(halo!, halo!.effects, ["rom-light-guidance"], 1_000); expect(ordinary.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "healing"))).toBe(false); expect(guided.some((effect) => effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => modifier.kind === "healing" && modifier.direction === "done" && modifier.value === 0.05))).toBe(true); }); it("heals multiple nearby allies with Priest Group Heal", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer", }); usePartyStore.getState().initializeParty("priest-group-heal", 30, useCombatStore.getState().health, "healer", 3, "rom"); const members = usePartyStore.getState().members.slice(0, 2); expect(members).toHaveLength(2); members.forEach((member, index) => { usePartyStore.getState().damageMember(member.id, Math.max(100, Math.floor(member.maxHealth * 0.4))); registerPartyRuntimePosition(member.id, [3 + index * 2, 0, 0]); }); const injured = members.map((member) => usePartyStore.getState().members.find((entry) => entry.id === member.id)!.health); expect(useCombatStore.getState().castAbility("rom-priest-primary-8", undefined, 1_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 4_100); members.forEach((member, index) => { const healed = usePartyStore.getState().members.find((entry) => entry.id === member.id)!.health; expect(healed, `Group Heal recipient ${index + 1}`).toBeGreaterThan(injured[index]!); }); }); it("preserves Soul Source's 90-source-unit all-teammate heal", () => { const soulSource = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490288)!; expect(soulSource.radius).toBe(9); expect(soulSource.effects.some((effect) => effect.kind === "heal" && effect.scope === "party")).toBe(true); }); it("gives Holy Shield physical immunity to the party", () => { const holyShield = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490495)!; expect(holyShield.effects.some((effect) => effect.kind === "apply-aura" && effect.scope === "party" && effect.aura.modifiers?.some((modifier) => modifier.kind === "damage" && modifier.direction === "taken" && modifier.school === "physical" && modifier.value <= -1))).toBe(true); }); it("taunts nearby enemies with Knight/Scout Heavenly Arrow", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 30, role: "tank", }); for (const [id, x] of [["arrow-a", 3], ["arrow-b", 5]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 10_000 }); setMobPosition(id, [x, 0, 0]); } useCombatStore.getState().selectMob("arrow-a"); expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-30", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().mobs["arrow-a"]!.forcedTarget?.actorId).toBe(PLAYER_AGGRO_ID); expect(useCombatStore.getState().mobs["arrow-b"]!.forcedTarget?.actorId).toBe(PLAYER_AGGRO_ID); }); it.each([ [30, "living", false, 0], [30, "undead", true, 10_000], [60, "living", true, 5_000], [60, "demon", true, 10_000], ] as const)("uses Holy Chains against %s-level %s for the correct duration", ( level, creatureFamily, allowed, durationMs, ) => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level, role: "tank", }); useCombatStore.getState().registerMob("chains-target", { name: "Chains Target", maxHealth: 10_000, creatureFamily, }); setMobPosition("chains-target", [3, 0, 0]); useCombatStore.getState().selectMob("chains-target"); const cast = useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-25", undefined, 1_000); if (!allowed) { expect(cast).toMatchObject({ ok: false, reason: "condition-not-met" }); return; } expect(cast).toMatchObject({ ok: true }); const root = useCombatStore.getState().mobs["chains-target"]!.statuses.find((status) => status.kind === "root"); expect(root?.endsAt).toBe(1_000 + durationMs); }); it("damages multiple nearby enemies with Knight Whirlwind Shield", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 30, role: "tank", }); for (const [id, x] of [["shield-a", 3], ["shield-b", 4]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 10_000 }); setMobPosition(id, [x, 0, 0]); } useCombatStore.getState().selectMob("shield-a"); expect(useCombatStore.getState().castAbility("rom-knight-primary-3", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().mobs["shield-a"]!.health).toBeLessThan(10_000); expect(useCombatStore.getState().mobs["shield-b"]!.health).toBeLessThan(10_000); }); it("pays Sacrificial Cure's HP cost while healing an ally", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer", }); usePartyStore.getState().initializeParty("priest-sacrifice", 60, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.6)); registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); const casterHealth = useCombatStore.getState().health; const allyHealth = usePartyStore.getState().members[0]!.health; expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-40", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().health).toBeLessThan(casterHealth); expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(allyHealth); }); it("Life Link lets Urgent Heal also heal the Priest", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer", }); usePartyStore.getState().initializeParty("priest-life-link", 30, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; useCombatStore.getState().damagePlayer(Math.floor(useCombatStore.getState().maxHealth * 0.5)); usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.5)); registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); const casterHealth = useCombatStore.getState().health; const allyHealth = usePartyStore.getState().members[0]!.health; expect(useCombatStore.getState().castAbility("rom-priest-general-0", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 2_100); expect(useCombatStore.getState().health).toBeGreaterThan(casterHealth); expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(allyHealth); }); it("Last Prayer shortens only the next cast to one second", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer", }); usePartyStore.getState().initializeParty("priest-last-prayer", 30, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.5)); registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-25", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().castAbility("rom-priest-primary-2", undefined, 2_600)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeLessThanOrEqual(1_000); useCombatStore.getState().tick(0.1, 3_700); expect(useCombatStore.getState().castAbility("rom-priest-primary-2", undefined, 4_300)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeGreaterThan(1_000); }); it("Splendor Ripple adds healing when the Priest casts Wave Armor at level 60", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer", }); usePartyStore.getState().initializeParty("priest-ripple", 60, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.5)); registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); const injured = usePartyStore.getState().members[0]!.health; expect(useCombatStore.getState().castAbility("rom-priest-primary-1", undefined, 1_000)) .toMatchObject({ ok: true }); expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(injured); }); it("casts Devotion Halo on the party and gains Light Guidance at level 70", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 70, role: "healer", }); usePartyStore.getState().initializeParty("priest-halo", 70, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; registerPartyRuntimePosition(member.id, [3, 0, 0]); expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-70-devotion-halo", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id && aura.definition.id === "rom-pair-devotion-halo")).toBe(true); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id && aura.definition.id === "rom-pair-devotion-halo-mana")).toBe(true); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID && aura.definition.id === "rom-pair-devotion-halo-mana")).toBe(false); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID && aura.definition.id === "rom-pair-light-guidance-healing")).toBe(true); }); it("reduces a companion's actual Mana spending while Devotion Halo is active", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 70, role: "healer", }); usePartyStore.getState().initializeParty("priest-halo-cost", 70, useCombatStore.getState().health, "healer", 2, "rom"); const partyMember = usePartyStore.getState().members[0]!; registerPartyRuntimePosition(partyMember.id, [3, 0, 0]); const member = { ...partyMember, classId: "rom-priest" as const, secondaryClassId: "rom-knight" as const, resourceType: "mana" as const, resource: 100_000, maxResource: 100_000, resourcePools: { mana: 100_000 }, cooldowns: {}, globalCooldownEndsAt: 0, }; const spell = abilitiesForCharacter("rom-priest", "rom-knight") .find((ability) => !ability.passive && ability.unlockLevel <= 70 && ability.cost.resource === "mana" && ability.cost.amount >= 10)!; const ordinary = commitPartyAbilityUse(member, spell, 2_000)!; expect(ordinary).not.toBeNull(); expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-70-devotion-halo", undefined, 1_000)) .toMatchObject({ ok: true }); const discounted = commitPartyAbilityUse(member, spell, 2_000)!; expect(discounted).not.toBeNull(); expect(discounted.resourcePools.mana).toBeGreaterThan(ordinary.resourcePools.mana!); }); it("keeps Blood Punishment's two benefits on the live Knight/Scout Blood Arrow aura", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 79, role: "tank", }); expect(useCombatStore.getState().castAbility("rom-scout-general-4", undefined, 1_000)) .toMatchObject({ ok: true }); const arrowAuras = useCombatStore.getState().auras.filter((aura) => aura.targetId === PLAYER_AGGRO_ID && aura.definition.utilityTags?.includes("rom-self-health-drain")); expect(arrowAuras).toHaveLength(1); expect(arrowAuras[0]?.definition.modifiers).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "healing", direction: "taken", value: 0.15 }), expect.objectContaining({ kind: "damage", direction: "dealt", school: "holy", value: 0.05 }), ])); expect(useCombatStore.getState().auras.some((aura) => aura.definition.id === "rom-pair-blood-arrow-healing-received")) .toBe(false); }); it("summons the Priest/Knight Light Fairy companion", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer", }); expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-50", undefined, 1_000)) .toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 7_100); expect(useCombatStore.getState().summons.some((summon) => summon.creatureId === 102108)).toBe(true); useCombatStore.getState().tick(1, 8_100); expect(useCombatStore.getState().summons.some((summon) => summon.creatureId === 102108)).toBe(true); }); it("places Disregard Danger's redirect on a teammate and Parry on the Knight", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 60, role: "tank", }); usePartyStore.getState().initializeParty("knight-redirect", 60, useCombatStore.getState().health, "tank", 2, "rom"); const member = usePartyStore.getState().members[0]!; registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-50", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id && (aura.definition.redirectDamagePercent ?? 0) > 0)).toBe(true); expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID && aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "parry"))).toBe(true); }); it("shares 40% of one incoming party hit, then leaves later hits with the ally", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 60, role: "tank", }); usePartyStore.getState().initializeParty("knight-redirect-hit", 60, useCombatStore.getState().health, "tank", 2, "rom"); const member = usePartyStore.getState().members[0]!; registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-50", undefined, 1_000)) .toMatchObject({ ok: true }); const redirectAura = useCombatStore.getState().auras.find((aura) => aura.targetId === member.id && aura.definition.utilityTags?.includes("rom-disregard-danger-one-hit")); expect(redirectAura?.definition.redirectDamagePercent).toBe(0.4); expect(redirectAura?.expiresAt).toBe(11_000); useCombatStore.getState().registerMob("disregard-danger-attacker", { name: "Disregard Danger attacker", maxHealth: 200, xpReward: 0, }); setMobPosition("disregard-danger-attacker", [5, 0, 0]); useCombatStore.getState().damageMob("disregard-danger-attacker", 5, 1_000, { actorId: member.id, role: "tank", }); const playerBeforeFirstHit = useCombatStore.getState().health; const memberBeforeFirstHit = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health; expect(useCombatStore.getState().advanceMobCombat(1_450)).toBeGreaterThan(0); const playerLossOnFirstHit = playerBeforeFirstHit - useCombatStore.getState().health; const memberLossOnFirstHit = memberBeforeFirstHit - usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health; const firstHitDamage = playerLossOnFirstHit + memberLossOnFirstHit; expect(playerLossOnFirstHit).toBeGreaterThan(0); expect(memberLossOnFirstHit).toBeGreaterThan(0); expect(playerLossOnFirstHit).toBe(Math.round(firstHitDamage * 0.4)); expect(useCombatStore.getState().auras.some((aura) => aura.instanceId === redirectAura?.instanceId)).toBe(false); const playerBeforeSecondHit = useCombatStore.getState().health; const memberBeforeSecondHit = usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health; expect(useCombatStore.getState().advanceMobCombat(4_000)).toBeGreaterThan(0); expect(useCombatStore.getState().health).toBe(playerBeforeSecondHit); expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)!.health) .toBeLessThan(memberBeforeSecondHit); }); it("keeps Master of Parry through one block and removes it after the second", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 60, role: "tank", }); useCombatStore.setState((state) => ({ resource: 100, resourcePools: { ...state.resourcePools, rage: 100 }, })); const castResult = useCombatStore.getState().castAbility("rom-knight-rom-warrior-elite-40", undefined, 1_000); expect(castResult.ok, JSON.stringify(castResult)).toBe(true); const masterOfParry = useCombatStore.getState().auras.find((aura) => ( aura.targetId === PLAYER_AGGRO_ID && aura.definition.utilityTags?.includes(ROM_KNIGHT_MASTER_OF_PARRY_TAG) && aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" && modifier.stat === "parry") )); expect(masterOfParry?.definition.durationMs).toBe(15_000); expect(masterOfParry?.expiresAt).toBe(16_000); expect(masterOfParry?.definition.modifiers).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "stat", stat: "parry", operation: "flat", value: 5_000 }), ])); expect(masterOfParry?.procStates).toEqual(expect.arrayContaining([ expect.objectContaining({ procId: ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, remainingCharges: 2 }), ])); const timedTriggerAura = useCombatStore.getState().auras.find((aura) => ( aura.targetId === PLAYER_AGGRO_ID && aura.definition.procs?.some((proc) => ( proc.action.kind === "custom" && proc.action.id === "rom-trigger-skill" && proc.action.data?.spellId === 493094 )) )); expect(timedTriggerAura?.definition.durationMs).toBe(15_000); expect(timedTriggerAura?.expiresAt).toBe(16_000); // Make each incoming melee attack a deterministic successful block so the // test exercises the same block event used by normal combat. const forcedBlockAuras = applyCombatAura(useCombatStore.getState().auras, { definition: { id: "test-forced-block", name: "Test forced block", disposition: "buff", durationMs: null, modifiers: [ { kind: "stat", stat: "incoming-melee-hit", operation: "flat", value: 1_000 }, { kind: "stat", stat: "incoming-critical-strike", operation: "flat", value: -1_000 }, { kind: "stat", stat: "dodge", operation: "flat", value: -1_000 }, { kind: "stat", stat: "parry", operation: "flat", value: -10_000 }, { kind: "stat", stat: "block-chance", operation: "flat", value: 1_000 }, ], }, sourceId: PLAYER_AGGRO_ID, targetId: PLAYER_AGGRO_ID, now: 1_000, }).auras; useCombatStore.setState({ auras: [...forcedBlockAuras] }); const forcedDefender = characterCombatDefender(useCombatStore.getState()); expect(forcedDefender.dodgePercent).toBe(0); expect(forcedDefender.parryPercent).toBe(0); expect(forcedDefender.blockPercent).toBeGreaterThan(100); useCombatStore.getState().registerMob("master-of-parry-attacker", { name: "Master of Parry attacker", maxHealth: 200, xpReward: 0, }); setMobPosition("master-of-parry-attacker", [3, 0, 0]); useCombatStore.getState().selectMob("master-of-parry-attacker"); useCombatStore.getState().damageMob("master-of-parry-attacker", 5, 1_000); expect(useCombatStore.getState().mobs["master-of-parry-attacker"]).toMatchObject({ engaged: true }); useCombatStore.getState().advanceMobCombat(1_450, [0, 0, 0]); const firstMobState = useCombatStore.getState().mobs["master-of-parry-attacker"]; expect(firstMobState?.attackRevision, JSON.stringify(firstMobState)).toBe(1); expect(useCombatStore.getState().lastCombatResult?.blocked).toBe(true); expect(useCombatStore.getState().auras.find((aura) => aura.instanceId === masterOfParry?.instanceId) ?.procStates).toEqual(expect.arrayContaining([ expect.objectContaining({ procId: ROM_KNIGHT_MASTER_OF_PARRY_BLOCK_PROC, remainingCharges: 1 }), ])); expect(useCombatStore.getState().auras.some((aura) => aura.instanceId === masterOfParry?.instanceId)).toBe(true); useCombatStore.getState().advanceMobCombat(4_000, [0, 0, 0]); expect(useCombatStore.getState().mobs["master-of-parry-attacker"]?.attackRevision).toBe(2); expect(useCombatStore.getState().auras.some((aura) => aura.instanceId === masterOfParry?.instanceId)).toBe(false); }); });