Skip to content

Commit 1e02384

Browse files
committed
Protect daily progress from stale challenge tabs
2 parents 80a1123 + 84ae44c commit 1e02384

3 files changed

Lines changed: 67 additions & 17 deletions

File tree

index.html

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,19 +191,26 @@ <h2 id="journal-title">SIGNAL LOG</h2>
191191
}));
192192
let progressStorage = null;
193193
try { progressStorage = window.localStorage; } catch {}
194+
const loadedProgress = LittleAndroidProgress.loadProgress(progressStorage);
194195
const savedProgress = LittleAndroidProgress.scopeProgressToChallenge(
195-
LittleAndroidProgress.loadProgress(progressStorage),
196+
loadedProgress,
196197
dailyChallenge.dateKey,
197198
);
199+
if (loadedProgress.challengeDate !== savedProgress.challengeDate) {
200+
LittleAndroidProgress.saveProgress(progressStorage, savedProgress);
201+
}
198202
const feedback = LittleAndroidFeedback.createFeedbackController(window, progressStorage);
199203
const metrics = LittleAndroidEngagement.createLocalMetrics(progressStorage);
200204
metrics.increment('sessions');
205+
let progressGeneration = savedProgress.generation;
201206
for (const fragment of signalFragments) {
202207
fragment.discovered = savedProgress.discoveredFragments.includes(fragment.id);
203208
fragment.collected = savedProgress.collectedFragments.includes(fragment.id);
204209
}
205210
const discoveredNPCs = new Set(savedProgress.discoveredNPCs);
206-
const quest = { complete: savedProgress.complete };
211+
const quest = {
212+
complete: savedProgress.complete || signalFragments.every(fragment => fragment.collected),
213+
};
207214

