fuzzyworld

dm / commit

feat: version mismatch modal with adventure log export

f862a5cfc1af48d09005849c5b1ec42a66346d61 · Pavel Safronov · 2026-05-27 21:51:54 -0700 · parents bfd83c711a03

feat: version mismatch modal with adventure log export

When a saved session's version doesn't match SAVE_VERSION, show a
modal instead of silently discarding. User can download their adventure
as a Markdown file (character summary, world notes, full conversation
log) before the session is cleared, or skip straight to Start fresh.
Modal is non-dismissible by backdrop click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 public/index.html | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 108 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index 7f28f9f..963dc01 100644
--- a/public/index.html
+++ b/public/index.html
@@ -955,6 +955,43 @@ body {
   margin-bottom: 24px;
 }
 
+/* ── VERSION MISMATCH MODAL ─────────────────────── */
+
+#version-modal {
+  display: none;
+  position: fixed;
+  inset: 0;
+  z-index: 500;
+  background: rgba(0,0,0,0.85);
+  align-items: center;
+  justify-content: center;
+}
+#version-modal.open { display: flex; }
+.version-inner {
+  background: linear-gradient(135deg, #0e0e1a, #141420);
+  border: 2px solid #d97706;
+  border-radius: 10px;
+  padding: 32px;
+  width: 480px;
+  max-width: 95vw;
+  box-shadow: 0 0 60px rgba(217,119,6,0.2), 0 20px 60px rgba(0,0,0,0.8);
+}
+.version-title {
+  font-family: 'Cinzel', serif;
+  font-size: 1rem;
+  font-weight: 600;
+  color: #f59e0b;
+  margin-bottom: 12px;
+  letter-spacing: 0.06em;
+}
+.version-body {
+  font-size: 0.9rem;
+  color: var(--text-dim);
+  line-height: 1.6;
+  margin-bottom: 24px;
+}
+.version-actions { display: flex; gap: 10px; flex-wrap: wrap; }
+
 /* ── REPORT MODAL ───────────────────────────────── */
 
 #report-modal {
@@ -1194,6 +1231,21 @@ body {
   </div>
 </div>
 
+<!-- Version mismatch modal -->
+<div id="version-modal">
+  <div class="version-inner">
+    <div class="version-title">📜 Game Format Updated</div>
+    <p class="version-body">
+      The app has been updated and your saved adventure uses an older format that can no longer be loaded.
+      You can download your adventure log as a Markdown file before it's cleared, or start fresh right away.
+    </p>
+    <div class="version-actions">
+      <button class="wn-save-btn" style="width:auto;padding:10px 20px" id="version-download-btn">⬇ Download adventure log</button>
+      <button class="share-btn share-btn-secondary" id="version-discard-btn">Start fresh</button>
+    </div>
+  </div>
+</div>
+
 <!-- Report Issue modal -->
 <div id="report-modal">
   <div class="report-inner">
@@ -1627,9 +1679,9 @@ async function tryResumeSession() {
   try { session = JSON.parse(sessionRaw); } catch { return false; }
 
   if (session.version !== SAVE_VERSION) {
-    console.log(`Save version mismatch (saved ${session.version ?? 'none'}, current ${SAVE_VERSION}) — discarding old session.`);
-    localStorage.removeItem('dm_session');
-    return false;
+    console.log(`Save version mismatch (saved ${session.version ?? 'none'}, current ${SAVE_VERSION}) — offering export.`);
+    showVersionMismatchModal(session);
+    return true; // returning true suppresses the creation screen until the modal is dismissed
   }
 
   gameState = { ...session.gameState, apiKey: currentApiKey };
@@ -1657,6 +1709,59 @@ async function tryResumeSession() {
   return true;
 }
 
+// ── VERSION MISMATCH ──────────────────────────────
+
+function generateAdventureMarkdown(session) {
+  const c = session.gameState?.character;
+  const lines = ['# Solo Dungeon Master — Adventure Log', ''];
+
+  if (c) {
+    lines.push(`**Character:** ${c.name} — Level ${c.level} ${c.race} ${c.class}`);
+    lines.push(`**HP:** ${c.hp}/${c.maxHp} · **XP:** ${c.xp} · **Gold:** ${c.gold} GP`);
+    if (c.inventory?.length) lines.push(`**Inventory:** ${c.inventory.join(', ')}`);
+  }
+  if (session.sessionCost) {
+    lines.push(`**Session cost:** $${session.sessionCost.toFixed(4)}`);
+  }
+  if (session.worldNotes) {
+    lines.push('', '## World Notes', '', session.worldNotes);
+  }
+
+  lines.push('', '---', '', '## Adventure');
+
+  for (const msg of (session.conversationHistory || [])) {
+    if (!msg.content || typeof msg.content !== 'string') continue;
+    const speaker = msg.role === 'user' ? `**${c?.name || 'You'}**` : '**DM**';
+    lines.push('', `${speaker}: ${msg.content}`);
+  }
+
+  return lines.join('\n');
+}
+
+function showVersionMismatchModal(session) {
+  const modal = document.getElementById('version-modal');
+  modal.classList.add('open');
+
+  document.getElementById('version-download-btn').onclick = () => {
+    const md = generateAdventureMarkdown(session);
+    const blob = new Blob([md], { type: 'text/markdown' });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    const charName = session.gameState?.character?.name?.replace(/\s+/g, '-') || 'adventure';
+    a.href = url;
+    a.download = `${charName}-adventure-log.md`;
+    a.click();
+    URL.revokeObjectURL(url);
+  };
+
+  const dismiss = () => {
+    modal.classList.remove('open');
+    localStorage.removeItem('dm_session');
+    document.getElementById('creation-screen').style.display = '';
+  };
+  document.getElementById('version-discard-btn').onclick = dismiss;
+}
+
 // ── CREATION SCREEN ───────────────────────────────────
 
 async function buildCreationScreen() {