Skip to content

Commit 9d1a05f

Browse files
Add backup storage key and enhance state management with fallback logic
1 parent 3daa509 commit 9d1a05f

4 files changed

Lines changed: 104 additions & 22 deletions

File tree

src/game/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from "./types.js";
1313

1414
export const STORAGE_KEY = "skills17.frontend.loc-clicker.state.v1";
15+
export const STORAGE_BACKUP_KEY = `${STORAGE_KEY}.backup`;
1516

1617
export const DEVELOPER_LEVELS: Record<DeveloperLevel, DeveloperConfig> = {
1718
junior: {

src/game/persistence.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
1-
import { STORAGE_KEY } from "./constants.js";
1+
import { STORAGE_BACKUP_KEY, STORAGE_KEY } from "./constants.js";
22
import type { GameState, StorageLike } from "./types.js";
33
import { createInitialState, normalizeState } from "./engine.js";
44

55
export function saveState(
66
state: GameState,
77
storage: StorageLike = window.localStorage,
88
): void {
9-
storage.setItem(STORAGE_KEY, JSON.stringify(state));
9+
const serialized = JSON.stringify(state);
10+
storage.setItem(STORAGE_KEY, serialized);
11+
storage.setItem(STORAGE_BACKUP_KEY, serialized);
1012
}
1113

1214
export function loadState(
1315
storage: StorageLike = window.localStorage,
1416
nowMs = Date.now(),
1517
): GameState {
16-
const raw = storage.getItem(STORAGE_KEY);
17-
if (!raw) {
18+
const candidates = [
19+
readStoredState(storage, STORAGE_KEY, nowMs),
20+
readStoredState(storage, STORAGE_BACKUP_KEY, nowMs),
21+
].filter((value): value is GameState => value !== null);
22+
23+
if (candidates.length === 0) {
1824
return createInitialState(nowMs);
1925
}
2026

27+
return candidates.reduce((latest, candidate) =>
28+
candidate.lastTickAt > latest.lastTickAt ? candidate : latest,
29+
);
30+
}
31+
32+
function readStoredState(
33+
storage: StorageLike,
34+
key: string,
35+
nowMs: number,
36+
): GameState | null {
37+
const raw = storage.getItem(key);
38+
if (!raw) {
39+
return null;
40+
}
41+
2142
try {
2243
return normalizeState(JSON.parse(raw), nowMs);
2344
} catch {
24-
return createInitialState(nowMs);
45+
return null;
2546
}
2647
}
2748

src/lib/css/styles.css

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,34 @@ input {
14891489
flex: 1 1 auto;
14901490
}
14911491

1492+
.game-shell[data-mobile-screen="shop"] .tile-shop,
1493+
.game-shell[data-mobile-screen="shop"] .tile-owned {
1494+
flex: 0 0 auto;
1495+
overflow: visible;
1496+
padding-right: 0;
1497+
}
1498+
1499+
.game-shell[data-mobile-screen="shop"] .tile-shop {
1500+
order: 1;
1501+
}
1502+
1503+
.game-shell[data-mobile-screen="shop"] .tile-owned {
1504+
order: 2;
1505+
}
1506+
1507+
.game-shell[data-mobile-screen="release"] .tile-utility {
1508+
flex: 0 0 auto;
1509+
overflow: visible;
1510+
padding-right: 0;
1511+
}
1512+
1513+
.game-shell[data-mobile-screen="release"] [data-ui="prestige-upgrade-list"],
1514+
.game-shell[data-mobile-screen="release"] [data-ui="achievement-list"] {
1515+
max-height: none;
1516+
overflow: visible;
1517+
padding-right: 0;
1518+
}
1519+
14921520
.game-shell[data-mobile-screen="ops"] .tile-bugs,
14931521
.game-shell[data-mobile-screen="ops"] .tile-events,
14941522
.game-shell[data-mobile-screen="play"] .tile-hero,
@@ -1503,7 +1531,7 @@ input {
15031531
height: clamp(220px, calc(100svh - 430px), 284px);
15041532
flex-direction: column;
15051533
justify-content: space-between;
1506-
gap: 6px;
1534+
gap: 4px;
15071535
overflow: hidden;
15081536
}
15091537

@@ -1527,15 +1555,15 @@ input {
15271555
}
15281556

15291557
.game-shell[data-mobile-screen="play"] .tile-hero .company-progress {
1530-
margin-top: 4px;
1558+
margin-top: 2px;
15311559
}
15321560

15331561
.game-shell[data-mobile-screen="play"] .tile-hero .click-primary-wrap {
15341562
margin: 0;
15351563
}
15361564

15371565
.game-shell[data-mobile-screen="play"] .tile-hero .loc-visual-wrap {
1538-
margin-top: 2px;
1566+
margin-top: 0;
15391567
}
15401568

15411569
.game-shell[data-mobile-screen="play"] .tile-hero .loc-visual {
@@ -1554,7 +1582,7 @@ input {
15541582
}
15551583

15561584
.game-shell[data-mobile-screen="play"] .tile-hero .combo-meter {
1557-
margin-top: 2px;
1585+
margin-top: 0;
15581586
}
15591587

15601588
.game-shell[data-mobile-screen="play"] .tile-hero .team-visual {

test/persistence.test.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { expect, test } from "vitest";
22

33
import { createInitialState } from "../src/game/engine.js";
4-
import { STORAGE_KEY } from "../src/game/constants.js";
4+
import { STORAGE_BACKUP_KEY, STORAGE_KEY } from "../src/game/constants.js";
55
import {
66
decodeShareState,
77
encodeShareState,
88
loadState,
99
saveState,
1010
} from "../src/game/persistence.js";
1111

12-
function createStorage(seedValue: string | null = null) {
13-
let currentValue = seedValue;
12+
function createStorage(seedValue: string | null = null, backupValue: string | null = null) {
13+
const values = new Map<string, string | null>([
14+
[STORAGE_KEY, seedValue],
15+
[STORAGE_BACKUP_KEY, backupValue],
16+
]);
17+
1418
return {
15-
getItem() {
16-
return currentValue;
19+
getItem(key: string) {
20+
return values.get(key) ?? null;
1721
},
18-
setItem(_key: string, value: string) {
19-
currentValue = value;
22+
setItem(key: string, value: string) {
23+
values.set(key, value);
2024
},
2125
};
2226
}
@@ -43,24 +47,22 @@ test("loadState falls back to a fresh state on bad JSON", () => {
4347
});
4448

4549
test("saveState writes JSON to storage under the storage key", () => {
46-
let writtenKey = "";
47-
let writtenValue = "";
50+
const written = new Map<string, string>();
4851
const storage = {
4952
getItem() {
5053
return null;
5154
},
5255
setItem(key: string, value: string) {
53-
writtenKey = key;
54-
writtenValue = value;
56+
written.set(key, value);
5557
},
5658
};
5759
const state = createInitialState(1_500);
5860
state.totalClicks = 4;
5961

6062
saveState(state, storage);
6163

62-
expect(writtenKey).toBe(STORAGE_KEY);
63-
expect(JSON.parse(writtenValue).totalClicks).toBe(4);
64+
expect(JSON.parse(written.get(STORAGE_KEY) ?? "").totalClicks).toBe(4);
65+
expect(JSON.parse(written.get(STORAGE_BACKUP_KEY) ?? "").totalClicks).toBe(4);
6466
});
6567

6668
test("loadState normalizes persisted values", () => {
@@ -90,6 +92,36 @@ test("loadState creates a fresh state when storage is empty", () => {
9092
expect(loaded.dollars).toBe(0);
9193
});
9294

95+
test("loadState falls back to backup snapshot when primary is corrupted", () => {
96+
const backup = JSON.stringify({
97+
totalClicks: 23,
98+
lastTickAt: 8_000,
99+
dollars: 12,
100+
});
101+
102+
const loaded = loadState(createStorage("{bad-json", backup), 9_000);
103+
104+
expect(loaded.totalClicks).toBe(23);
105+
expect(loaded.dollars).toBe(12);
106+
expect(loaded.lastTickAt).toBe(8_000);
107+
});
108+
109+
test("loadState prefers the newest valid snapshot", () => {
110+
const primary = JSON.stringify({
111+
totalClicks: 11,
112+
lastTickAt: 5_000,
113+
});
114+
const backup = JSON.stringify({
115+
totalClicks: 17,
116+
lastTickAt: 8_000,
117+
});
118+
119+
const loaded = loadState(createStorage(primary, backup), 9_000);
120+
121+
expect(loaded.totalClicks).toBe(17);
122+
expect(loaded.lastTickAt).toBe(8_000);
123+
});
124+
93125
test("encode and decode use browser base64 helpers when available", () => {
94126
const originalWindow = globalThis.window;
95127
const state = createInitialState(1_000);

0 commit comments

Comments
 (0)