From 9eb99069a65b8eccd0bed3bae959928335d06908 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 13 Apr 2026 09:41:48 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20all=208=20open=20bug=20tickets?= =?UTF-8?q?=20(#242=E2=80=93#249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #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 --- apps/api/src/routes/game.ts | 43 ++++++++++++++++++- .../src/components/game/companionPanel.tsx | 3 +- apps/web/src/components/game/questPanel.tsx | 4 +- apps/web/src/components/ui/resourceBar.tsx | 2 +- apps/web/src/context/gameContext.tsx | 12 +++++- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index fda6ccd..b0a56e2 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -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, diff --git a/apps/web/src/components/game/companionPanel.tsx b/apps/web/src/components/game/companionPanel.tsx index a770a60..93c6122 100644 --- a/apps/web/src/components/game/companionPanel.tsx +++ b/apps/web/src/components/game/companionPanel.tsx @@ -163,7 +163,8 @@ const CompanionPanel = (): JSX.Element => { const progressByUnlockType: Record = { apotheosis: state.apotheosis?.count ?? 0, lifetimeBosses: state.player.lifetimeBossesDefeated, - lifetimeGold: state.player.lifetimeGoldEarned, + // eslint-disable-next-line stylistic/max-len -- Long expression; splitting would reduce readability + lifetimeGold: state.player.lifetimeGoldEarned + state.player.totalGoldEarned, lifetimeQuests: state.player.lifetimeQuestsCompleted, prestige: state.prestige.count, transcendence: state.transcendence?.count ?? 0, diff --git a/apps/web/src/components/game/questPanel.tsx b/apps/web/src/components/game/questPanel.tsx index a71872c..42f0b61 100644 --- a/apps/web/src/components/game/questPanel.tsx +++ b/apps/web/src/components/game/questPanel.tsx @@ -114,6 +114,9 @@ const QuestCard = ({ }
{quest.rewards.map((reward, rewardIndex) => { + if (reward.type === "crystals" && (reward.amount ?? 0) === 0) { + return null; + } return ( {reward.type === "gold" @@ -121,7 +124,6 @@ const QuestCard = ({ {reward.type === "essence" && `✨ ${formatNumber(reward.amount ?? 0)}`} {reward.type === "crystals" - && (reward.amount ?? 0) > 0 && `💎 ${formatNumber(reward.amount ?? 0)}`} {reward.type === "upgrade" && "🔓 Upgrade"} {reward.type === "adventurer" && "👥 New Adventurer"} diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index acfc0ff..f992b29 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -218,7 +218,7 @@ const ResourceBar = ({ : ""}`}> {"💎"} - {formatInteger(crystals)} + {formatNumber(crystals)} {"Crystals"} {crystalsFull diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index 39bc7ad..7da5708 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -1356,10 +1356,11 @@ export const GameProvider = ({ newlyFailedQuestsReference.current = []; } - // Auto-save every 30 seconds (skip if a force sync is in-flight to avoid signature collisions) + // Auto-save every 30 seconds (skip if a force sync or auto-prestige is in-flight to avoid signature collisions) if (Date.now() - lastSaveReference.current >= autoSaveIntervalMs) { lastSaveReference.current = Date.now(); - if (stateReference.current !== null && !isSyncingReference.current) { + // eslint-disable-next-line stylistic/max-len -- Long condition; splitting would reduce readability + if (stateReference.current !== null && !isSyncingReference.current && !isAutoPrestigingReference.current) { void saveGame({ state: stateReference.current, ...signatureReference.current === null @@ -1856,6 +1857,13 @@ export const GameProvider = ({ }, }; }); + + /* + * Buying a prestige upgrade mutates DB state; clear the cached signature + * so the next auto-save doesn't collide with a stale one. + */ + signatureReference.current = null; + localStorage.removeItem("elysium_save_signature"); } catch (error_: unknown) { logError("buy_prestige_upgrade", error_); // Silently ignore — server errors shouldn't crash the UI -- 2.52.0