/* 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, lifetimeGoldEarned = 0, totalClicks = 0, ) => ({ avatar: null, characterName: "Tester", discordId: "test_id", discriminator: "0", lifetimeAchievementsUnlocked: 0, lifetimeAdventurersRecruited: 0, lifetimeBossesDefeated: 0, lifetimeClicks: 0, lifetimeGoldEarned: lifetimeGoldEarned, lifetimeQuestsCompleted: 0, totalClicks: totalClicks, totalGoldEarned: totalGoldEarned, username: "testuser", }); const makeMinimalState = (overrides: Partial = {}): 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); }); it("accumulates current-run gold into lifetime total", () => { const state = makeMinimalState({ player: makePlayer(4_000_000, 1_000_000), }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.player.lifetimeGoldEarned).toBe(5_000_000); }); it("accumulates current-run clicks into lifetime total", () => { const state = makeMinimalState({ player: makePlayer(4_000_000, 0, 500), }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.player.lifetimeClicks).toBe(500); }); it("accumulates defeated bosses into lifetime total", () => { const defeatedBoss = { bountyRunestones: 0, crystalReward: 0, currentHp: 0, damagePerSecond: 10, description: "A boss", equipmentRewards: [] as string[], essenceReward: 0, goldReward: 100, id: "boss_1", maxHp: 100, name: "Boss One", prestigeRequirement: 0, status: "defeated" as const, upgradeRewards: [] as string[], zoneId: "zone_1", }; const state = makeMinimalState({ bosses: [ defeatedBoss ] }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.player.lifetimeBossesDefeated).toBe(1); }); it("preserves bountyRunestonesClaimed flag on bosses across prestige", () => { const claimedBoss = { bountyRunestones: 5, bountyRunestonesClaimed: true, crystalReward: 0, currentHp: 0, damagePerSecond: 10, description: "A boss", equipmentRewards: [] as string[], essenceReward: 0, goldReward: 100, id: "troll_king", maxHp: 100, name: "Troll King", prestigeRequirement: 0, status: "defeated" as const, upgradeRewards: [] as string[], zoneId: "verdant_vale", }; const state = makeMinimalState({ bosses: [ claimedBoss ] as GameState["bosses"] }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); const matchingBoss = prestigeState.bosses.find((boss) => { return boss.id === "troll_king"; }); expect(matchingBoss?.bountyRunestonesClaimed).toBe(true); }); it("accumulates completed quests into lifetime total", () => { const quest = { id: "q_1", name: "A Quest", description: "Do the thing", status: "completed" as const, zoneId: "zone_1", }; const state = makeMinimalState({ quests: [ quest ] as GameState["quests"] }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.player.lifetimeQuestsCompleted).toBe(1); }); it("accumulates recruited adventurers into lifetime total", () => { const adventurer = { combatPower: 10, count: 5, essencePerSecond: 0, goldPerSecond: 1, id: "adv_1", level: 1, unlocked: true, }; const state = makeMinimalState({ adventurers: [ adventurer ] as GameState["adventurers"] }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.player.lifetimeAdventurersRecruited).toBe(5); }); it("preserves achievements from current state across prestige", () => { const achievement = { description: "Did a thing", id: "ach_persisted", name: "Achiever", requirement: 1, type: "totalClicks" as const, unlockedAt: Date.now(), }; const state = makeMinimalState({ achievements: [ achievement ] as GameState["achievements"] }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.achievements).toEqual([ achievement ]); }); it("accumulates unlocked achievements into lifetime total", () => { const achievement = { description: "Did a thing", id: "ach_1", name: "Achiever", requirement: 1, type: "totalClicks" as const, unlockedAt: Date.now(), }; const state = makeMinimalState({ achievements: [ achievement ] as GameState["achievements"] }); const { prestigeState } = buildPostPrestigeState(state, "Tester"); expect(prestigeState.player.lifetimeAchievementsUnlocked).toBe(1); }); });