2 Commits

Author SHA1 Message Date
nerdychara a62f714da1 fix: update shadow marsh explore times
CI / Lint, Build & Test (pull_request) Successful in 1m15s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m23s
2026-07-09 14:43:53 -04:00
hikari 9bb1d01d2b fix: resolve all 8 open bug tickets (#242–#249) (#250)
CI / Lint, Build & Test (push) Successful in 2m15s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m13s
## Summary

- **#242** — Crystals in the resource bar now use `formatNumber` to respect the player's notation setting (suffix/scientific/engineering)
- **#243** — Companion unlock progress includes current-run gold (`totalGoldEarned`) on both client and server, so companions unlock at the correct threshold
- **#244** — Empty green reward bubbles no longer render for quest crystal rewards with a zero amount
- **#245/#248** — Auto-save skips when `isAutoPrestigingReference.current` is true, preventing it from racing with an in-flight prestige and breaking the optimistic lock
- **#246** — Generated and uploaded CDN images for `crystal_pulse`, `crystal_surge`, and `crystal_tempest` upgrades
- **#247** — `validateAndSanitize` merges daily challenge progress by taking the max of client vs. server progress per challenge, so stale auto-saves can no longer roll back server-side completions
- **#249** — Cached save signature is cleared after `buyPrestigeUpgrade` succeeds, preventing a stale-signature mismatch on the next auto-save

## Test plan

- [ ] Lint passes (`pnpm lint`)
- [ ] Build passes (`pnpm build`)
- [ ] Tests pass with 100% coverage (`pnpm test`)
- [ ] Crystals display in resource bar respects notation setting
- [ ] No empty reward bubbles on quests that don't award crystals
- [ ] Companion progress bar shows correct value including current-run gold
- [ ] Auto-prestige no longer causes save errors
- [ ] Crafting a recipe updates daily challenge progress persistently (not rolled back by next auto-save)
- [ ] Buying a prestige upgrade does not cause a signature mismatch error on next save
- [ ] Crystal upgrade images display correctly in-game

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #250
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-04-13 09:50:20 -07:00
6 changed files with 60 additions and 8 deletions
+2 -2
View File
@@ -626,7 +626,7 @@ export const defaultExplorations: Array<ExplorationArea> = [
{ {
description: description:
"A burial mound. Something was interred here that should not have been — or perhaps something interred itself, which is a different and more troubling problem.", "A burial mound. Something was interred here that should not have been — or perhaps something interred itself, which is a different and more troubling problem.",
durationSeconds: 90 * 60, durationSeconds: 75 * 60,
events: [ events: [
{ {
effect: { amount: 70_000, type: "gold_gain" }, effect: { amount: 70_000, type: "gold_gain" },
@@ -671,7 +671,7 @@ export const defaultExplorations: Array<ExplorationArea> = [
{ {
description: description:
"The bottommost point of the Shadow Marshes, where the water is perfectly still and perfectly black. Your scouts can see the bottom. The bottom is very far down.", "The bottommost point of the Shadow Marshes, where the water is perfectly still and perfectly black. Your scouts can see the bottom. The bottom is very far down.",
durationSeconds: 90 * 60, durationSeconds: 100 * 60,
events: [ events: [
{ {
effect: { amount: 120_000, type: "gold_gain" }, effect: { amount: 120_000, type: "gold_gain" },
+42 -1
View File
@@ -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,
+3 -1
View File
@@ -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"}
+1 -1
View File
@@ -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
+10 -2
View File
@@ -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