fix: preserve all-time stats, achievements, and boss first-kill across prestige (#47)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m4s
CI / Lint, Build & Test (push) Successful in 1m8s

Resolves #37, resolves #38, and resolves #39 — three related bugs where prestige incorrectly reset data that should survive all prestige resets.

## Changes

### fix: preserve lifetime player stats across prestige (#37)
After prestige, `GameState.player.lifetime*` fields were stale — they reflected values from *before* the current run. The Prisma Player record was incremented correctly, but the GameState JSON saved to the DB had old values, so the UI showed wrong all-time totals on reload.

`buildPostPrestigeState` now computes the run-stat contributions (bosses defeated, quests completed, adventurers recruited, achievements unlocked, gold earned, clicks) and folds them into the fresh player object before writing the prestige state.

### fix: preserve achievements across prestige (#38)
`buildPostPrestigeState` was reconstructing achievements from `defaultAchievements` (via `initialGameState`), resetting all unlocked achievements on every prestige. Achievements are now carried forward from `currentState.achievements` instead.

### fix: preserve boss first-kill state across prestige (#39)
Added `bountyRunestonesClaimed?: boolean` to the `Boss` type. The boss challenge route now:
- Only awards the first-kill bounty runestones if `bountyRunestonesClaimed !== true`
- Sets `bountyRunestonesClaimed = true` on first defeat

`buildPostPrestigeState` maps the fresh boss list and carries the `bountyRunestonesClaimed` flag forward from the current state, so the bounty is never re-awarded in subsequent prestige runs. The boss panel badge is also hidden for bosses whose bounty is already claimed.

## Test Coverage
All three fixes include new tests covering the new behaviours. API coverage remains at 100%.

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #47
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #47.
This commit is contained in:
2026-03-09 21:53:58 -07:00
committed by Naomi Carrigan
parent 4d7e624358
commit d0790890ee
6 changed files with 242 additions and 14 deletions
+21
View File
@@ -305,4 +305,25 @@ describe("boss route", () => {
const res = await challenge({ bossId: "test_boss" });
expect(res.status).toBe(500);
});
it("does not re-award bounty runestones when bountyRunestonesClaimed is true", async () => {
const state = makeState({
bosses: [makeBoss({
bountyRunestonesClaimed: true,
currentHp: 100,
damagePerSecond: 1,
maxHp: 100,
})] as GameState["bosses"],
adventurers: [makeAdventurer()] as GameState["adventurers"],
prestige: { count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 5 },
zones: [],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await challenge({ bossId: "test_boss" });
expect(res.status).toBe(200);
const body = await res.json() as { won: boolean; rewards: { bountyRunestones: number } };
expect(body.won).toBe(true);
expect(body.rewards.bountyRunestones).toBe(0);
});
});