generated from nhcarrigan/template
427 lines
16 KiB
TypeScript
427 lines
16 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,
|
||
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> = {}): 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", () => {
|
||
// base × (0+1)^2.5 = 1_000_000 × 1 = 1_000_000
|
||
expect(calculatePrestigeThreshold(0)).toBe(1_000_000);
|
||
});
|
||
|
||
it("returns base × 2^2.5 at count 1", () => {
|
||
// base × (1+1)^2.5 = 1_000_000 × 2^2.5
|
||
expect(calculatePrestigeThreshold(1)).toBeCloseTo(1_000_000 * Math.pow(2, 2.5));
|
||
});
|
||
|
||
it("returns base × 3^2.5 at count 2", () => {
|
||
// base × (2+1)^2.5 = 1_000_000 × 3^2.5
|
||
expect(calculatePrestigeThreshold(2)).toBeCloseTo(1_000_000 * Math.pow(3, 2.5));
|
||
});
|
||
|
||
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(cbrt(4_000_000 / 1_000_000)) × 15 = floor(cbrt(4)) × 15 = 1 × 15 = 15
|
||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
|
||
expect(result).toBe(15);
|
||
});
|
||
|
||
it("applies echo runestone multiplier", () => {
|
||
// floor(cbrt(4)) × 15 = 15; × 2 = 30
|
||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
|
||
expect(result).toBe(30);
|
||
});
|
||
|
||
it("applies purchased runestone upgrade multiplier", () => {
|
||
// With "runestone_gain_1" purchased (multiplier 1.25): floor(15 × 1.25) = 18
|
||
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] });
|
||
expect(result).toBe(18);
|
||
});
|
||
|
||
it("caps base runestones before multipliers", () => {
|
||
// cbrt(9_261_000_000 / 1_000_000) = cbrt(9261) = 21 → 21 × 10 = 210, capped at 200
|
||
const result = calculateRunestones({ totalGoldEarned: 9_261_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
|
||
expect(result).toBe(200);
|
||
});
|
||
});
|
||
|
||
describe("calculateProductionMultiplier", () => {
|
||
it("returns 1 at count 0", () => {
|
||
expect(calculateProductionMultiplier(0)).toBe(1);
|
||
});
|
||
|
||
it("returns 1.3 at count 1", () => {
|
||
expect(calculateProductionMultiplier(1)).toBeCloseTo(1.3);
|
||
});
|
||
|
||
it("scales exponentially", () => {
|
||
expect(calculateProductionMultiplier(10)).toBeCloseTo(Math.pow(1.3, 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 100 at prestige 10", () => {
|
||
expect(calculateMilestoneBonus(10)).toBe(100);
|
||
});
|
||
|
||
it("returns 225 at prestige 15", () => {
|
||
expect(calculateMilestoneBonus(15)).toBe(225);
|
||
});
|
||
});
|
||
|
||
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 autoPrestigeMaxRunestonesOnly when set", () => {
|
||
const state = makeMinimalState({
|
||
prestige: { autoPrestigeMaxRunestonesOnly: true, count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
|
||
});
|
||
const { prestigeData } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeData.autoPrestigeMaxRunestonesOnly).toBe(true);
|
||
});
|
||
|
||
it("omits autoPrestigeMaxRunestonesOnly when not set", () => {
|
||
const state = makeMinimalState();
|
||
const { prestigeData } = buildPostPrestigeState(state, "Tester");
|
||
expect(prestigeData.autoPrestigeMaxRunestonesOnly).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("sets bountyRunestonesClaimed on bosses defeated before the flag was introduced", () => {
|
||
const legacyDefeatedBoss = {
|
||
bountyRunestones: 5,
|
||
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: [ legacyDefeatedBoss ] 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);
|
||
});
|
||
});
|