import { beforeEach, describe, expect, it } from "vitest"; import { defaultActionBarForCharacter, resourceProfileForClass } from "./abilityCatalog"; import { PLAYER_AGGRO_ID } from "./aggro"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; import { useCombatStore } from "./combatStore"; import { companionActorFromMember, companionActorWithMemberRuntime } from "./combatActorAuthority"; describe("RuneWaker combat runtime", () => { beforeEach(() => { clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); }); it("creates keyed pools for both classes and changes the active resource on orientation swap", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", secondaryClassId: "rom-scout", raceId: "rom-human", level: 70, xp: 456, }); expect(useCombatStore.getState()).toMatchObject({ classId: "rom-warrior", secondaryClassId: "rom-scout", resourceName: "Rage", xp: 456, }); expect(useCombatStore.getState().maxResourcePools).toMatchObject({ rage: 100, focus: 100 }); expect(new Set(Object.values(useCombatStore.getState().actionBindings).filter(Boolean))).toEqual( new Set(defaultActionBarForCharacter("rom-warrior", "rom-scout", 70).slice(0, 24)), ); useCombatStore.getState().initializeCharacter({ classId: "rom-scout", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 70, xp: 456, }); expect(useCombatStore.getState()).toMatchObject({ classId: "rom-scout", secondaryClassId: "rom-warrior", resourceName: "Focus", xp: 456, }); expect(useCombatStore.getState().maxResourcePools).toMatchObject({ focus: 100, rage: 100 }); }); it("aggregates multi-cost rows by resource before validating a cast", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-mage", secondaryClassId: "rom-warden", raceId: "rom-elf", level: 50, }); const maximumMana = useCombatStore.getState().maxResourcePools.mana!; const availableMana = Math.max(100, Math.round(maximumMana * 0.05)); useCombatStore.setState((state) => ({ resource: availableMana, resourcePools: { ...state.resourcePools, mana: availableMana }, })); expect(useCombatStore.getState().castAbility("rom-mage-rom-warden-elite-50", undefined, 1_000)).toMatchObject({ ok: false, reason: "not-enough-resource", }); }); it.each(["rom-warrior", "rom-scout", "rom-rogue"] as const)( "preserves %s secondary mana in player and companion resource snapshots", (classId) => { useCombatStore.getState().initializeCharacter({ classId, secondaryClassId: "rom-warden", raceId: "rom-elf", level: 80, }); const state = useCombatStore.getState(); expect(state.maxResourcePools.mana).toBe(100); expect(state.resourcePools.mana).toBe(100); state.synchronizeActors(); expect(useCombatStore.getState().actors[PLAYER_AGGRO_ID].resources.mana).toMatchObject({ current: 100, maximum: 100 }); usePartyStore.getState().initializeParty("rom-secondary-mana", 80, state.health); const member = { ...usePartyStore.getState().members[0]!, classId, raceId: state.raceId, resourceType: resourceProfileForClass(classId).type, maxResource: state.maxResource, resourcePools: state.resourcePools, gearStats: state.gearStats, }; const actor = companionActorFromMember(member); expect(actor.resources.mana).toMatchObject({ current: 100, maximum: 100 }); expect(companionActorWithMemberRuntime(actor, member).resources.mana) .toMatchObject({ current: 100, maximum: 100 }); }, ); it.each([ ["rom-priest", "rom-human", "rom-priest-general-1"], ["rom-druid", "rom-elf", "rom-druid-primary-0"], ] as const)("lets a level-one %s cast its starting attack", (classId, raceId, abilityId) => { useCombatStore.getState().initializeCharacter({ classId, raceId, level: 1 }); useCombatStore.getState().registerMob("starter-dummy", { name: "Starter Dummy", maxHealth: 1_000 }); setMobPosition("starter-dummy", [3, 0, 0]); useCombatStore.getState().selectMob("starter-dummy"); expect(useCombatStore.getState().castAbility(abilityId, undefined, 1_000)).toMatchObject({ ok: true }); }); it("uses source coefficients for direct damage even without equipment", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", raceId: "rom-human", level: 20 }); useCombatStore.getState().registerMob("coefficient-dummy", { name: "Coefficient Dummy", maxHealth: 1_000 }); setMobPosition("coefficient-dummy", [3, 0, 0]); useCombatStore.getState().selectMob("coefficient-dummy"); useCombatStore.setState((state) => ({ resource: 100, resourcePools: { ...state.resourcePools, rage: 100 }, })); expect(useCombatStore.getState().castAbility("rom-warrior-primary-1", undefined, 1_000)).toMatchObject({ ok: true }); expect(useCombatStore.getState().mobs["coefficient-dummy"]!.health).toBeLessThan(1_000); }); it("finishes a RuneWaker direct heal on the selected party member", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 20 }); usePartyStore.getState().initializeParty("rom-urgent-heal", 20, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; usePartyStore.getState().damageMember(member.id, 500); registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); const injuredHealth = 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().activeCast).toBeNull(); expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(injuredHealth); }); it("uses native RoM Rage gains from auto attacks and health damage", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", raceId: "rom-human", level: 1, }); useCombatStore.getState().registerMob("rage-dummy", { name: "Rage Dummy", maxHealth: 1_000 }); setMobPosition("rage-dummy", [3, 0, 0]); useCombatStore.getState().selectMob("rage-dummy"); useCombatStore.setState({ shield: 100 }); expect(useCombatStore.getState().damagePlayer(5)).toBe(0); expect(useCombatStore.getState().resourcePools.rage).toBe(0); useCombatStore.setState({ shield: 0 }); expect(useCombatStore.getState().castAbility("rom-warrior-general-0", undefined, 1_000)).toMatchObject({ ok: false, reason: "not-enough-resource", }); expect(useCombatStore.getState().autoAttackTargetId).toBe("rage-dummy"); useCombatStore.getState().tick(0.1, 1_000); expect(useCombatStore.getState().resourcePools.rage).toBe(6); expect(useCombatStore.getState().resource).toBe(6); const healthAfterFirstSwing = useCombatStore.getState().mobs["rage-dummy"]!.health; expect(healthAfterFirstSwing).toBeLessThan(1_000); useCombatStore.getState().tick(1, 2_000); expect(useCombatStore.getState().mobs["rage-dummy"]!.health).toBe(healthAfterFirstSwing); expect(useCombatStore.getState().resourcePools.rage).toBe(6); expect(useCombatStore.getState().damagePlayer(5)).toBeGreaterThan(0); expect(useCombatStore.getState().resourcePools.rage).toBe(13); useCombatStore.getState().tick(1, 3_400); const secondSwing = useCombatStore.getState().lastCombatResult!; if (secondSwing.finalDamage > 0) { expect(useCombatStore.getState().mobs["rage-dummy"]!.health).toBeLessThan(healthAfterFirstSwing); expect(useCombatStore.getState().resourcePools.rage).toBe(19); } else { expect(useCombatStore.getState().mobs["rage-dummy"]!.health).toBe(healthAfterFirstSwing); expect(useCombatStore.getState().resourcePools.rage).toBe(13); } }); it("uses Champion's attack cadence instead of Warrior's per-hit Rage rules", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 1 }); useCombatStore.getState().registerMob("champion-dummy", { name: "Champion Dummy", maxHealth: 1_000 }); setMobPosition("champion-dummy", [3, 0, 0]); useCombatStore.getState().selectMob("champion-dummy"); useCombatStore.getState().castAbility("rom-champion-general-0", undefined, 1_000); useCombatStore.getState().tick(1, 1_000); expect(useCombatStore.getState().resourcePools.rage).toBe(6); useCombatStore.getState().damagePlayer(5); expect(useCombatStore.getState().resourcePools.rage).toBe(6); useCombatStore.getState().tick(1, 2_000); expect(useCombatStore.getState().resourcePools.rage).toBe(12); }); it("decays native RoM Rage while out of combat", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warrior", raceId: "rom-human", level: 1 }); useCombatStore.setState((state) => ({ resource: 20, resourcePools: { ...state.resourcePools, rage: 20 }, })); useCombatStore.getState().tick(1, 1_000); expect(useCombatStore.getState().resource).toBe(17); expect(useCombatStore.getState().resourcePools.rage).toBe(17); }); it("keeps native summons as temporary allied combat actors", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warden", raceId: "rom-elf", level: 20 }); const maximumMana = useCombatStore.getState().maxResourcePools.mana!; useCombatStore.getState().castAbility("rom-warden-primary-1", undefined, 1_000); useCombatStore.getState().tick(0.1, 5_000); expect(useCombatStore.getState().summons).toEqual([ expect.objectContaining({ sourceAbilityId: "rom-warden-primary-1", creatureId: 102297 }), ]); expect(useCombatStore.getState().resourcePools.mana).toBe(maximumMana - Math.round(maximumMana * 0.15)); useCombatStore.getState().tick(0.1, 20_001); expect(useCombatStore.getState().summons).toEqual([]); }); it("raises an incapacitated party member with source resurrection behavior", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", raceId: "rom-human", level: 20 }); usePartyStore.getState().initializeParty("rom-resurrection", 20, useCombatStore.getState().health, "healer", 2, "rom"); const member = usePartyStore.getState().members[0]!; usePartyStore.getState().damageMember(member.id, member.maxHealth * 10); registerPartyRuntimePosition(member.id, [3, 0, 0]); usePartyStore.getState().selectMember(member.id); expect(usePartyStore.getState().members[0]!.health).toBe(0); expect(useCombatStore.getState().castAbility("rom-priest-primary-5", undefined, 1_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 7_000); expect(useCombatStore.getState()).toMatchObject({ activeCast: null, feedback: { kind: "success" } }); expect(usePartyStore.getState().members[0]).toMatchObject({ health: Math.round(member.maxHealth * 0.35), status: "ready", }); }); it("chains Plasma Arrow, Electric Compression, and Static Field through their source states", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-mage", raceId: "rom-human", level: 20 }); useCombatStore.getState().registerMob("static-dummy", { name: "Static Dummy", maxHealth: 10_000 }); setMobPosition("static-dummy", [3, 0, 0]); useCombatStore.getState().selectMob("static-dummy"); expect(useCombatStore.getState().castAbility("rom-mage-primary-55", undefined, 1_000)).toMatchObject({ ok: false, reason: "condition-not-met", }); expect(useCombatStore.getState().castAbility("rom-mage-primary-52", undefined, 2_000)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 4_100); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-charged"))).toBe(true); expect(useCombatStore.getState().castAbility("rom-mage-primary-55", undefined, 4_200)).toMatchObject({ ok: true }); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-charged"))).toBe(false); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-static-field-charge"))).toBe(true); expect(useCombatStore.getState().castAbility("rom-mage-primary-56", undefined, 5_800)).toMatchObject({ ok: true }); useCombatStore.getState().tick(0.1, 6_900); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-static-field-charge"))).toBe(false); }); it("lets Remodeled Body prevent lethal damage once", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-champion", raceId: "rom-dwarf", level: 80 }); const now = Date.now(); expect(useCombatStore.getState().castAbility("rom-champion-primary-16", undefined, now)).toMatchObject({ ok: true }); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-remodeled-body"))).toBe(true); useCombatStore.getState().damagePlayer(useCombatStore.getState().maxHealth * 10); expect(useCombatStore.getState().health).toBe(useCombatStore.getState().maxHealth); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-remodeled-body"))).toBe(false); }); it("builds and consumes Revival charges from damage taken", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-rogue", secondaryClassId: "rom-warden", raceId: "rom-elf", level: 80, }); useCombatStore.getState().damagePlayer(100); useCombatStore.getState().damagePlayer(100); useCombatStore.getState().damagePlayer(100); const revival = useCombatStore.getState().auras.find((aura) => aura.definition.utilityTags?.includes("rom-revival-charge")); expect(revival?.stacks).toBe(3); const injuredHealth = useCombatStore.getState().health; const manaBefore = useCombatStore.getState().resourcePools.mana!; expect(useCombatStore.getState().castAbility("rom-rogue-rom-warden-elite-60", undefined, Date.now())).toMatchObject({ ok: true }); expect(useCombatStore.getState().health).toBeGreaterThan(injuredHealth); expect(useCombatStore.getState().resourcePools.mana).toBe(manaBefore - 15); expect(useCombatStore.getState().resourcePools.energy).toBe(100); expect(useCombatStore.getState().auras.some((aura) => aura.definition.utilityTags?.includes("rom-revival-charge"))).toBe(false); }); });