Files
elysium/apps/api/test/services/offlineProgress.spec.ts
T
hikari b67eae9d46 test: add 100% coverage for apps/api and packages/types
Adds full Vitest test suites with @vitest/coverage-v8, targeting 100%
statement/branch/function/line coverage. Uses v8 ignore comments for
genuinely unreachable defensive branches.
2026-03-07 19:59:48 -08:00

190 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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> = {}): 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);
});
});