diff --git a/apps/api/src/services/prestige.ts b/apps/api/src/services/prestige.ts index db2e0be..0d9140b 100644 --- a/apps/api/src/services/prestige.ts +++ b/apps/api/src/services/prestige.ts @@ -205,10 +205,51 @@ const buildPostPrestigeState = ( }; const freshState = initialGameState(currentState.player, characterName); + + // Compute current-run contributions to accumulate into lifetime totals + const runBossesDefeated = currentState.bosses.filter((boss) => { + return boss.status === "defeated"; + }).length; + const runQuestsCompleted = currentState.quests.filter((quest) => { + return quest.status === "completed"; + }).length; + let runAdventurersRecruited = 0; + for (const adventurer of currentState.adventurers) { + runAdventurersRecruited = runAdventurersRecruited + adventurer.count; + } + const runAchievementsUnlocked = currentState.achievements.filter( + (achievement) => { + return achievement.unlockedAt !== null; + }, + ).length; + const prestigeState: GameState = { ...freshState, lastTickAt: Date.now(), - prestige: prestigeData, + + /* + * Fold current-run totals into lifetime stats so the GameState reflects + * the true all-time values immediately after prestige. + */ + player: { + ...freshState.player, + lifetimeAchievementsUnlocked: + freshState.player.lifetimeAchievementsUnlocked + + runAchievementsUnlocked, + lifetimeAdventurersRecruited: + freshState.player.lifetimeAdventurersRecruited + + runAdventurersRecruited, + lifetimeBossesDefeated: + freshState.player.lifetimeBossesDefeated + runBossesDefeated, + lifetimeClicks: + freshState.player.lifetimeClicks + currentState.player.totalClicks, + lifetimeGoldEarned: + freshState.player.lifetimeGoldEarned + + currentState.player.totalGoldEarned, + lifetimeQuestsCompleted: + freshState.player.lifetimeQuestsCompleted + runQuestsCompleted, + }, + prestige: prestigeData, // Codex lore persists across prestiges — players keep their discovered entries ...currentState.codex === undefined ? {} diff --git a/apps/api/test/services/prestige.spec.ts b/apps/api/test/services/prestige.spec.ts index 724a021..6b09d2f 100644 --- a/apps/api/test/services/prestige.spec.ts +++ b/apps/api/test/services/prestige.spec.ts @@ -13,14 +13,24 @@ import { } 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 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 => @@ -242,4 +252,85 @@ describe("buildPostPrestigeState", () => { 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("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("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); + }); });