feat: add schema version system with outdated save detection

Introduces a schema version field to GameState. Saves without the
current schema version are flagged on load (showing a modal prompting
reset or proceed), and cloud saves from outdated clients are rejected
server-side. Removes all backfill code now that outdated saves are
handled via the reset flow instead.

New POST /game/reset endpoint creates a fresh save for players who
choose to reset. Save version and current schema version are displayed
in the sidebar below the app version.
This commit is contained in:
2026-03-07 17:34:26 -08:00
committed by Naomi Carrigan
parent 4ed3ccc69c
commit 5d2d71983a
11 changed files with 205 additions and 276 deletions
+6 -1
View File
@@ -10,7 +10,7 @@ interface FloatText {
}
export const ClickArea = (): React.JSX.Element => {
const { state, handleClick, formatNumber } = useGame();
const { state, handleClick, formatNumber, saveSchemaVersion, currentSchemaVersion } = useGame();
const [floats, setFloats] = useState<FloatText[]>([]);
const nextIdRef = useRef(0);
@@ -41,6 +41,11 @@ export const ClickArea = (): React.JSX.Element => {
<section className="click-area">
<h1 className="game-title">Elysium</h1>
<p className="game-version">v{__WEB_VERSION__}</p>
{currentSchemaVersion > 0 && (
<p className="game-schema-version">
Save: v{saveSchemaVersion} / Latest: v{currentSchemaVersion}
</p>
)}
<h2>Guild Hall</h2>
<div className="click-button-wrapper">
<button
+6 -1
View File
@@ -13,6 +13,7 @@ import { CodexToast } from "./CodexToast.js";
import { EditProfileModal } from "./EditProfileModal.js";
import { EquipmentPanel } from "./EquipmentPanel.js";
import { OfflineModal } from "./OfflineModal.js";
import { OutdatedSchemaModal } from "./OutdatedSchemaModal.js";
import { PrestigePanel } from "./PrestigePanel.js";
import { ApotheosisPanel } from "./ApotheosisPanel.js";
import { TranscendencePanel } from "./TranscendencePanel.js";
@@ -52,9 +53,10 @@ const BASE_TABS: { id: Tab; label: string }[] = [
];
export const GameLayout = (): React.JSX.Element => {
const { state, isLoading, error, battleResult, dismissBattle, lastSavedAt, isSyncing, forceSync, newCodexEntryIds, newStoryChapterIds, loginBonus, dismissLoginBonus } = useGame();
const { state, isLoading, error, battleResult, dismissBattle, lastSavedAt, isSyncing, forceSync, newCodexEntryIds, newStoryChapterIds, loginBonus, dismissLoginBonus, schemaOutdated } = useGame();
const [activeTab, setActiveTab] = useState<Tab>("adventurers");
const [editingProfile, setEditingProfile] = useState(false);
const [dismissedOutdatedWarning, setDismissedOutdatedWarning] = useState(false);
if (isLoading) {
return (
@@ -91,6 +93,9 @@ export const GameLayout = (): React.JSX.Element => {
onForceSync={forceSync}
/>
<OfflineModal />
{schemaOutdated && !dismissedOutdatedWarning && (
<OutdatedSchemaModal onDismiss={() => { setDismissedOutdatedWarning(true); }} />
)}
<AchievementToast />
<CodexToast />
<StoryToast />
@@ -0,0 +1,49 @@
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
interface OutdatedSchemaModalProps {
onDismiss: () => void;
}
export const OutdatedSchemaModal = ({ onDismiss }: OutdatedSchemaModalProps): React.JSX.Element => {
const { resetProgress } = useGame();
const [isResetting, setIsResetting] = useState(false);
const handleReset = async (): Promise<void> => {
setIsResetting(true);
await resetProgress();
setIsResetting(false);
};
return (
<div className="modal-overlay">
<div className="modal offline-modal">
<h2> Outdated Save Data</h2>
<p>
Your save data is from an older version of Elysium and may cause bugs or unexpected
behaviour. Cloud saves are <strong>disabled</strong> until you reset your progress.
</p>
<p>
Resetting will start you fresh all progress will be lost.
</p>
<div className="outdated-modal-actions">
<button
className="outdated-modal-reset-button"
onClick={() => { void handleReset(); }}
disabled={isResetting}
type="button"
>
{isResetting ? "Resetting…" : "Reset Progress"}
</button>
<button
className="modal-close-button"
onClick={onDismiss}
type="button"
>
Proceed with Bugs
</button>
</div>
</div>
</div>
);
};