feat: migrate runtime backend to node server

This commit is contained in:
victo
2026-04-08 16:41:29 +08:00
parent 9d2fc9e4b8
commit a83841ff2d
70 changed files with 8239 additions and 1561 deletions

View File

@@ -12,7 +12,12 @@ import { normalizeNpcPersistentState } from '../data/npcInteractions';
import { normalizeQuestLogEntries } from '../data/questFlow';
import { normalizeGameRuntimeStats } from '../data/runtimeStats';
import { ensureSceneEncounterPreview, TREASURE_ENCOUNTERS_ENABLED } from '../data/sceneEncounterPreviews';
import {clearSavedSnapshot, readSavedSnapshot, writeSavedSnapshot} from '../persistence/gameSaveStorage';
import type { SavedGameSnapshot } from '../persistence/gameSaveStorage';
import {
deleteSaveSnapshot,
getSaveSnapshot,
putSaveSnapshot,
} from '../services/storageService';
import {
applyStoryEngineMigration,
buildSaveMigrationManifest,
@@ -169,9 +174,24 @@ export function useGamePersistence({
resetStoryState: () => void;
}) {
const [hasSavedGame, setHasSavedGame] = useState(false);
const [savedSnapshot, setSavedSnapshot] = useState<SavedGameSnapshot | null>(null);
useEffect(() => {
setHasSavedGame(Boolean(readSavedSnapshot()));
let isActive = true;
void getSaveSnapshot()
.then((snapshot) => {
if (!isActive) return;
setSavedSnapshot(snapshot);
setHasSavedGame(Boolean(snapshot));
})
.catch((error) => {
console.warn('[useGamePersistence] failed to load remote snapshot', error);
});
return () => {
isActive = false;
};
}, []);
useEffect(() => {
@@ -180,21 +200,24 @@ export function useGamePersistence({
if (!canPersist) return;
const timeoutId = window.setTimeout(() => {
const didSave = writeSavedSnapshot({
void putSaveSnapshot({
gameState,
bottomTab,
currentStory,
});
if (didSave) {
setHasSavedGame(true);
}
})
.then((snapshot) => {
setSavedSnapshot(snapshot);
setHasSavedGame(true);
})
.catch((error) => {
console.warn('[useGamePersistence] failed to autosave remote snapshot', error);
});
}, AUTO_SAVE_DELAY_MS);
return () => window.clearTimeout(timeoutId);
}, [bottomTab, currentStory, gameState, isLoading]);
const saveCurrentGame = useCallback((override?: {
const saveCurrentGame = useCallback(async (override?: {
gameState?: GameState;
bottomTab?: BottomTab;
currentStory?: StoryMoment | null;
@@ -207,28 +230,40 @@ export function useGamePersistence({
return false;
}
const didSave = writeSavedSnapshot({
gameState: nextGameState,
bottomTab: nextBottomTab,
currentStory: nextStory,
});
if (didSave) {
try {
const snapshot = await putSaveSnapshot({
gameState: nextGameState,
bottomTab: nextBottomTab,
currentStory: nextStory,
});
setSavedSnapshot(snapshot);
setHasSavedGame(true);
return true;
} catch (error) {
console.warn('[useGamePersistence] failed to save remote snapshot', error);
return false;
}
return didSave;
}, [bottomTab, currentStory, gameState]);
const clearSavedGame = useCallback(() => {
clearSavedSnapshot();
const clearSavedGame = useCallback(async () => {
try {
await deleteSaveSnapshot();
} catch (error) {
console.warn('[useGamePersistence] failed to delete remote snapshot', error);
}
setSavedSnapshot(null);
setHasSavedGame(false);
}, []);
const continueSavedGame = useCallback(() => {
const snapshot = readSavedSnapshot();
const continueSavedGame = useCallback(async () => {
const snapshot = savedSnapshot ?? await getSaveSnapshot().catch((error) => {
console.warn('[useGamePersistence] failed to refetch remote snapshot', error);
return null;
});
if (!snapshot) {
clearSavedGame();
setSavedSnapshot(null);
setHasSavedGame(false);
return false;
}
@@ -236,9 +271,10 @@ export function useGamePersistence({
setGameState(normalizeSavedGameState(snapshot.gameState));
setBottomTab(snapshot.bottomTab ?? 'adventure');
hydrateStoryState(normalizeSavedStory(snapshot.currentStory));
setSavedSnapshot(snapshot);
setHasSavedGame(true);
return true;
}, [clearSavedGame, hydrateStoryState, resetStoryState, setBottomTab, setGameState]);
}, [hydrateStoryState, resetStoryState, savedSnapshot, setBottomTab, setGameState]);
return {
hasSavedGame,