208215
function isWalkable(tx,ty){
209216
if(tx<0||ty<0||tx>=MW||ty>=MH)return false;
@@ -998,6 +1005,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
9981005
return {
9991006
version: LittleAndroidProgress.SAVE_VERSION,
10001007
challengeDate: dailyChallenge.dateKey,
1008+
generation: progressGeneration,
10011009
discoveredFragments: signalFragments.filter(fragment => fragment.discovered).map(fragment => fragment.id),
10021010
collectedFragments: signalFragments.filter(fragment => fragment.collected).map(fragment => fragment.id),
10031011
discoveredNPCs: [...discoveredNPCs],
@@ -1006,12 +1014,26 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10061014
}
10071015

10081016
function persistProgress() {
1009-
const latest = LittleAndroidProgress.scopeProgressToChallenge(
1010-
LittleAndroidProgress.loadProgress(progressStorage),
1011-
dailyChallenge.dateKey,
1017+
const stored = LittleAndroidProgress.loadProgress(progressStorage);
1018+
if (stored.challengeDate && stored.challengeDate !== dailyChallenge.dateKey) return false;
1019+
const latest = LittleAndroidProgress.scopeProgressToChallenge(stored, dailyChallenge.dateKey);
1020+
const merged = LittleAndroidProgress.mergeProgress(
1021+
latest,
1022+
currentProgress(),
1023+
signalFragments.map(fragment => fragment.id),
10121024
);
1013-
const merged = LittleAndroidProgress.mergeProgress(latest, currentProgress());
1025+
progressGeneration = merged.generation;
1026+
for (const fragment of signalFragments) {
1027+
fragment.discovered = merged.discoveredFragments.includes(fragment.id);
1028+
fragment.collected = merged.collectedFragments.includes(fragment.id);
1029+
}
1030+
discoveredNPCs.clear();
1031+
for (const identity of merged.discoveredNPCs) discoveredNPCs.add(identity);
1032+
quest.complete = merged.complete;
1033+
document.body.classList.toggle('world-restored', quest.complete);
1034+
updateQuestUI();
10141035
LittleAndroidProgress.saveProgress(progressStorage, merged);
1036+
return merged;
10151037
}
10161038

10171039
function rememberNPC(npc) {
@@ -1071,7 +1093,9 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10711093
discoveredNPCs.clear();
10721094
quest.complete = false;
10731095
document.body.classList.remove('world-restored');
1074-
LittleAndroidProgress.clearProgress(progressStorage);
1096+
const storedGeneration = LittleAndroidProgress.loadProgress(progressStorage).generation;
1097+
progressGeneration = Math.max(progressGeneration, storedGeneration) + 1;
1098+
LittleAndroidProgress.saveProgress(progressStorage, currentProgress());
10751099
updateQuestUI();
10761100
updateJournal();
10771101
showStatus('SIGNAL SEARCH RESTARTED', 2);
@@ -1085,8 +1109,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10851109
confirmReset(message) :
10861110
typeof window.confirm === 'function' && window.confirm(message);
10871111
if (!confirmed) return false;
1088-
resetQuest();
1089-
return true;
1112+
return resetQuest();
10901113
}
10911114

10921115
function performAction() {

src/progress.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
return {
1313
version: SAVE_VERSION,
1414
challengeDate: '',
15+
generation: 0,
1516
discoveredFragments: [],
1617
collectedFragments: [],
1718
discoveredNPCs: [],
@@ -32,6 +33,7 @@
3233
return {
3334
version: SAVE_VERSION,
3435
challengeDate: typeof parsed.challengeDate === 'string' ? parsed.challengeDate : '',
36+
generation: Number.isSafeInteger(parsed.generation) && parsed.generation >= 0 ? parsed.generation : 0,
3537
discoveredFragments: normalizeStringList(parsed.discoveredFragments),
3638
collectedFragments: normalizeStringList(parsed.collectedFragments),
3739
discoveredNPCs: normalizeStringList(parsed.discoveredNPCs),
@@ -57,29 +59,38 @@
5759
return {
5860
...defaultProgress(),
5961
challengeDate,
62+
generation: normalized.generation + 1,
6063
discoveredNPCs: normalized.discoveredNPCs,
6164
};
6265
}
6366

64-
function mergeProgress(base, incoming) {
67+
function mergeProgress(base, incoming, requiredFragmentIds = []) {
6568
const current = parseProgress(base);
6669
const next = parseProgress(incoming);
70+
if (current.generation !== next.generation) {
71+
return current.generation > next.generation ? current : next;
72+
}
73+
const collectedFragments = normalizeStringList([
74+
...current.collectedFragments,
75+
...next.collectedFragments,
76+
]);
6777
return {
6878
version: SAVE_VERSION,
6979
challengeDate: next.challengeDate || current.challengeDate,
80+
generation: current.generation,
7081
discoveredFragments: normalizeStringList([
7182
...current.discoveredFragments,
7283
...next.discoveredFragments,
7384
]),
74-
collectedFragments: normalizeStringList([
75-
...current.collectedFragments,
76-
...next.collectedFragments,
77-
]),
85+
collectedFragments,
7886
discoveredNPCs: normalizeStringList([
7987
...current.discoveredNPCs,
8088
...next.discoveredNPCs,
8189
]),
82-
complete: current.complete || next.complete,
90+
complete: current.complete || next.complete || (
91+
requiredFragmentIds.length > 0 &&
92+
requiredFragmentIds.every(id => collectedFragments.includes(id))
93+
),
8394
};
8495
}
8596

tests/progress.test.cjs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ test('progress normalizes lists and ignores unknown fields', () => {
4141
assert.deepEqual(parsed, {
4242
version: SAVE_VERSION,
4343
challengeDate: '2026-07-16',
44+
generation: 0,
4445
discoveredFragments: ['a'],
4546
collectedFragments: ['a'],
4647
discoveredNPCs: ['scout'],
@@ -79,11 +80,14 @@ test('daily progress restores only for the matching challenge date', () => {
7980
complete: true,
8081
};
8182
assert.deepEqual(scopeProgressToChallenge(completed, '2026-07-15'), completed);
82-
assert.deepEqual(scopeProgressToChallenge(completed, '2026-07-16'), {
83+
const rolled = scopeProgressToChallenge(completed, '2026-07-16');
84+
assert.deepEqual(rolled, {
8385
...defaultProgress(),
8486
challengeDate: '2026-07-16',
87+
generation: 1,
8588
discoveredNPCs: ['OVERSEER'],
8689
});
90+
assert.deepEqual(mergeProgress(rolled, completed, ['field-coil']), rolled);
8791
});
8892

8993
test('progress round-trips and clears through storage', () => {
@@ -106,10 +110,22 @@ test('progress merges monotonically across stale tabs', () => {
106110
...defaultProgress(),
107111
discoveredNPCs: ['OVERSEER'],
108112
};
109-
assert.deepEqual(mergeProgress(firstTab, staleTab), {
113+
assert.deepEqual(mergeProgress(firstTab, staleTab, ['field-coil']), {
110114
...defaultProgress(),
111115
discoveredFragments: ['field-coil'],
112116
collectedFragments: ['field-coil'],
113117
discoveredNPCs: ['OVERSEER'],
118+
complete: true,
114119
});
115120
});
121+
122+
test('a newer reset generation defeats stale tab writes', () => {
123+
const reset = { ...defaultProgress(), generation: 2 };
124+
const stale = {
125+
...defaultProgress(),
126+
generation: 1,
127+
collectedFragments: ['field-coil'],
128+
complete: true,
129+
};
130+
assert.deepEqual(mergeProgress(reset, stale, ['field-coil']), reset);
131+
});

0 commit comments

Comments
 (0)