generated from nhcarrigan/template
fix: resolve all 8 open bug tickets (#242–#249)
- #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:
@@ -681,6 +681,45 @@ const validateAndSanitize = (
|
|||||||
storySpread = { story: previous.story };
|
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 {
|
return {
|
||||||
...incoming,
|
...incoming,
|
||||||
achievements,
|
achievements,
|
||||||
@@ -693,6 +732,7 @@ const validateAndSanitize = (
|
|||||||
...apotheosisSpread,
|
...apotheosisSpread,
|
||||||
...explorationSpread,
|
...explorationSpread,
|
||||||
...storySpread,
|
...storySpread,
|
||||||
|
...dailyChallengesSpread,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1024,7 +1064,8 @@ gameRouter.post("/save", async(context) => {
|
|||||||
const companionUnlocks = computeUnlockedCompanionIds({
|
const companionUnlocks = computeUnlockedCompanionIds({
|
||||||
apotheosisCount: stateToSave.apotheosis?.count ?? 0,
|
apotheosisCount: stateToSave.apotheosis?.count ?? 0,
|
||||||
lifetimeBossesDefeated: playerRecord?.lifetimeBossesDefeated ?? 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,
|
lifetimeQuestsCompleted: playerRecord?.lifetimeQuestsCompleted ?? 0,
|
||||||
prestigeCount: stateToSave.prestige.count,
|
prestigeCount: stateToSave.prestige.count,
|
||||||
transcendenceCount: stateToSave.transcendence?.count ?? 0,
|
transcendenceCount: stateToSave.transcendence?.count ?? 0,
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ const CompanionPanel = (): JSX.Element => {
|
|||||||
const progressByUnlockType: Record<string, number> = {
|
const progressByUnlockType: Record<string, number> = {
|
||||||
apotheosis: state.apotheosis?.count ?? 0,
|
apotheosis: state.apotheosis?.count ?? 0,
|
||||||
lifetimeBosses: state.player.lifetimeBossesDefeated,
|
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,
|
lifetimeQuests: state.player.lifetimeQuestsCompleted,
|
||||||
prestige: state.prestige.count,
|
prestige: state.prestige.count,
|
||||||
transcendence: state.transcendence?.count ?? 0,
|
transcendence: state.transcendence?.count ?? 0,
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ const QuestCard = ({
|
|||||||
}
|
}
|
||||||
<div className="quest-rewards">
|
<div className="quest-rewards">
|
||||||
{quest.rewards.map((reward, rewardIndex) => {
|
{quest.rewards.map((reward, rewardIndex) => {
|
||||||
|
if (reward.type === "crystals" && (reward.amount ?? 0) === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span className="reward-tag" key={`${reward.type}-${reward.targetId ?? String(reward.amount ?? rewardIndex)}`}>
|
<span className="reward-tag" key={`${reward.type}-${reward.targetId ?? String(reward.amount ?? rewardIndex)}`}>
|
||||||
{reward.type === "gold"
|
{reward.type === "gold"
|
||||||
@@ -121,7 +124,6 @@ const QuestCard = ({
|
|||||||
{reward.type === "essence"
|
{reward.type === "essence"
|
||||||
&& `✨ ${formatNumber(reward.amount ?? 0)}`}
|
&& `✨ ${formatNumber(reward.amount ?? 0)}`}
|
||||||
{reward.type === "crystals"
|
{reward.type === "crystals"
|
||||||
&& (reward.amount ?? 0) > 0
|
|
||||||
&& `💎 ${formatNumber(reward.amount ?? 0)}`}
|
&& `💎 ${formatNumber(reward.amount ?? 0)}`}
|
||||||
{reward.type === "upgrade" && "🔓 Upgrade"}
|
{reward.type === "upgrade" && "🔓 Upgrade"}
|
||||||
{reward.type === "adventurer" && "👥 New Adventurer"}
|
{reward.type === "adventurer" && "👥 New Adventurer"}
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ const ResourceBar = ({
|
|||||||
: ""}`}>
|
: ""}`}>
|
||||||
<span className="resource-icon">{"💎"}</span>
|
<span className="resource-icon">{"💎"}</span>
|
||||||
<span className="resource-value">
|
<span className="resource-value">
|
||||||
{formatInteger(crystals)}
|
{formatNumber(crystals)}
|
||||||
</span>
|
</span>
|
||||||
<span className="resource-label">{"Crystals"}</span>
|
<span className="resource-label">{"Crystals"}</span>
|
||||||
{crystalsFull
|
{crystalsFull
|
||||||
|
|||||||
@@ -1356,10 +1356,11 @@ export const GameProvider = ({
|
|||||||
newlyFailedQuestsReference.current = [];
|
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) {
|
if (Date.now() - lastSaveReference.current >= autoSaveIntervalMs) {
|
||||||
lastSaveReference.current = Date.now();
|
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({
|
void saveGame({
|
||||||
state: stateReference.current,
|
state: stateReference.current,
|
||||||
...signatureReference.current === null
|
...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) {
|
} catch (error_: unknown) {
|
||||||
logError("buy_prestige_upgrade", error_);
|
logError("buy_prestige_upgrade", error_);
|
||||||
// Silently ignore — server errors shouldn't crash the UI
|
// Silently ignore — server errors shouldn't crash the UI
|
||||||
|
|||||||
Reference in New Issue
Block a user