generated from nhcarrigan/template
d1d1f70c75
- Fix strict-boolean-expressions in 7 route files (runtime body validation) - Fix no-unnecessary-condition in profile.ts and offlineProgress.ts (defensive null checks) - Extend v8 ignore next-N counts in game.ts to reach 100% coverage - Add CI requirements to CLAUDE.md (lint + build + test must pass before commit) - Add manual verification checklist (verify.md) - Remove progress.md
246 lines
8.9 KiB
TypeScript
246 lines
8.9 KiB
TypeScript
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
||
/* eslint-disable max-lines -- 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 {
|
||
buildPostPrestigeState,
|
||
calculateMilestoneBonus,
|
||
calculatePrestigeThreshold,
|
||
calculateProductionMultiplier,
|
||
calculateRunestones,
|
||
computeRunestoneMultipliers,
|
||
isEligibleForPrestige,
|
||
} from "../../src/services/prestige.js";
|
||
import type { GameState } from "@elysium/types";
|
||
|
||
const makePlayer = (totalGoldEarned: number) => ({
|
||
discordId: "test_id",
|
||
username: "testuser",
|
||
discriminator: "0",
|
||
avatar: null,
|
||
totalGoldEarned,
|
||
totalClicks: 0,
|
||
characterName: "Tester",
|
||
});
|
||
|
||
const makeMinimalState = (overrides: Partial<GameState> = {}): GameState =>
|
||
({
|
||
player: makePlayer(0),
|
||
resources: { gold: 0, essence: 0, crystals: 0, runestones: 0 },
|
||
adventurers: [],
|
||
upgrades: [],
|
||
quests: [],
|
||
bosses: [],
|
||
equipment: [],
|
||
achievements: [],
|
||
zones: [],
|
||
exploration: { areas: [], materials: [], craftedRecipeIds: [], craftedGoldMultiplier: 1, craftedEssenceMultiplier: 1, craftedClickMultiplier: 1, craftedCombatMultiplier: 1 },
|
||
companions: { unlockedCompanionIds: [], activeCompanionId: null },
|
||
prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||
baseClickPower: 1,
|
||
lastTickAt: 0,
|
||
schemaVersion: 1,
|
||
...overrides,
|
||
} as GameState);
|
||
|
||
describe("calculatePrestigeThreshold", () => {
|
||
it("returns base threshold at count 0", () => {
|
||
expect(calculatePrestigeThreshold(0)).toBe(1_000_000);
|
||
});
|
||
|
||
it("returns 5× at count 1", () => {
|
||
expect(calculatePrestigeThreshold(1)).toBe(5_000_000);
|
||
});
|
||
|
||
it("returns 25× at count 2", () => {
|
||
expect(calculatePrestigeThreshold(2)).toBe(25_000_000);
|
||
});
|
||
|
||
it("applies threshold multiplier correctly", () => {
|
||
expect(calculatePrestigeThreshold(0, 2)).toBe(2_000_000);
|
||
});
|
||
});
|
||
|
||
describe("isEligibleForPrestige", () => {
|
||
it("returns true when totalGoldEarned meets threshold", () => {
|
||
const state = makeMinimalState({ player: makePlayer(1_000_000) });
|
||
expect(isEligibleForPrestige(state)).toBe(true);
|
||
});
|
||
|
||
it("returns false when totalGoldEarned is below threshold", () => {
|
||
const state = makeMinimalState({ player: makePlayer(999_999) });
|
||
expect(isEligibleForPrestige(state)).toBe(false);
|
||
});
|
||
|
||
it("uses echoPrestigeThresholdMultiplier from transcendence when present", () => {
|
||
const state = makeMinimalState({
|
||
player: makePlayer(2_000_000),
|
||
transcendence: {
|
||
count: 1, echoes: 0, purchasedUpgradeIds: [],
|
||
echoIncomeMultiplier: 1, echoCombatMultiplier: 1,
|
||
echoPrestigeThresholdMultiplier: 2,
|
||
echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1,
|
||
},
|
||
});
|
||
// threshold = 1_000_000 × 2 = 2_000_000 — exactly meets
|
||
expect(isEligibleForPrestige(state)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("calculateRunestones", () => {
|
||
it("calculates basic runestones formula", () => {
|
||
// floor(sqrt(4_000_000 / 1_000_000)) × 10 = floor(2) × 10 = 20
|
||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
|
||
expect(result).toBe(20);
|
||
});
|
||
|
||
it("applies echo runestone multiplier", () => {
|
||
// floor(sqrt(4) × 10) = 20; × 2 = 40
|
||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
|
||
expect(result).toBe(40);
|
||
});
|
||
|
||
it("applies purchased runestone upgrade multiplier", () => {
|
||
// With "runestones_1" purchased (multiplier 1.25): floor(20 × 1.25) = 25
|
||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] });
|
||
expect(result).toBeGreaterThan(20);
|
||
});
|
||
});
|
||
|
||
describe("calculateProductionMultiplier", () => {
|
||
it("returns 1 at count 0", () => {
|
||
expect(calculateProductionMultiplier(0)).toBe(1);
|
||
});
|
||
|
||
it("returns 1.15 at count 1", () => {
|
||
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.15);
|
||
});
|
||
|
||
it("scales exponentially", () => {
|
||
expect(calculateProductionMultiplier(10)).toBeCloseTo(Math.pow(1.15, 10));
|
||
});
|
||
});
|
||
|
||
describe("calculateMilestoneBonus", () => {
|
||
it("returns 0 for non-milestone prestiges", () => {
|
||
expect(calculateMilestoneBonus(1)).toBe(0);
|
||
expect(calculateMilestoneBonus(3)).toBe(0);
|
||
expect(calculateMilestoneBonus(4)).toBe(0);
|
||
});
|
||
|
||
it("returns 25 at prestige 5", () => {
|
||
expect(calculateMilestoneBonus(5)).toBe(25);
|
||
});
|
||
|
||
it("returns 50 at prestige 10", () => {
|
||
expect(calculateMilestoneBonus(10)).toBe(50);
|
||
});
|
||
|
||
it("returns 75 at prestige 15", () => {
|
||
expect(calculateMilestoneBonus(15)).toBe(75);
|
||
});
|
||
});
|
||
|
||
describe("computeRunestoneMultipliers", () => {
|
||
it("returns all 1s with empty ids", () => {
|
||
const result = computeRunestoneMultipliers([]);
|
||
expect(result.runestonesIncomeMultiplier).toBe(1);
|
||
expect(result.runestonesClickMultiplier).toBe(1);
|
||
expect(result.runestonesEssenceMultiplier).toBe(1);
|
||
expect(result.runestonesCrystalMultiplier).toBe(1);
|
||
});
|
||
|
||
it("applies income upgrade when purchased", () => {
|
||
const result = computeRunestoneMultipliers(["income_1"]);
|
||
expect(result.runestonesIncomeMultiplier).toBeGreaterThan(1);
|
||
expect(result.runestonesClickMultiplier).toBe(1);
|
||
});
|
||
|
||
it("applies click upgrade when purchased", () => {
|
||
const result = computeRunestoneMultipliers(["click_power_1"]);
|
||
expect(result.runestonesClickMultiplier).toBeGreaterThan(1);
|
||
expect(result.runestonesIncomeMultiplier).toBe(1);
|
||
});
|
||
|
||
it("applies essence upgrade when purchased", () => {
|
||
const result = computeRunestoneMultipliers(["essence_1"]);
|
||
expect(result.runestonesEssenceMultiplier).toBeGreaterThan(1);
|
||
});
|
||
|
||
it("applies crystals upgrade when purchased", () => {
|
||
const result = computeRunestoneMultipliers(["crystal_1"]);
|
||
expect(result.runestonesCrystalMultiplier).toBeGreaterThan(1);
|
||
});
|
||
});
|
||
|
||
describe("buildPostPrestigeState", () => {
|
||
it("increments prestige count", () => {
|
||
const state = makeMinimalState({ player: makePlayer(4_000_000) });
|
||
const { prestigeData } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeData.count).toBe(1);
|
||
});
|
||
|
||
it("sums runestones earned", () => {
|
||
const state = makeMinimalState({ player: makePlayer(4_000_000) });
|
||
const { prestigeData, runestonesEarned } = buildPostPrestigeState(state, "Tester");
|
||
expect(runestonesEarned).toBeGreaterThan(0);
|
||
expect(prestigeData.runestones).toBe(runestonesEarned);
|
||
});
|
||
|
||
it("adds milestone runestones at prestige 5", () => {
|
||
const state = makeMinimalState({
|
||
player: makePlayer(100_000_000),
|
||
prestige: { count: 4, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||
});
|
||
const { milestoneRunestones } = buildPostPrestigeState(state, "Tester");
|
||
expect(milestoneRunestones).toBe(25);
|
||
});
|
||
|
||
it("persists codex from current state", () => {
|
||
const codex = { entries: [{ id: "e1", unlockedAt: 1000, sourceType: "exploration" as const }] };
|
||
const state = makeMinimalState({ codex });
|
||
const { prestigeState } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeState.codex).toEqual(codex);
|
||
});
|
||
|
||
it("persists story from current state", () => {
|
||
const story = { unlockedChapterIds: ["ch1"], completedChapters: [] };
|
||
const state = makeMinimalState({ story });
|
||
const { prestigeState } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeState.story).toEqual(story);
|
||
});
|
||
|
||
it("persists transcendence from current state", () => {
|
||
const transcendence = {
|
||
count: 1, echoes: 10, purchasedUpgradeIds: [],
|
||
echoIncomeMultiplier: 1, echoCombatMultiplier: 1,
|
||
echoPrestigeThresholdMultiplier: 1,
|
||
echoPrestigeRunestoneMultiplier: 1, echoMetaMultiplier: 1,
|
||
};
|
||
const state = makeMinimalState({ transcendence });
|
||
const { prestigeState } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeState.transcendence).toEqual(transcendence);
|
||
});
|
||
|
||
it("preserves autoPrestigeEnabled when set", () => {
|
||
const state = makeMinimalState({
|
||
prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [], autoPrestigeEnabled: true },
|
||
});
|
||
const { prestigeData } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeData.autoPrestigeEnabled).toBe(true);
|
||
});
|
||
|
||
it("omits autoPrestigeEnabled when not set", () => {
|
||
const state = makeMinimalState();
|
||
const { prestigeData } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeData.autoPrestigeEnabled).toBeUndefined();
|
||
});
|
||
|
||
it("preserves apotheosis data across prestige", () => {
|
||
const apotheosis = { count: 2 };
|
||
const state = makeMinimalState({ apotheosis });
|
||
const { prestigeState } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeState.apotheosis).toEqual(apotheosis);
|
||
});
|
||
});
|