dm / commit
feat: persist adventure session across page reloads
public/index.html | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index f9b87b0..a897a67 100644 --- a/public/index.html +++ b/public/index.html @@ -1267,6 +1267,55 @@ let gameState = null; let conversationHistory = []; let isBusy = false; +// ── SESSION PERSISTENCE ─────────────────────────────── + +function saveSession() { + if (!gameState) return; + const { apiKey: _, ...stateToSave } = gameState; + try { + localStorage.setItem('dm_session', JSON.stringify({ + gameState: stateToSave, + conversationHistory, + messagesHTML: document.getElementById('messages').innerHTML, + })); + } catch (e) { + console.warn('Session save failed (storage full?):', e); + } +} + +async function tryResumeSession() { + if (getSharedBlob()) return false; + const sessionRaw = localStorage.getItem('dm_session'); + if (!sessionRaw) return false; + const keyRaw = localStorage.getItem('dm_api_key'); + if (!keyRaw) return false; + + let session, apiKey; + try { + session = JSON.parse(sessionRaw); + apiKey = await decryptFromStorage(keyRaw); + if (!apiKey) return false; + } catch { return false; } + + gameState = { ...session.gameState, apiKey }; + conversationHistory = session.conversationHistory || []; + + document.getElementById('creation-screen').style.display = 'none'; + document.getElementById('game-screen').classList.add('visible'); + document.getElementById('messages').innerHTML = session.messagesHTML || ''; + document.getElementById('header-title').textContent = + `${gameState.character.name} — ${gameState.character.race} ${gameState.character.class}`; + + updateSidebar(); + buildDiceRoller(); + + const msgEl = document.getElementById('messages'); + msgEl.scrollTop = msgEl.scrollHeight; + + console.log(`▶ Resumed session — ${conversationHistory.length} messages, Lv ${gameState.character.level} ${gameState.character.name}`); + return true; +} + // ── CREATION SCREEN ─────────────────────────────────── async function buildCreationScreen() { @@ -1326,6 +1375,7 @@ async function buildCreationScreen() { document.getElementById('forget-key-btn').addEventListener('click', () => { localStorage.removeItem('dm_api_key'); + localStorage.removeItem('dm_session'); document.getElementById('api-key-input').value = ''; document.getElementById('key-saved-badge').style.display = 'none'; document.getElementById('forget-key-btn').style.display = 'none'; @@ -1848,6 +1898,7 @@ async function sendMessage(text, showAsPlayer = true) { finalizeDMMessage(dmEl, rawText); if (statUpdate) applyStatUpdate(statUpdate); conversationHistory.push({ role: 'assistant', content: rawText }); + saveSession(); console.log(`✅ Turn complete. History length: ${conversationHistory.length}`); } catch (err) { @@ -1920,12 +1971,18 @@ document.getElementById('new-game-btn').addEventListener('click', () => { conversationHistory = []; isBusy = false; document.querySelectorAll('.sel-card').forEach(c => c.classList.remove('selected')); + localStorage.removeItem('dm_session'); checkBeginReady(); }); // ── INIT ────────────────────────────────────────────── -buildCreationScreen().catch(console.error); +(async () => { + // Always build the creation screen so grids/listeners are ready for "New Game" + await buildCreationScreen(); + // Attempt resume — overwrites the game/creation screen state if a session exists + await tryResumeSession(); +})().catch(console.error); </script> </body> </html>