/
Dgunamel
/
META_SCAN
Обзор
Документация
Войти
/
Dgunamel
/
META_SCAN
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
dev
src/lib/stats.ts
153 строки
6 KB
studio-agent
Squash 'studio-workflow-seed-71c2daea-aa89-42dd-b80e-3340378b7f39' into dev
29 июл 2026, 23:47
29 июл 2026, 23:47
2d16536
Код
Авторство
О чём код?
"use client"; import { useEffect, useState, useCallback } from "react"; import type { CalendarId } from "./calendars"; const STORAGE_KEY = "metascan:state:v1"; export interface Achievement { id: string; title: string; description: string; icon: string; unlocked: boolean; progress: number; goal: number; } export interface AppState { totalRuns: number; // всего прохождений 20 шагов conversionsByCalendar: Record<string, number>; // раз использовали каждый календарь moonChecks: number; spaceChecks: number; weatherChecks: number; factsRead: number; fastestRunMs: number | null; // мс lastRunMs: number | null; fastestRunCalendar: CalendarId | null; unlockedCalendars: CalendarId[]; achievements: Record<string, boolean>; voiceEnabled: boolean; themeMode: "standard" | "slav"; } const DEFAULT_STATE: AppState = { totalRuns: 0, conversionsByCalendar: {}, moonChecks: 0, spaceChecks: 0, weatherChecks: 0, factsRead: 0, fastestRunMs: null, lastRunMs: null, fastestRunCalendar: null, unlockedCalendars: ["met", "gregorian", "julian", "islamic", "hebrew", "buddhist", "persian", "chinese", "maya"], achievements: {}, voiceEnabled: false, themeMode: "standard", }; export function loadState(): AppState { if (typeof window === "undefined") return DEFAULT_STATE; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return DEFAULT_STATE; return { ...DEFAULT_STATE, ...JSON.parse(raw) }; } catch { return DEFAULT_STATE; } } export function saveState(state: AppState): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch {} } export const ACHIEVEMENT_DEFS: Array<{ id: string; title: string; description: string; icon: string; goal: number; type: string }> = [ { id: "novice", title: "Новичок", description: "Завершить любой анализ 1 раз", icon: "Sparkles", goal: 1, type: "runs" }, { id: "bureaucrat", title: "Бюрократ", description: "Пройти все 20 шагов 50 раз", icon: "Stamp", goal: 50, type: "runs" }, { id: "speedster", title: "Скоростной", description: "Пройти все шаги за 30 секунд", icon: "Zap", goal: 1, type: "speed" }, { id: "calendarian", title: "Календарист", description: "Сконвертировать в 5 разных календарей", icon: "CalendarRange", goal: 5, type: "calendars" }, { id: "guru", title: "Календарный гуру", description: "Сконвертировать во все 9 календарей", icon: "Crown", goal: 9, type: "calendars" }, { id: "lunatic", title: "Лунатик", description: "Узнать фазу Луны 5 раз", icon: "Moon", goal: 5, type: "moon" }, { id: "cosmonaut", title: "Космонавт", description: "Проверить космическую погоду 5 раз", icon: "Rocket", goal: 5, type: "space" }, { id: "collector", title: "Коллекционер", description: "Узнать дату во всех 9 календарях", icon: "Trophy", goal: 9, type: "calendars" }, { id: "secret-keeper", title: "Хранитель тайн", description: "Открыть все 4 секретных календаря", icon: "KeyRound", goal: 4, type: "secrets" }, ]; export interface AchievementUpdate { newlyUnlocked: string[]; state: AppState; } export function useAppState() { const [state, setState] = useState<AppState>(DEFAULT_STATE); const [hydrated, setHydrated] = useState(false); useEffect(() => { setState(loadState()); setHydrated(true); }, []); const update = useCallback((mutator: (s: AppState) => AppState) => { setState((prev) => { const next = mutator(prev); saveState(next); return next; }); }, []); return { state, update, hydrated }; } // Пересчёт достижений export function recomputeAchievements(state: AppState): { achievements: Record<string, boolean>; newlyUnlocked: string[] } { const ach: Record<string, boolean> = {}; const newly: string[] = []; for (const def of ACHIEVEMENT_DEFS) { let value = 0; if (def.type === "runs") value = state.totalRuns; else if (def.type === "moon") value = state.moonChecks; else if (def.type === "space") value = state.spaceChecks; else if (def.type === "calendars") value = Object.keys(state.conversionsByCalendar).filter((k) => state.conversionsByCalendar[k] > 0).length; else if (def.type === "speed") value = state.fastestRunMs !== null && state.fastestRunMs <= 30000 ? 1 : 0; else if (def.type === "secrets") value = state.unlockedCalendars.filter((c) => ["flat", "cats", "dogs", "elephants"].includes(c)).length; const reached = value >= def.goal; if (reached && !state.achievements[def.id]) { newly.push(def.id); } ach[def.id] = reached || !!state.achievements[def.id]; } return { achievements: ach, newlyUnlocked: newly }; } // Проверка разблокировки секретных календарей export function checkSecretUnlocks(totalRuns: number, current: CalendarId[]): { unlocked: CalendarId[]; newly: CalendarId[] } { const map: Record<string, number> = { flat: 10, cats: 25, dogs: 50, elephants: 100 }; const result: CalendarId[] = [...current]; const newly: CalendarId[] = []; for (const [k, v] of Object.entries(map)) { if (totalRuns >= v && !result.includes(k as CalendarId)) { result.push(k as CalendarId); newly.push(k as CalendarId); } } return { unlocked: result, newly }; } export function progressForAchievement(state: AppState, defId: string): number { const def = ACHIEVEMENT_DEFS.find((d) => d.id === defId); if (!def) return 0; let value = 0; if (def.type === "runs") value = state.totalRuns; else if (def.type === "moon") value = state.moonChecks; else if (def.type === "space") value = state.spaceChecks; else if (def.type === "calendars") value = Object.keys(state.conversionsByCalendar).filter((k) => state.conversionsByCalendar[k] > 0).length; else if (def.type === "speed") value = state.fastestRunMs !== null && state.fastestRunMs <= 30000 ? 1 : 0; else if (def.type === "secrets") value = state.unlockedCalendars.filter((c) => ["flat", "cats", "dogs", "elephants"].includes(c)).length; return Math.min(value, def.goal); }