feat: add production campaigns, gameplay labs, and review documentation

This commit is contained in:
cabinw
2026-09-13 12:27:10 +08:00
parent d1dffa9338
commit 4644e975b0
214 changed files with 98994 additions and 134 deletions
+54
View File
@@ -0,0 +1,54 @@
export const tutorialProgressKey = campaignId => `hex-trails-tutorial-v1-${campaignId}`;
function tutorialCount(count) {
return Number.isInteger(count) && count > 0 ? count : 0;
}
function normalizeProgress(value, count) {
const valid = index => Number.isInteger(index) && index >= 0 && index < count;
return {
current: valid(value?.current) ? value.current : 0,
completed: [...new Set(Array.isArray(value?.completed) ? value.completed.filter(valid) : [])],
};
}
export function readTutorialProgress(storage, campaignId, count) {
count = tutorialCount(count);
let value;
if (count) {
try { value = JSON.parse(storage?.getItem(tutorialProgressKey(campaignId)) ?? '{}'); } catch {}
}
return normalizeProgress(value, count);
}
export function writeTutorialProgress(storage, campaignId, count, progress) {
count = tutorialCount(count);
if (!count || !storage) return false;
try {
storage.setItem(tutorialProgressKey(campaignId), JSON.stringify(normalizeProgress(progress, count)));
return true;
} catch { return false; }
}
export function tutorialLaunch(search, count, progress, campaignProgress) {
count = tutorialCount(count);
if (!count) return null;
const params = new URLSearchParams(search);
const requested = params.get('tutorial');
if (/^\d+$/.test(requested ?? '')) {
const index = Number(requested) - 1;
if (Number.isInteger(index) && index >= 0 && index < count) return index;
}
// Existing level links keep their destination, including links to level 1.
if (params.has('level')) return null;
if ((Number.isInteger(campaignProgress?.current) && campaignProgress.current > 0)
|| (Array.isArray(campaignProgress?.completed) && campaignProgress.completed.length > 0)) return null;
const normalized = normalizeProgress(progress, count);
const completed = new Set(normalized.completed);
for (let offset = 0; offset < count; offset += 1) {
const index = (normalized.current + offset) % count;
if (!completed.has(index)) return index;
}
return null;
}