import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { abilitiesForClass, abilityAtLevel, abilityById, type AbilityDefinition } from "./abilityCatalog"; import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore } from "./combatStore"; import { registerCombatSpatialProvider, resetCombatSpatialProvider } from "./combatSpatial"; import { targetingRulesForAbility } from "./abilityRules"; import { clearMobRuntimeRegistry, getMobPosition, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; function registerMob(id: string, position: readonly [number, number, number], boss = false): void { useCombatStore.getState().registerMob(id, { name: id, maxHealth: 10_000, xpReward: 0, boss }); setMobPosition(id, position); } describe("extended dungeon combat fidelity", () => { beforeEach(() => { resetCombatSpatialProvider(); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "priest", level: 80 }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); usePartyStore.getState().initializeParty("actor-authority", 80, useCombatStore.getState().health); useCombatStore.getState().synchronizeActors(); }); afterEach(resetCombatSpatialProvider); it("keeps companion vitals authoritative when compatibility party records are stale", () => { const member = usePartyStore.getState().members[0]; const actor = useCombatStore.getState().actors[member.id]; useCombatStore.setState((state) => ({ actors: { ...state.actors, [member.id]: { ...actor, health: 7, alive: true } }, })); usePartyStore.setState((state) => ({ members: state.members.map((candidate) => candidate.id === member.id ? { ...candidate, health: candidate.maxHealth } : candidate), })); useCombatStore.getState().synchronizeActors(); expect(useCombatStore.getState().actors[member.id].health).toBe(7); expect(usePartyStore.getState().members.find((candidate) => candidate.id === member.id)?.health).toBe(7); }); it("binds combo points to the enemy that generated them", () => { useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 20 }); registerMob("combo-a", [0, 0, 3]); registerMob("combo-b", [1, 0, 2]); useCombatStore.getState().selectMob("combo-a"); expect(useCombatStore.getState().castAbility("rogue-sinister-strike", { now: 1_000 }).ok).toBe(true); expect(useCombatStore.getState()).toMatchObject({ comboPoints: 1, comboTargetId: "combo-a" }); useCombatStore.setState((state) => ({ globalCooldownEndsAt: 0, cooldowns: {}, resource: state.maxResource, resourcePools: { ...state.resourcePools, energy: state.maxResource }, })); useCombatStore.getState().selectMob("combo-b"); const secondStrike = useCombatStore.getState().castAbility("rogue-sinister-strike", { now: 2_000 }); expect(secondStrike.ok, secondStrike.reason).toBe(true); expect(useCombatStore.getState()).toMatchObject({ comboPoints: 1, comboTargetId: "combo-b" }); }); it("applies boss control and taunt diminishing returns through immunity", () => { registerMob("dr-boss", [0, 0, 3], true); const durations: number[] = []; for (const now of [1_000, 2_000, 3_000]) { expect(useCombatStore.getState().controlMob("dr-boss", `stun-${now}`, "stun", 8_000, now)).toBe(true); const status = useCombatStore.getState().mobs["dr-boss"].statuses.find((entry) => entry.kind === "stun")!; durations.push(status.endsAt - now); } expect(durations).toEqual([3_000, 1_500, 750]); expect(useCombatStore.getState().controlMob("dr-boss", "stun-immune", "stun", 8_000, 4_000)).toBe(false); const tauntDurations: number[] = []; for (const now of [5_000, 6_000, 7_000]) { expect(useCombatStore.getState().tauntMob("dr-boss", { actorId: PLAYER_AGGRO_ID, role: "tank" }, 3_000, now)).toBe(true); tauntDurations.push(useCombatStore.getState().mobs["dr-boss"].forcedTarget!.endsAt - now); } expect(tauntDurations).toEqual([3_000, 1_950, 1_268]); expect(useCombatStore.getState().tauntMob("dr-boss", { actorId: PLAYER_AGGRO_ID, role: "tank" }, 3_000, 8_000)).toBe(false); }); it("moves and confirms a collision-projected ground reticle", () => { useCombatStore.getState().initializeCharacter({ classId: "mage", level: 80 }); const groundAbility = abilitiesForClass("mage") .map((ability) => abilityAtLevel(ability, 80)) .find((ability) => targetingRulesForAbility(ability).shape === "ground" && ability.range.max > 1)!; expect(groundAbility).toBeDefined(); registerCombatSpatialProvider({ actor: (id) => ({ id, position: [0, 0, 0], yaw: 0 }), hasLineOfSight: () => true, projectGroundPoint: (point, maximumDistance, origin = [0, 0, 0]) => { const dx = point[0] - origin[0]; const dz = point[2] - origin[2]; const distance = Math.hypot(dx, dz); const scale = distance > maximumDistance ? maximumDistance / distance : 1; return [origin[0] + dx * scale, 0, origin[2] + dz * scale]; }, }); expect(useCombatStore.getState().beginGroundTarget(groundAbility.id)).toBe(true); const before = useCombatStore.getState().pendingGroundTarget!; expect(before.valid).toBe(true); useCombatStore.getState().moveGroundTarget(1, 0, 0, 0.1); const moved = useCombatStore.getState().pendingGroundTarget!; expect(moved.position).not.toEqual(before.position); expect(Math.hypot(moved.position[0], moved.position[2])).toBeLessThanOrEqual(moved.maximumRange); expect(useCombatStore.getState().confirmGroundTarget(undefined, 1_000).ok).toBe(true); expect(useCombatStore.getState().pendingGroundTarget).toBeNull(); const activeCast = useCombatStore.getState().activeCast; if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt); expect(useCombatStore.getState().areaEffects).toHaveLength(1); }); it("keeps summons targetable, credits their owner, replaces totems, and expires them", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-warden", raceId: "rom-elf", level: 20 }); useCombatStore.getState().synchronizeActors(); registerMob("summon-target", [0, 0, 3]); useCombatStore.getState().selectMob("summon-target"); expect(useCombatStore.getState().castAbility("rom-warden-primary-1", { now: 1_000 }).ok).toBe(true); useCombatStore.getState().tick(0.1, 5_000); const summon = useCombatStore.getState().summons[0]; expect(summon).toBeDefined(); expect(useCombatStore.getState().actors[summon.id]).toMatchObject({ kind: "summon", ownerId: PLAYER_AGGRO_ID, alive: true }); expect(useCombatStore.getState().selectFriendlyActor(summon.id)).toBe(true); const beforeDamage = summon.health; expect(useCombatStore.getState().damageSummon(summon.id, 5)).toBe(5); expect(useCombatStore.getState().actors[summon.id].health).toBe(beforeDamage - 5); expect(useCombatStore.getState().healActor(summon.id, 5, { sourceActorId: PLAYER_AGGRO_ID }).effectiveAmount).toBe(5); const baseAbility = abilityById("rom-warden-primary-1")!; const combatSummonAbility: AbilityDefinition = { ...baseAbility, id: "test-guardian", name: "Test Guardian" }; useCombatStore.getState().summonActor( PLAYER_AGGRO_ID, combatSummonAbility, { kind: "summon", creatureId: 2, coefficient: 0.5, durationMs: 10_000 }, [0, 0, 0], "summon-target", 6_000, ); useCombatStore.getState().tick(1, 7_500); expect(useCombatStore.getState().mobs["summon-target"].threatByActor[PLAYER_AGGRO_ID]).toBeGreaterThan(0); expect(useCombatStore.getState().mobs["summon-target"].threatByActor[summon.id] ?? 0).toBe(0); const totemAbility: AbilityDefinition = { ...baseAbility, id: "test-totem", name: "Test Totem" }; const totemEffect = { kind: "summon" as const, creatureId: 1, coefficient: 0.2, durationMs: 10_000 }; useCombatStore.getState().summonActor(PLAYER_AGGRO_ID, totemAbility, totemEffect, [0, 0, 0], null, 8_000); useCombatStore.getState().summonActor(PLAYER_AGGRO_ID, totemAbility, totemEffect, [1, 0, 0], null, 8_100); expect(useCombatStore.getState().summons.filter((candidate) => candidate.exclusiveGroup === `${PLAYER_AGGRO_ID}:totem`)).toHaveLength(1); useCombatStore.getState().tick(0.1, 20_001); expect(useCombatStore.getState().summons.some((candidate) => candidate.id === summon.id)).toBe(false); expect(useCombatStore.getState().actors[summon.id]).toBeUndefined(); }); it("executes damaging abilities from Classic, RuneWaker, and CoA catalogs through one runtime", () => { const cases = [ { classId: "mage" as const, level: 20, source: "classic" }, { classId: "rom-mage" as const, level: 20, source: "runewaker" }, { classId: "stormbringer" as const, level: 60, source: "coa" }, ]; registerCombatSpatialProvider({ actor: (id) => ({ id, position: id === PLAYER_AGGRO_ID ? [0, 0, 0] : getMobPosition(id) ?? [0, 0, 0], yaw: 0 }), hasLineOfSight: () => true, projectGroundPoint: (point) => point, }); for (const entry of cases) { useCombatStore.getState().initializeCharacter({ classId: entry.classId, level: entry.level }); const ability = abilitiesForClass(entry.classId) .map((candidate) => abilityAtLevel(candidate, entry.level)) .find((candidate) => ( !candidate.passive && candidate.target === "hostile" && candidate.talentEntryId === undefined && candidate.effects.some((effect) => effect.kind === "damage") )); expect(ability, `${entry.source} executable damage ability`).toBeDefined(); const id = `${entry.source}-target`; const range = Math.max(1, Math.min(10, ability!.range.max)); registerMob(id, [0, 0, range]); useCombatStore.getState().selectMob(id); const before = useCombatStore.getState().mobs[id].health; const cast = useCombatStore.getState().castAbility(ability!.id, { now: 1_000 }); expect(cast.ok, `${entry.source} cast: ${cast.reason ?? "ok"}`).toBe(true); const activeCast = useCombatStore.getState().activeCast; if (activeCast) useCombatStore.getState().tick(0.1, activeCast.completesAt); expect(useCombatStore.getState().mobs[id].health, `${entry.source} damage`).toBeLessThan(before); clearMobRuntimeRegistry(); } }); });