/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */ /* eslint-disable @typescript-eslint/consistent-type-assertions -- Tests build minimal state objects */ import { describe, expect, it } from "vitest"; import { calculateOfflineEarnings } from "../../src/services/offlineProgress.js"; import type { GameState } from "@elysium/types"; const makeState = (overrides: Partial = {}): GameState => ({ lastTickAt: 0, adventurers: [], upgrades: [], equipment: [], prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestonesIncomeMultiplier: 1, runestonesEssenceMultiplier: 1, }, ...overrides, } as GameState); describe("calculateOfflineEarnings", () => { it("returns zero earnings when no adventurers", () => { const state = makeState({ lastTickAt: 0 }); const result = calculateOfflineEarnings(state, 60_000); expect(result.offlineGold).toBe(0); expect(result.offlineEssence).toBe(0); expect(result.offlineSeconds).toBe(60); }); it("returns zero when all adventurers have count 0", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 0, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], }); const result = calculateOfflineEarnings(state, 60_000); expect(result.offlineGold).toBe(0); }); it("returns zero when adventurer is not unlocked", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: false, count: 5, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], }); const result = calculateOfflineEarnings(state, 60_000); expect(result.offlineGold).toBe(0); }); it("calculates basic gold earnings correctly", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 2, goldPerSecond: 5, essencePerSecond: 0 }] as GameState["adventurers"], }); const result = calculateOfflineEarnings(state, 10_000); // 2 adventurers × 5 gps × 10 seconds = 100 gold expect(result.offlineGold).toBe(100); expect(result.offlineSeconds).toBe(10); }); it("calculates basic essence earnings correctly", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 0, essencePerSecond: 3 }] as GameState["adventurers"], }); const result = calculateOfflineEarnings(state, 10_000); expect(result.offlineEssence).toBe(30); }); it("caps earnings at 8 hours regardless of elapsed time", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 1, essencePerSecond: 0 }] as GameState["adventurers"], }); const twelveHoursMs = 12 * 60 * 60 * 1000; const result = calculateOfflineEarnings(state, twelveHoursMs); const maxSeconds = 8 * 60 * 60; expect(result.offlineGold).toBe(maxSeconds); expect(result.offlineSeconds).toBe(maxSeconds); }); it("applies global upgrade multiplier", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], upgrades: [{ id: "u1", purchased: true, target: "global", multiplier: 2 }] as GameState["upgrades"], }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineGold).toBe(20); }); it("applies adventurer-specific upgrade multiplier", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "peasant", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], upgrades: [{ id: "u1", purchased: true, target: "adventurer", adventurerId: "peasant", multiplier: 3 }] as GameState["upgrades"], }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineGold).toBe(30); }); it("does not apply upgrade for different adventurer", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "peasant", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], upgrades: [{ id: "u1", purchased: true, target: "adventurer", adventurerId: "knight", multiplier: 3 }] as GameState["upgrades"], }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineGold).toBe(10); }); it("applies equipment gold multiplier for equipped items only", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], equipment: [ { id: "e1", equipped: true, bonus: { goldMultiplier: 2 } }, { id: "e2", equipped: false, bonus: { goldMultiplier: 5 } }, ] as GameState["equipment"], }); const result = calculateOfflineEarnings(state, 1_000); // Only e1 applies: 10 × 2 × 1s = 20 expect(result.offlineGold).toBe(20); }); it("applies runestone income multiplier to gold", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestonesIncomeMultiplier: 2, runestonesEssenceMultiplier: 1, } as GameState["prestige"], }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineGold).toBe(20); }); it("applies runestone essence multiplier to essence", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 0, essencePerSecond: 5 }] as GameState["adventurers"], prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestonesIncomeMultiplier: 1, runestonesEssenceMultiplier: 3, } as GameState["prestige"], }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineEssence).toBe(15); }); it("defaults to 1 when runestonesIncomeMultiplier is undefined", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 2 }] as GameState["adventurers"], // Prestige without runestone multiplier fields — hits the ?? 1 fallback prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] } as GameState["prestige"], }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineGold).toBe(10); expect(result.offlineEssence).toBe(2); }); it("defaults to 1 when equipment is undefined", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], equipment: undefined, }); const result = calculateOfflineEarnings(state, 1_000); expect(result.offlineGold).toBe(10); }); it("defaults goldMultiplier to 1 when equipment item has no goldMultiplier", () => { const state = makeState({ lastTickAt: 0, adventurers: [{ id: "a", unlocked: true, count: 1, goldPerSecond: 10, essencePerSecond: 0 }] as GameState["adventurers"], equipment: [ { id: "e1", equipped: true, bonus: {} }, // no goldMultiplier — hits ?? 1 ] as GameState["equipment"], }); const result = calculateOfflineEarnings(state, 1_000); // goldMultiplier defaults to 1, so no boost expect(result.offlineGold).toBe(10); }); });