import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForClass, abilityById } from "./abilityCatalog"; import { DEFAULT_ENEMY_ATTACK } from "./enemyAttacks"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { createConfiguredPartyMember, usePartyStore } from "./partyStore"; import { ROM_KNIGHT_DIVINE_PUNISHMENT_TAG, ROM_KNIGHT_GODS_VENGEANCE_TAG, ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG, romKnightDivinePunishmentAuraForSealStacks, ROM_KNIGHT_SHIELD_OF_VALOR_TAG, } from "./romKnightOverrides"; import { romEffectsForCast } from "./romCombatMechanics"; function stableCombatUnit(seed: string): number { let hash = 2166136261; for (let index = 0; index < seed.length; index += 1) { hash ^= seed.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0) / 0x1_0000_0000; } function godsVengeanceCastTime(wantProc: boolean): number { for (let now = 11_500; now < 100_000; now += 100) { const roll = stableCombatUnit( `${PLAYER_AURA_ENTITY_ID}:rom-knight-general-0:gods-vengeance-target:${now}:rom-gods-vengeance`, ); if ((roll < 0.5) === wantProc) return now; } throw new Error("Could not find a deterministic God's Vengeance cast-time seed."); } describe("RuneWaker Knight periodic skills", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.setState({ playerPosition: [0, 0, 0] }); }); it("keeps Holy Light Domain as a three-pulse Holy area effect", () => { const domain = abilityById("rom-knight-rom-mage-elite-15")!; expect(domain).toMatchObject({ dbcSpellId: 491606, target: "self", radius: 10 }); expect(domain.effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "dot", school: "holy", ticks: 3, intervalMs: 2_000, tag: "rom-knight-holy-light-domain", }), ])); expect(domain.effects.some((effect) => effect.kind === "apply-aura" && effect.aura.procs?.some((proc) => proc.action.kind === "custom" && proc.action.id === "rom-trigger-skill"))).toBe(false); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-mage", raceId: "rom-human", level: 80, }); for (const [id, x] of [["domain-near", 5], ["domain-edge", 9], ["domain-far", 12]] as const) { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 }); setMobPosition(id, [x, 0, 0]); } const initialHealth = Object.fromEntries(Object.entries(useCombatStore.getState().mobs) .map(([id, mob]) => [id, mob.health])); expect(useCombatStore.getState().castAbility(domain.id, undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().effects.filter((effect) => effect.kind === "dot") .map((effect) => effect.targetId).sort()).toEqual(["domain-edge", "domain-near"]); expect(useCombatStore.getState().mobs["domain-near"]!.health).toBe(initialHealth["domain-near"]); useCombatStore.getState().tick(0.1, 3_000); const afterFirstPulse = useCombatStore.getState(); expect(afterFirstPulse.mobs["domain-near"]!.health).toBeLessThan(initialHealth["domain-near"]!); expect(afterFirstPulse.mobs["domain-edge"]!.health).toBeLessThan(initialHealth["domain-edge"]!); expect(afterFirstPulse.mobs["domain-far"]!.health).toBe(initialHealth["domain-far"]); }); it("adds War Prayer Holy Seal stacks on its six three-second pulses", () => { const prayer = abilityById("rom-knight-rom-mage-elite-50")!; expect(prayer).toMatchObject({ dbcSpellId: 490078, target: "hostile" }); expect(prayer.effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "damage", school: "holy" }), expect.objectContaining({ kind: "dot", ticks: 6, intervalMs: 3_000, tag: "rom-knight-war-prayer-seal", }), ])); expect(prayer.effects.some((effect) => effect.kind === "apply-aura" && effect.aura.procs?.some((proc) => proc.action.kind === "custom" && proc.action.id === "rom-trigger-skill"))).toBe(false); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-mage", raceId: "rom-human", level: 80, }); useCombatStore.getState().registerMob("war-prayer-target", { name: "War Prayer Target", maxHealth: 100_000 }); setMobPosition("war-prayer-target", [3, 0, 0]); useCombatStore.getState().selectMob("war-prayer-target"); expect(useCombatStore.getState().castAbility(prayer.id, undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().comboPoints).toBe(0); expect(useCombatStore.getState().effects).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "dot", targetId: "war-prayer-target", remainingTicks: 6 }), ])); for (let tick = 1; tick <= 6; tick += 1) { useCombatStore.getState().tick(0.1, 1_000 + tick * 3_000); expect(useCombatStore.getState().comboTargetId).toBe("war-prayer-target"); expect(useCombatStore.getState().comboPoints).toBe(Math.min(tick, 4)); } const punishment = abilityById("rom-knight-general-0")!; expect(useCombatStore.getState().castAbility(punishment.id, undefined, 20_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().comboPoints).toBe(0); expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "damage" && event.abilityId === punishment.id && event.targetActorId === "war-prayer-target" && event.effectiveAmount > 0)).toBe(true); }); it("uses current CoA's 28.5% Divine Punishment damage bonus only on Punishment", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 80, }); const state = useCombatStore.getState(); const passive = state.abilities.find((ability) => ability.dbcSpellId === 491608); expect(passive?.passive).toBe(true); const passiveAura = state.auras.find((aura) => aura.definition.utilityTags?.includes(ROM_KNIGHT_DIVINE_PUNISHMENT_TAG)); expect(passiveAura).toBeDefined(); expect(passive?.effects).toEqual([ expect.objectContaining({ kind: "apply-aura", aura: expect.objectContaining({ utilityTags: [ROM_KNIGHT_DIVINE_PUNISHMENT_TAG], }), }), ]); const passiveEffect = passive?.effects[0]; expect(passiveEffect?.kind).toBe("apply-aura"); if (passiveEffect?.kind === "apply-aura") expect(passiveEffect.aura.modifiers).toBeUndefined(); const punishment = abilityById("rom-knight-general-0")!; const context = { sealStacks: 4, bleedCount: 0, charged: false, mainHandDagger: false, casterMaxHealth: state.maxHealth, targetHealthPercent: 1, } as const; const baseline = romEffectsForCast(punishment, context); const boosted = romEffectsForCast(punishment, { ...context, passiveTags: passiveAura?.definition.utilityTags ?? [], }); const damageValues = (effects: typeof baseline) => effects .filter((effect) => effect.kind === "damage" || effect.kind === "dot") .map((effect) => ({ coefficient: effect.coefficient, basePoints: "basePoints" in effect ? effect.basePoints ?? 0 : 0, pointsPerLevel: "pointsPerLevel" in effect ? effect.pointsPerLevel ?? 0 : 0, })); expect(damageValues(baseline)).toHaveLength(2); expect(damageValues(boosted)).toHaveLength(2); for (const [index, base] of damageValues(baseline).entries()) { expect(damageValues(boosted)[index]!.coefficient).toBeCloseTo(base.coefficient * 1.285); expect(damageValues(boosted)[index]!.basePoints).toBeCloseTo(base.basePoints * 1.285); expect(damageValues(boosted)[index]!.pointsPerLevel).toBeCloseTo(base.pointsPerLevel * 1.285); } expect(romEffectsForCast(abilityById("rom-knight-primary-1")!, { ...context, passiveTags: passiveAura?.definition.utilityTags ?? [], })).toEqual(romEffectsForCast(abilityById("rom-knight-primary-1")!, context)); }); it("replaces the old Light-only aura with current CoA's general per-seal vulnerability data", () => { expect(romKnightDivinePunishmentAuraForSealStacks(3)?.aura).toMatchObject({ durationMs: 10_000, modifiers: [{ kind: "damage", direction: "taken", operation: "percent", value: 0.06 }], }); expect(romKnightDivinePunishmentAuraForSealStacks(3)?.aura.id) .toBe("rom-knight-divine-punishment-damage-taken"); }); it("uses the local Knight/Priest Compassion row bridged to current CoA behavior and no longer changes Holy Seal cooldown", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-priest", raceId: "rom-human", level: 80, }); const state = useCombatStore.getState(); const holySeal = state.abilities.find((ability) => ability.dbcSpellId === 490149)!; const compassionPassive = state.abilities.find((ability) => ability.dbcSpellId === 491635)!; expect(compassionPassive).toMatchObject({ name: "Compassion of Holy Light", passive: true }); expect(compassionPassive.effects).toEqual([ expect.objectContaining({ kind: "apply-aura", aura: expect.objectContaining({ utilityTags: [ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG] }), }), ]); const tags = state.auras.find((aura) => aura.definition.utilityTags?.includes(ROM_KNIGHT_COMPASSION_OF_HOLY_LIGHT_TAG)) ?.definition.utilityTags ?? []; expect(romEffectsForCast(holySeal, { sealStacks: 0, bleedCount: 0, charged: false, mainHandDagger: false, casterMaxHealth: state.maxHealth, targetHealthPercent: 1, passiveTags: tags, })).toEqual(romEffectsForCast(holySeal, { sealStacks: 0, bleedCount: 0, charged: false, mainHandDagger: false, casterMaxHealth: state.maxHealth, targetHealthPercent: 1, })); }); it.each([true, false])("lets God's Vengeance preserve Holy Seal when the 50% roll is %s", (preservesSeal) => { const godVengeance = abilityById("rom-knight-rom-rogue-elite-50")!; expect(godVengeance).toMatchObject({ dbcSpellId: 499934, passive: true }); expect(godVengeance.effects).toEqual([ expect.objectContaining({ kind: "apply-aura", recipient: "caster", aura: expect.objectContaining({ utilityTags: [ROM_KNIGHT_GODS_VENGEANCE_TAG] }), }), ]); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-rogue", raceId: "rom-human", level: 80, }); useCombatStore.getState().registerMob("gods-vengeance-target", { name: "God's Vengeance Target", maxHealth: 1_000_000, }); setMobPosition("gods-vengeance-target", [3, 0, 0]); useCombatStore.getState().selectMob("gods-vengeance-target"); expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([ expect.objectContaining({ targetId: PLAYER_AURA_ENTITY_ID, definition: expect.objectContaining({ utilityTags: [ROM_KNIGHT_GODS_VENGEANCE_TAG] }), }), ])); expect(useCombatStore.getState().castAbility("rom-knight-primary-1", undefined, 1_000)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().comboPoints).toBe(4); const castTime = godsVengeanceCastTime(preservesSeal); expect(useCombatStore.getState().castAbility("rom-knight-general-0", undefined, castTime)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().comboPoints).toBe(preservesSeal ? 4 : 0); expect(useCombatStore.getState().effects.some((effect) => effect.kind === "dot" && effect.targetId === "gods-vengeance-target" && effect.tag === "rom-holy-seal")) .toBe(preservesSeal); expect(useCombatStore.getState().combatEvents.some((event) => event.kind === "damage" && event.abilityId === "rom-knight-general-0" && event.targetActorId === "gods-vengeance-target" && event.effectiveAmount > 0)).toBe(true); }); it("reduces Holy Seal cooldown by one second when Shield of Valor parries", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", raceId: "rom-human", level: 80, role: "tank", }); const holySeal = useCombatStore.getState().abilities.find((ability) => ability.dbcSpellId === 490149)!; const shieldOfValor = abilityById("rom-knight-primary-10")!; expect(shieldOfValor).toMatchObject({ dbcSpellId: 490468 }); useCombatStore.setState((state) => ({ gearStats: { ...state.gearStats, parryRating: 100_000 }, })); expect(useCombatStore.getState().castAbility(shieldOfValor.id, undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([ expect.objectContaining({ targetId: PLAYER_AURA_ENTITY_ID, definition: expect.objectContaining({ utilityTags: expect.arrayContaining([ROM_KNIGHT_SHIELD_OF_VALOR_TAG]) }), }), ])); useCombatStore.setState((state) => ({ cooldowns: { ...state.cooldowns, [holySeal.id]: 25_000 }, })); useCombatStore.getState().registerMob("shield-of-valor-attacker", { name: "Shield of Valor Attacker", maxHealth: 100_000, attacks: [DEFAULT_ENEMY_ATTACK], }); setMobPosition("shield-of-valor-attacker", [2, 0, 0]); useCombatStore.getState().damageMob("shield-of-valor-attacker", 1, 1_100); let parryAt: number | null = null; for (let now = 1_450; now < 16_000; now += 100) { useCombatStore.getState().advanceMobCombat(now); if (useCombatStore.getState().lastCombatResult?.parried) { parryAt = now; break; } } expect(parryAt).not.toBeNull(); expect(useCombatStore.getState().cooldowns[holySeal.id]).toBe(24_000); }); it("reduces only the party Knight's Holy Seal cooldown on an active Shield of Valor parry", () => { const runIncomingCase = ( suffix: string, auraActive: boolean, parryRating: number, wantedOutcome: "parry" | "hit", ) => { const seed = `shield-of-valor-party-${suffix}`; useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80 }); const knight = createConfiguredPartyMember({ name: "Shield of Valor Knight", classId: "rom-knight", specialization: "Primary", role: "tank", }, 80, 0, seed); const knightAbilities = abilitiesForClass("rom-knight"); const holySeal = knightAbilities.find((ability) => ability.source === "runewaker" && ability.dbcSpellId === 490_149)!; const shieldOfValor = knightAbilities.find((ability) => ability.source === "runewaker" && ability.dbcSpellId === 490_468)!; usePartyStore.getState().initializeParty(seed, 80, useCombatStore.getState().health, "tank", 2, "rom"); usePartyStore.setState({ members: [{ ...knight, gearStats: { ...knight.gearStats, parryRating }, cooldowns: { ...knight.cooldowns, [holySeal.id]: 25_000 }, }] }); registerPartyRuntimePosition(knight.id, [0, 0, 0]); useCombatStore.getState().synchronizeActors(); if (auraActive) { const shieldAura = shieldOfValor.effects.find((effect) => effect.kind === "apply-aura"); if (!shieldAura || shieldAura.kind !== "apply-aura") throw new Error("Shield of Valor has no aura row"); useCombatStore.getState().applyActorAura(knight.id, knight.id, shieldOfValor, shieldAura, 1_000); } const mobId = `shield-of-valor-party-attacker-${suffix}`; useCombatStore.getState().registerMob(mobId, { name: "Shield of Valor party attacker", maxHealth: 100_000, xpReward: 0, attacks: [DEFAULT_ENEMY_ATTACK], }); setMobPosition(mobId, [2, 0, 0]); useCombatStore.getState().damageMob(mobId, 1, 1_100, { actorId: knight.id, role: "tank", }); let outcome: string | undefined; for (let now = 1_450; now < 16_000; now += 100) { useCombatStore.getState().advanceMobCombat(now); const event = [...useCombatStore.getState().combatEvents].reverse().find((entry) => ( entry.sourceActorId === mobId && entry.targetActorId === knight.id && entry.occurredAt === now )); if (event?.outcome === wantedOutcome) { outcome = event.outcome; break; } } return { outcome, cooldown: useCombatStore.getState().actors[knight.id]?.cooldowns[holySeal.id], }; }; const activeParry = runIncomingCase("active-parry", true, 100_000, "parry"); expect(activeParry.outcome).toBe("parry"); expect(activeParry.cooldown).toBe(24_000); const inactiveParry = runIncomingCase("inactive-parry", false, 100_000, "parry"); expect(inactiveParry.outcome).toBe("parry"); expect(inactiveParry.cooldown).toBe(25_000); const activeHit = runIncomingCase("active-hit", true, 0, "hit"); expect(activeHit.outcome).toBe("hit"); expect(activeHit.cooldown).toBe(25_000); }); it("activates the source-backed Evil Shackles passive state", () => { const passive = abilityById("rom-knight-rom-scout-elite-60")!; expect(passive).toMatchObject({ dbcSpellId: 499926, passive: true }); expect(passive.effects).toEqual([ expect.objectContaining({ kind: "apply-aura", recipient: "caster", aura: expect.objectContaining({ durationMs: null, utilityTags: ["rom-evil-shackles"], }), }), ]); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 80, }); expect(useCombatStore.getState().auras).toEqual(expect.arrayContaining([ expect.objectContaining({ definition: expect.objectContaining({ utilityTags: ["rom-evil-shackles"] }), }), ])); }); it("lets Evil Shackles root non-Undead/Demon targets for half duration", () => { const holyChains = abilityById("rom-knight-rom-scout-elite-25")!; const rootDuration = (targetTags: readonly string[]) => { const root = romEffectsForCast(holyChains, { sealStacks: 0, bleedCount: 0, charged: false, mainHandDagger: false, casterMaxHealth: 2_000, targetHealthPercent: 1, passiveTags: ["rom-evil-shackles"], targetTags, }).find((effect) => effect.kind === "apply-aura" && effect.control?.kind === "root"); return root?.kind === "apply-aura" ? root.aura.durationMs : undefined; }; expect(rootDuration(["rom-target-undead-demon"])).toBe(10_000); expect(rootDuration(["rom-target-living"])).toBe(5_000); expect(rootDuration([])).toBe(5_000); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 50, }); useCombatStore.getState().registerMob("shackles-target", { name: "Living target", maxHealth: 100_000, creatureFamily: "living", }); setMobPosition("shackles-target", [3, 0, 0]); useCombatStore.getState().selectMob("shackles-target"); expect(useCombatStore.getState().castAbility(holyChains.id, undefined, 1_000)).toMatchObject({ ok: false, reason: "condition-not-met", }); useCombatStore.getState().registerMob("shackles-target", { name: "Unknown target", maxHealth: 100_000, creatureFamily: "unknown", }); expect(useCombatStore.getState().castAbility(holyChains.id, undefined, 2_000)).toMatchObject({ ok: false, reason: "condition-not-met", }); useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 80, }); useCombatStore.getState().registerMob("shackles-target", { name: "Living target", maxHealth: 100_000, creatureFamily: "living", }); setMobPosition("shackles-target", [3, 0, 0]); useCombatStore.getState().selectMob("shackles-target"); expect(useCombatStore.getState().castAbility(holyChains.id, undefined, 3_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().mobs["shackles-target"]!.statuses).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: "root", endsAt: 8_000 }), ])); }); });