fix: resolve all 8 open bug tickets (#242–#249)
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m32s
CI / Lint, Build & Test (pull_request) Successful in 1m39s

- #242: use formatNumber for crystals in resource bar to respect notation setting
- #243: include current-run gold in companion unlock progress (client + server)
- #244: skip rendering empty reward spans for zero-crystal quest rewards
- #245/#248: skip auto-save while auto-prestige is in-flight to prevent optimistic lock collision
- #246: generate and upload CDN images for crystal_pulse, crystal_surge, crystal_tempest upgrades
- #247: merge daily challenge progress in validateAndSanitize (take max of client vs server) to prevent stale auto-saves rolling back server-side completions
- #249: clear cached signature after buying prestige upgrade to prevent mismatch on next save
This commit is contained in:
2026-04-13 09:41:48 -07:00
committed by Naomi Carrigan
parent e341db56af
commit 9eb99069a6
5 changed files with 58 additions and 6 deletions
+42 -1
View File
@@ -681,6 +681,45 @@ const validateAndSanitize = (
storySpread = { story: previous.story };
}
/*
* Merge daily challenge progress: take the maximum progress for each
* challenge so a stale auto-save arriving after a craft/boss/etc. update
* cannot silently roll back server-side challenge completions.
*/
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 35 -- @preserve */
let dailyChallengesSpread: object = {};
// eslint-disable-next-line stylistic/max-len -- Long condition; splitting would reduce readability
if (incoming.dailyChallenges !== undefined && previous.dailyChallenges !== undefined) {
const previousChallengeMap = new Map(
previous.dailyChallenges.challenges.map((challenge) => {
return [ challenge.id, challenge ];
}),
);
// eslint-disable-next-line stylistic/max-len -- Long chain; splitting would reduce readability
const mergedChallenges = incoming.dailyChallenges.challenges.map((challenge) => {
const serverChallenge = previousChallengeMap.get(challenge.id);
if (serverChallenge === undefined) {
return challenge;
}
// eslint-disable-next-line stylistic/max-len -- Long expression; splitting would reduce readability
const bestProgress = Math.max(challenge.progress, serverChallenge.progress);
return {
...challenge,
completed: bestProgress >= challenge.target,
progress: bestProgress,
};
});
dailyChallengesSpread = {
dailyChallenges: {
...incoming.dailyChallenges,
challenges: mergedChallenges,
},
};
} else if (previous.dailyChallenges !== undefined) {
dailyChallengesSpread = { dailyChallenges: previous.dailyChallenges };
}
return {
...incoming,
achievements,
@@ -693,6 +732,7 @@ const validateAndSanitize = (
...apotheosisSpread,
...explorationSpread,
...storySpread,
...dailyChallengesSpread,
};
};
@@ -1024,7 +1064,8 @@ gameRouter.post("/save", async(context) => {
const companionUnlocks = computeUnlockedCompanionIds({
apotheosisCount: stateToSave.apotheosis?.count ?? 0,
lifetimeBossesDefeated: playerRecord?.lifetimeBossesDefeated ?? 0,
lifetimeGoldEarned: playerRecord?.lifetimeGoldEarned ?? 0,
// eslint-disable-next-line stylistic/max-len -- Long property; splitting would reduce readability
lifetimeGoldEarned: (playerRecord?.lifetimeGoldEarned ?? 0) + stateToSave.player.totalGoldEarned,
lifetimeQuestsCompleted: playerRecord?.lifetimeQuestsCompleted ?? 0,
prestigeCount: stateToSave.prestige.count,
transcendenceCount: stateToSave.transcendence?.count ?? 0,