diff --git a/src/App.svelte b/src/App.svelte
index df5b9e7..402d02c 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -1,6 +1,9 @@
@@ -30,13 +48,13 @@
{:else if currentScene === 'StoryIntro'}
{:else if currentScene === 'LevelIntro'}
-
+
{:else if currentScene === 'LevelSelect'}
{:else if currentScene === 'Game'}
-
+
{:else if currentScene === 'GameOver'}
-
+
{:else if currentScene === 'Settings'}
{:else if currentScene === 'Guide'}
@@ -44,6 +62,17 @@
{/if}
{/key}
+
+ {#if showMigrationModal}
+
+
+
+
{getText('migration.title')}
+
{getText('migration.body')}
+
+
+
+ {/if}
diff --git a/src/lib/progress.js b/src/lib/progress.js
index 68ce494..3351abd 100644
--- a/src/lib/progress.js
+++ b/src/lib/progress.js
@@ -1,6 +1,7 @@
// Level progress persistence via localStorage
const STORAGE_KEY = 'nntv-progress';
+const MIGRATION_KEY = 'nntv-migration-v2';
const DEFAULT_PROGRESS = {
maxLevel: 1,
@@ -9,22 +10,63 @@ const DEFAULT_PROGRESS = {
levelBestMoves: {},
};
-export function getProgress() {
+/**
+ * Returns { progress, needsMigrationModal }.
+ * If saved data has legacy `lives` field (v1 shape), discards it and sets
+ * needsMigrationModal = true so App.svelte can show the one-time modal.
+ */
+export function loadProgress() {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (data) {
const parsed = JSON.parse(data);
+ // Detect v1 save shape (has `lives` field)
+ if (parsed.lives !== undefined) {
+ // Wipe legacy data, save clean v2 shape
+ const fresh = { ...DEFAULT_PROGRESS };
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(fresh));
+ } catch (_) {
+ // Storage full — proceed with in-memory defaults
+ }
+ return { progress: fresh, needsMigrationModal: true };
+ }
return {
- maxLevel: parsed.maxLevel || 1,
- completedLevels: parsed.completedLevels || [],
- levelStars: parsed.levelStars || {},
- levelBestMoves: parsed.levelBestMoves || {},
+ progress: {
+ maxLevel: parsed.maxLevel || 1,
+ completedLevels: parsed.completedLevels || [],
+ levelStars: parsed.levelStars || {},
+ levelBestMoves: parsed.levelBestMoves || {},
+ },
+ needsMigrationModal: false,
};
}
- } catch (e) {
+ } catch (_) {
// Corrupted data, reset
}
- return { ...DEFAULT_PROGRESS };
+ return { progress: { ...DEFAULT_PROGRESS }, needsMigrationModal: false };
+}
+
+/** Legacy compat shim — returns progress only. Use loadProgress() for migration detection. */
+export function getProgress() {
+ return loadProgress().progress;
+}
+
+/** Whether the v2 migration modal has been acknowledged. */
+export function isMigrationAcknowledged() {
+ try {
+ return localStorage.getItem(MIGRATION_KEY) === 'done';
+ } catch (_) {
+ return true;
+ }
+}
+
+export function acknowledgeMigration() {
+ try {
+ localStorage.setItem(MIGRATION_KEY, 'done');
+ } catch (_) {
+ // ignore
+ }
}
// Calculate star rating: 3★ = ≤par, 2★ = par+1 to par+3, 1★ = completed
diff --git a/src/scenes/GameOver.svelte b/src/scenes/GameOver.svelte
index 2ba2e2a..b75ed91 100644
--- a/src/scenes/GameOver.svelte
+++ b/src/scenes/GameOver.svelte
@@ -1,21 +1,24 @@
-
{title}
+
{title}
{desc}
-
@@ -29,7 +32,7 @@
}
h1 { font: bold 48px Arial, sans-serif; }
h1.accent { color: var(--text-accent); }
- h1.danger { color: var(--text-danger); }
+ h1.bittersweet { color: var(--text-danger); }
p {
font: var(--font-body);
color: var(--text-secondary);
diff --git a/src/scenes/LevelSelect.svelte b/src/scenes/LevelSelect.svelte
index 0cb0999..1aac461 100644
--- a/src/scenes/LevelSelect.svelte
+++ b/src/scenes/LevelSelect.svelte
@@ -10,7 +10,7 @@
function selectLevel(num) {
if (num <= progress.maxLevel) {
- navigate('LevelIntro', { level: num, lives: 3 });
+ navigate('LevelIntro', { level: num });
}
}
diff --git a/src/scenes/StoryIntro.svelte b/src/scenes/StoryIntro.svelte
index b1e9f92..d56a7c6 100644
--- a/src/scenes/StoryIntro.svelte
+++ b/src/scenes/StoryIntro.svelte
@@ -9,13 +9,13 @@
const el = document.querySelector('.story-scroll');
if (el) {
el.addEventListener('animationend', () => {
- if (!scrollDone) { scrollDone = true; navigate('LevelIntro', { level: 1, lives: 3 }); }
+ if (!scrollDone) { scrollDone = true; navigate('LevelIntro', { level: 1 }); }
});
}
});
function skip() {
- if (!scrollDone) { scrollDone = true; navigate('LevelIntro', { level: 1, lives: 3 }); }
+ if (!scrollDone) { scrollDone = true; navigate('LevelIntro', { level: 1 }); }
}