feat: migrate runtime backend to node server
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
generateCharacterPanelChatSuggestions,
|
||||
generateCharacterPanelChatSummary,
|
||||
streamCharacterPanelChatReply,
|
||||
} from '../../services/ai';
|
||||
} from '../../services/aiService';
|
||||
import type {StoryGenerationContext} from '../../services/aiTypes';
|
||||
import type {
|
||||
Character,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
resolveSceneEncounterPreview,
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
|
||||
import { generateNextStep } from '../../services/ai';
|
||||
import { generateNextStep } from '../../services/aiService';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { appendStoryEngineCarrierMemory } from '../../services/storyEngine/echoMemory';
|
||||
import { createHistoryMoment } from '../../services/storyHistory';
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
resolveSceneEncounterPreview,
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
|
||||
import { generateNextStep, streamNpcChatDialogue } from '../../services/ai';
|
||||
import { generateNextStep, streamNpcChatDialogue } from '../../services/aiService';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { generateQuestForNpcEncounter } from '../../services/questDirector';
|
||||
import {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
removeInventoryItem,
|
||||
syncNpcTradeInventory,
|
||||
} from '../../data/npcInteractions';
|
||||
import { streamNpcChatDialogue, streamNpcRecruitDialogue } from '../../services/ai';
|
||||
import { streamNpcChatDialogue, streamNpcRecruitDialogue } from '../../services/aiService';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import {
|
||||
appendStoryEngineCarrierMemory,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import { getWorldCampScenePreset } from '../../data/scenePresets';
|
||||
import { sortStoryOptionsByPriority } from '../../data/stateFunctions';
|
||||
import { generateNextStep } from '../../services/ai';
|
||||
import { generateNextStep } from '../../services/aiService';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { createHistoryMoment } from '../../services/storyHistory';
|
||||
import type {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,13 +1,64 @@
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {clampVolume, readSavedSettings, writeSavedSettings} from '../persistence/gameSettingsStorage';
|
||||
import {
|
||||
clampVolume,
|
||||
DEFAULT_MUSIC_VOLUME,
|
||||
} from '../persistence/gameSettingsStorage';
|
||||
import { getSettings, putSettings } from '../services/storageService';
|
||||
|
||||
export function useGameSettings() {
|
||||
const [musicVolume, setMusicVolumeState] = useState(() => readSavedSettings().musicVolume);
|
||||
const [musicVolume, setMusicVolumeState] = useState(DEFAULT_MUSIC_VOLUME);
|
||||
const [hasHydratedSettings, setHasHydratedSettings] = useState(false);
|
||||
const lastSyncedVolumeRef = useRef(DEFAULT_MUSIC_VOLUME);
|
||||
|
||||
useEffect(() => {
|
||||
writeSavedSettings({musicVolume});
|
||||
}, [musicVolume]);
|
||||
let isActive = true;
|
||||
|
||||
void getSettings()
|
||||
.then((settings) => {
|
||||
if (!isActive) return;
|
||||
const nextVolume = clampVolume(settings.musicVolume);
|
||||
lastSyncedVolumeRef.current = nextVolume;
|
||||
setMusicVolumeState(nextVolume);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[useGameSettings] failed to load remote settings', error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (isActive) {
|
||||
setHasHydratedSettings(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasHydratedSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastSyncedVolumeRef.current === musicVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
|
||||
void putSettings({musicVolume})
|
||||
.then((settings) => {
|
||||
if (!isActive) return;
|
||||
lastSyncedVolumeRef.current = clampVolume(settings.musicVolume);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[useGameSettings] failed to persist remote settings', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [hasHydratedSettings, musicVolume]);
|
||||
|
||||
const setMusicVolume = useCallback((value: number) => {
|
||||
setMusicVolumeState(clampVolume(value));
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
sortStoryOptionsByPriority,
|
||||
} from '../data/stateFunctions';
|
||||
import { applyStoryReasoningRecovery } from '../data/storyRecovery';
|
||||
import { generateInitialStory, generateNextStep } from '../services/ai';
|
||||
import { generateInitialStory, generateNextStep } from '../services/aiService';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
|
||||
Reference in New Issue
Block a user