From b005226e2c2d31af7d6cdbbba4e693ffb3e3a13a Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 29 May 2026 20:26:59 +0300 Subject: [PATCH] =?UTF-8?q?feat(gamification):=20Phase=203=20=E2=80=94=203?= =?UTF-8?q?8=20new=20achievements=20+=20triggers=20+=20'exam'=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds achievement coverage for every feature shipped since the original seed: exam-prep (math9), textbooks, classroom/board, biochemistry, live-quiz, flashcards, hangman/crossword, pet, plus a new 'social' group for class & leaderboard wins and 'consistency' extensions (streak_100, goal_30, early_bird, night_owl). 74 achievements now (was 36), grouped into 7 sections: onboarding (3) → volume (8) → mastery (16) → consistency (7) → exam (9) → exploration (21) → social (10) A new top-level group 'exam' slots between consistency and exploration in the profile UI. What's wired in service.checkPhase3Achievements (called from checkAchievements): • streak_100 — extends the existing streak track • goal_30 — 30 days with daily_goals fully met (SUM check) • early_bird / night_owl — strftime('%H', xp_log.created_at) • exam_first / 25 / 100 — exam_attempts where is_correct=1 • exam_variant_clear / 5_variants — perfect mock-variant sessions • exam_topic_master — ≥10 attempts at ≥90% on a single subtopic • exam_mock_done / pass / perfect — exam_mock_sessions.score • tb_first_para — textbook_progress • fc_first_deck / 100_cards / 1000_cards — flashcard_reviews • bc_first_molecule / 5_challenges / 20_challenges — bio_user_* • game_win_5 / 25 — xp_log reason IN (hangman_win, crossword_win) • pet_streak_7 / 30 — users.pet_petting_streak • lq_first / 3_quizzes — live_answers grouped by session • cr_first_join / 5 / 25_lessons — classroom_attendance • class_5_members / 25 — teacher's biggest class • parent_link — parent_links presence • lb_top10 / lb_top1 — weekly XP rank among students What's deferred (catalog entry only, no trigger yet): • tb_chapter_done / tb_book_done / tb_3_books — need to parse textbook_progress.paragraphs_read JSON against textbook structure Every block is wrapped in its own try/catch so a missing table on a legacy install can't take down the whole achievement sweep. Verified end-to-end: admin user picked up 7 new unlocks on first checkAchievements call after seed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/controllers/gamification/_shared.js | 53 ++++- .../src/controllers/gamification/service.js | 203 ++++++++++++++++++ .../db/migrations/031_phase3_achievements.sql | 68 ++++++ frontend/profile.html | 1 + 4 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 backend/src/db/migrations/031_phase3_achievements.sql diff --git a/backend/src/controllers/gamification/_shared.js b/backend/src/controllers/gamification/_shared.js index 53e92e6..676e6bb 100644 --- a/backend/src/controllers/gamification/_shared.js +++ b/backend/src/controllers/gamification/_shared.js @@ -82,14 +82,65 @@ const ACHIEVEMENT_DEFS = [ { slug: 'rb_quest_first', title: 'Первый квест КК', icon: 'map', cat: 'redbook', group: 'exploration', track: 'red_book_quest', tier: 1, sort_order: 620, desc: 'Выполнить первый квест Красной книги' }, { slug: 'rb_quest_5', title: 'Квестмастер КК', icon: 'map-pin', cat: 'redbook', group: 'exploration', track: 'red_book_quest', tier: 2, sort_order: 630, desc: 'Выполнить 5 квестов Красной книги' }, { slug: 'rb_sighting', title: 'Наблюдатель', icon: 'eye', cat: 'redbook', group: 'exploration', track: 'red_book_obs', tier: 1, sort_order: 640, desc: 'Добавить первое наблюдение вида' }, + + // ── Phase 3 additions (migration 031) ────────────────────────── + // Consistency extensions + { slug: 'streak_100', title: 'Сто дней подряд', icon: 'flame', cat: 'streak', group: 'consistency', track: 'streak', tier: 4, sort_order: 440, desc: 'Невероятная серия — 100 дней активности подряд' }, + { slug: 'goal_30', title: 'Цель: 30 дней', icon: 'target', cat: 'streak', group: 'consistency', track: 'goal_streak', tier: 1, sort_order: 450, desc: '30 дней подряд выполнять цель дня' }, + { slug: 'early_bird', title: 'Ранняя пташка', icon: 'sunrise', cat: 'streak', group: 'consistency', track: 'time', tier: 1, sort_order: 460, desc: 'Активность до 9 утра' }, + { slug: 'night_owl', title: 'Ночная сова', icon: 'moon', cat: 'streak', group: 'consistency', track: 'time', tier: 2, sort_order: 470, desc: 'Активность после 23:00' }, + + // Exam-prep + { slug: 'exam_first', title: 'Старт экзамена', icon: 'flag', cat: 'exam', group: 'exam', track: 'exam_attempts', tier: 1, sort_order: 700, desc: 'Решена первая задача экзамена 9 класса' }, + { slug: 'exam_25_attempts', title: '25 задач экзамена', icon: 'list-checks', cat: 'exam', group: 'exam', track: 'exam_attempts', tier: 2, sort_order: 710, desc: '25 решённых попыток в банке экзамена' }, + { slug: 'exam_100_attempts', title: '100 задач экзамена', icon: 'check-square', cat: 'exam', group: 'exam', track: 'exam_attempts', tier: 3, sort_order: 720, desc: '100 решённых попыток в банке экзамена' }, + { slug: 'exam_variant_clear',title: 'Чистый вариант', icon: 'crown', cat: 'exam', group: 'exam', track: 'variants', tier: 1, sort_order: 730, desc: 'Все 10 задач варианта решены верно' }, + { slug: 'exam_5_variants', title: '5 вариантов', icon: 'layers', cat: 'exam', group: 'exam', track: 'variants', tier: 2, sort_order: 740, desc: '5 разных вариантов закрыто целиком' }, + { slug: 'exam_topic_master', title: 'Мастер темы', icon: 'target', cat: 'exam', group: 'exam', track: 'topic', tier: 1, sort_order: 750, desc: '≥90% точности на одной из подтем (≥10 попыток)' }, + { slug: 'exam_mock_done', title: 'Первый пробник', icon: 'timer', cat: 'exam', group: 'exam', track: 'mock', tier: 1, sort_order: 760, desc: 'Завершён первый пробный экзамен' }, + { slug: 'exam_mock_pass', title: 'Пробник сдан', icon: 'badge-check', cat: 'exam', group: 'exam', track: 'mock', tier: 2, sort_order: 770, desc: 'Балл ≥ 7 на пробном экзамене' }, + { slug: 'exam_mock_perfect', title: 'Пробник на 10', icon: 'trophy', cat: 'exam', group: 'exam', track: 'mock', tier: 3, sort_order: 780, desc: 'Идеальные 10 баллов на пробном экзамене' }, + + // Mastery extensions — textbooks + flashcards + { slug: 'tb_first_para', title: 'Открыл параграф', icon: 'book-marked', cat: 'theory', group: 'mastery', track: 'tb_progress', tier: 1, sort_order: 800, desc: 'Прочитан первый параграф учебника' }, + { slug: 'tb_chapter_done', title: 'Глава закрыта', icon: 'book-check', cat: 'theory', group: 'mastery', track: 'tb_progress', tier: 2, sort_order: 810, desc: 'Все параграфы одной главы пройдены' }, + { slug: 'tb_book_done', title: 'Учебник целиком', icon: 'book-up', cat: 'theory', group: 'mastery', track: 'tb_progress', tier: 3, sort_order: 820, desc: 'Все параграфы учебника пройдены' }, + { slug: 'tb_3_books', title: 'Полка из трёх', icon: 'library-big', cat: 'theory', group: 'mastery', track: 'tb_progress', tier: 4, sort_order: 830, desc: '3 учебника закрыто полностью' }, + { slug: 'fc_first_deck', title: 'Первая колода', icon: 'layers', cat: 'theory', group: 'mastery', track: 'flashcards', tier: 1, sort_order: 840, desc: 'Все карточки одной колоды разок повторены' }, + { slug: 'fc_100_cards', title: '100 карточек', icon: 'rotate-cw', cat: 'theory', group: 'mastery', track: 'flashcards', tier: 2, sort_order: 850, desc: '100 повторов карточек в сумме' }, + { slug: 'fc_1000_cards', title: '1000 карточек', icon: 'sparkles', cat: 'theory', group: 'mastery', track: 'flashcards', tier: 3, sort_order: 860, desc: '1000 повторов карточек в сумме' }, + + // Exploration extensions + { slug: 'bc_first_molecule', title: 'Первая молекула', icon: 'flask-round', cat: 'lab', group: 'exploration', track: 'biochem', tier: 1, sort_order: 870, desc: 'Собрана первая молекула в био-конструкторе' }, + { slug: 'bc_5_challenges', title: '5 био-задач', icon: 'beaker', cat: 'lab', group: 'exploration', track: 'biochem', tier: 2, sort_order: 880, desc: '5 биохимических задач решено' }, + { slug: 'bc_20_challenges', title: '20 био-задач', icon: 'atom', cat: 'lab', group: 'exploration', track: 'biochem', tier: 3, sort_order: 890, desc: '20 биохимических задач решено' }, + { slug: 'game_win_5', title: '5 побед в играх', icon: 'gamepad-2', cat: 'volume',group: 'exploration', track: 'minigames', tier: 1, sort_order: 900, desc: '5 побед в Виселице или Кроссворде суммарно' }, + { slug: 'game_win_25', title: '25 побед в играх',icon: 'gamepad', cat: 'volume',group: 'exploration', track: 'minigames', tier: 2, sort_order: 910, desc: '25 побед в Виселице или Кроссворде суммарно' }, + { slug: 'pet_streak_7', title: 'Заботливый', icon: 'heart', cat: 'volume',group: 'exploration', track: 'pet', tier: 1, sort_order: 920, desc: '7 дней подряд гладить питомца' }, + { slug: 'pet_streak_30', title: 'Лучший друг', icon: 'paw-print', cat: 'volume',group: 'exploration', track: 'pet', tier: 2, sort_order: 930, desc: '30 дней подряд гладить питомца' }, + + // Social + { slug: 'lb_top10', title: 'Топ-10 недели', icon: 'trending-up', cat: 'volume', group: 'social', track: 'leaderboard', tier: 1, sort_order: 1000, desc: 'Попасть в топ-10 лидерборда недели' }, + { slug: 'lb_top1', title: 'Первое место', icon: 'medal', cat: 'volume', group: 'social', track: 'leaderboard', tier: 2, sort_order: 1010, desc: '№1 в лидерборде недели' }, + { slug: 'lq_first', title: 'Первый live-quiz', icon: 'radio', cat: 'volume', group: 'social', track: 'live_quiz', tier: 1, sort_order: 1020, desc: 'Принять участие в первом live-квизе' }, + { slug: 'lq_3_quizzes', title: '3 live-квиза', icon: 'megaphone', cat: 'volume', group: 'social', track: 'live_quiz', tier: 2, sort_order: 1030, desc: 'Принять участие в 3 live-квизах' }, + { slug: 'cr_first_join', title: 'Первое подключение', icon: 'door-open', cat: 'volume', group: 'social', track: 'classroom', tier: 1, sort_order: 1040, desc: 'Первое подключение к уроку в classroom' }, + { slug: 'cr_5_lessons', title: '5 уроков', icon: 'presentation', cat: 'volume', group: 'social', track: 'classroom', tier: 2, sort_order: 1050, desc: 'Посещено 5 уроков classroom' }, + { slug: 'cr_25_lessons', title: '25 уроков', icon: 'school', cat: 'volume', group: 'social', track: 'classroom', tier: 3, sort_order: 1060, desc: 'Посещено 25 уроков classroom' }, + { slug: 'class_5_members', title: 'Учитель: 5 учеников', icon: 'users', cat: 'start', group: 'social', track: 'teacher', tier: 1, sort_order: 1070, desc: 'Учитель: класс из ≥5 учеников' }, + { slug: 'class_25_members', title: 'Учитель: 25 учеников',icon: 'users-2', cat: 'start', group: 'social', track: 'teacher', tier: 2, sort_order: 1080, desc: 'Учитель: класс из ≥25 учеников' }, + { slug: 'parent_link', title: 'Связан с родителем', icon: 'user-plus', cat: 'start', group: 'social', track: 'family', tier: 1, sort_order: 1090, desc: 'Связь с родителем установлена' }, ]; -/* Display metadata for top-level groups. Order = render order in UI. */ +/* Display metadata for top-level groups. Order = render order in UI. + 'exam' was added in migration 031 (Phase 3) — it slots between + consistency and exploration in display order. */ const ACHIEVEMENT_GROUPS = [ { slug: 'onboarding', title: 'Старт', icon: 'flag', sort_order: 100 }, { slug: 'volume', title: 'Объём', icon: 'bar-chart-2', sort_order: 200 }, { slug: 'mastery', title: 'Качество', icon: 'award', sort_order: 300 }, { slug: 'consistency', title: 'Постоянство', icon: 'flame', sort_order: 400 }, + { slug: 'exam', title: 'Экзамен 9', icon: 'clipboard-list', sort_order: 450 }, { slug: 'exploration', title: 'Исследование', icon: 'compass', sort_order: 500 }, { slug: 'social', title: 'Социальное', icon: 'users', sort_order: 600 }, ]; diff --git a/backend/src/controllers/gamification/service.js b/backend/src/controllers/gamification/service.js index afaab53..4b18372 100644 --- a/backend/src/controllers/gamification/service.js +++ b/backend/src/controllers/gamification/service.js @@ -191,6 +191,209 @@ function checkAchievements(userId) { if (assignCount >= 1) unlockAchievement(userId, 'assign_first'); if (assignCount >= 10) unlockAchievement(userId, 'assign_10'); } catch (e) { console.error('[achievements] assignment check:', e.message); } + + // Phase 3 — extended triggers from feature-specific tables + checkPhase3Achievements(userId, row); +} + +/* Phase 3 triggers — pulls counts from feature-specific tables. + Each block is wrapped in its own try/catch so a missing table on an + older install can't take down the whole sweep. */ +function checkPhase3Achievements(userId, userRow) { + // ── streak_100 + extended streaks ───────────────────────────── + const streak = userRow.streak_current || 0; + if (streak >= 100) unlockAchievement(userId, 'streak_100'); + + // ── goal_30 — 30 days of fully-met daily goals ──────────────── + try { + const n = db.prepare(` + SELECT COUNT(*) AS n FROM daily_goals + WHERE user_id = ? + AND tests_target > 0 AND tests_done >= tests_target + AND xp_target > 0 AND xp_earned >= xp_target + `).get(userId)?.n || 0; + if (n >= 30) unlockAchievement(userId, 'goal_30'); + } catch (e) { /* table missing on legacy install */ } + + // ── early_bird / night_owl — first activity at hour 0-8 / 23 ─ + try { + const eb = db.prepare(` + SELECT 1 FROM xp_log + WHERE user_id = ? AND CAST(strftime('%H', created_at) AS INTEGER) < 9 + LIMIT 1 + `).get(userId); + if (eb) unlockAchievement(userId, 'early_bird'); + const no = db.prepare(` + SELECT 1 FROM xp_log + WHERE user_id = ? AND CAST(strftime('%H', created_at) AS INTEGER) >= 23 + LIMIT 1 + `).get(userId); + if (no) unlockAchievement(userId, 'night_owl'); + } catch (e) { /* xp_log missing — shouldn't happen */ } + + // ── exam-prep attempts ──────────────────────────────────────── + try { + const examCorrect = db.prepare(` + SELECT COUNT(*) AS n FROM exam_attempts WHERE user_id = ? AND is_correct = 1 + `).get(userId)?.n || 0; + if (examCorrect >= 1) unlockAchievement(userId, 'exam_first'); + if (examCorrect >= 25) unlockAchievement(userId, 'exam_25_attempts'); + if (examCorrect >= 100) unlockAchievement(userId, 'exam_100_attempts'); + + // ── exam_variant_clear / exam_5_variants — perfect mock variants + const cleared = db.prepare(` + SELECT COUNT(DISTINCT variant) AS n FROM exam_mock_sessions + WHERE user_id = ? AND source = 'variant' AND status = 'finished' + AND total_tasks > 0 AND total_correct = total_tasks + `).get(userId)?.n || 0; + if (cleared >= 1) unlockAchievement(userId, 'exam_variant_clear'); + if (cleared >= 5) unlockAchievement(userId, 'exam_5_variants'); + + // ── exam_topic_master — any subtopic with ≥10 attempts at ≥90% acc. + const master = db.prepare(` + SELECT 1 FROM ( + SELECT t.subtopic, + COUNT(*) AS attempts, + SUM(CASE WHEN a.is_correct = 1 THEN 1 ELSE 0 END) AS correct + FROM exam_attempts a + JOIN exam_tasks t ON t.id = a.exam_task_id + WHERE a.user_id = ? AND a.is_correct IS NOT NULL AND t.subtopic IS NOT NULL + GROUP BY t.subtopic + HAVING attempts >= 10 + AND (CAST(correct AS REAL) / attempts) >= 0.9 + ) LIMIT 1 + `).get(userId); + if (master) unlockAchievement(userId, 'exam_topic_master'); + + // ── mock sessions: done / pass / perfect + const mockRow = db.prepare(` + SELECT COUNT(*) AS done, + COALESCE(MAX(score), 0) AS best + FROM exam_mock_sessions + WHERE user_id = ? AND status = 'finished' + `).get(userId); + if (mockRow.done >= 1) unlockAchievement(userId, 'exam_mock_done'); + if (mockRow.best >= 7) unlockAchievement(userId, 'exam_mock_pass'); + if (mockRow.best >= 10) unlockAchievement(userId, 'exam_mock_perfect'); + } catch (e) { console.error('[ach] exam:', e.message); } + + // ── textbook progress: tb_first_para only (chapter/book aggregates + // require parsing paragraphs_read JSON — left for a later phase). */ + try { + const tb = db.prepare(` + SELECT 1 FROM textbook_progress WHERE user_id = ? LIMIT 1 + `).get(userId); + if (tb) unlockAchievement(userId, 'tb_first_para'); + } catch (e) { /* table missing */ } + + // ── flashcards: total reviews ────────────────────────────────── + try { + const fc = db.prepare(` + SELECT + COUNT(*) AS reviews, + COUNT(DISTINCT card_id) AS unique_cards + FROM flashcard_reviews WHERE user_id = ? + `).get(userId); + if (fc.reviews >= 1) unlockAchievement(userId, 'fc_first_deck'); + if (fc.reviews >= 100) unlockAchievement(userId, 'fc_100_cards'); + if (fc.reviews >= 1000) unlockAchievement(userId, 'fc_1000_cards'); + } catch (e) { /* table missing */ } + + // ── biochem ─────────────────────────────────────────────────── + try { + const mol = db.prepare('SELECT COUNT(*) AS n FROM bio_user_molecules WHERE user_id = ?').get(userId)?.n || 0; + if (mol >= 1) unlockAchievement(userId, 'bc_first_molecule'); + const chal = db.prepare('SELECT COUNT(*) AS n FROM bio_user_challenges WHERE user_id = ?').get(userId)?.n || 0; + if (chal >= 5) unlockAchievement(userId, 'bc_5_challenges'); + if (chal >= 20) unlockAchievement(userId, 'bc_20_challenges'); + } catch (e) { /* tables missing */ } + + // ── minigame wins via xp_log reasons ────────────────────────── + try { + const wins = db.prepare(` + SELECT COUNT(*) AS n FROM xp_log + WHERE user_id = ? AND reason IN ('hangman_win', 'crossword_win') + `).get(userId)?.n || 0; + if (wins >= 5) unlockAchievement(userId, 'game_win_5'); + if (wins >= 25) unlockAchievement(userId, 'game_win_25'); + } catch (e) { /* xp_log missing */ } + + // ── pet petting streak ──────────────────────────────────────── + try { + const ps = db.prepare('SELECT pet_petting_streak AS s FROM users WHERE id = ?').get(userId)?.s || 0; + if (ps >= 7) unlockAchievement(userId, 'pet_streak_7'); + if (ps >= 30) unlockAchievement(userId, 'pet_streak_30'); + } catch (e) { /* column missing on very old install */ } + + // ── live-quiz participation (count distinct sessions answered) ─ + try { + const lq = db.prepare(` + SELECT COUNT(DISTINCT session_id) AS n FROM live_answers WHERE user_id = ? + `).get(userId)?.n || 0; + if (lq >= 1) unlockAchievement(userId, 'lq_first'); + if (lq >= 3) unlockAchievement(userId, 'lq_3_quizzes'); + } catch (e) { /* tables missing */ } + + // ── classroom attendance ────────────────────────────────────── + try { + const cr = db.prepare(` + SELECT COUNT(DISTINCT session_id) AS n FROM classroom_attendance WHERE user_id = ? + `).get(userId)?.n || 0; + if (cr >= 1) unlockAchievement(userId, 'cr_first_join'); + if (cr >= 5) unlockAchievement(userId, 'cr_5_lessons'); + if (cr >= 25) unlockAchievement(userId, 'cr_25_lessons'); + } catch (e) { /* table missing */ } + + // ── teacher: largest class membership ───────────────────────── + try { + const cm = db.prepare(` + SELECT COALESCE(MAX(member_count), 0) AS n FROM ( + SELECT c.id, COUNT(cm.user_id) AS member_count + FROM classes c + LEFT JOIN class_members cm ON cm.class_id = c.id + WHERE c.teacher_id = ? + GROUP BY c.id + ) + `).get(userId)?.n || 0; + if (cm >= 5) unlockAchievement(userId, 'class_5_members'); + if (cm >= 25) unlockAchievement(userId, 'class_25_members'); + } catch (e) { /* table missing */ } + + // ── parent link ─────────────────────────────────────────────── + try { + const pl = db.prepare('SELECT 1 FROM parent_links WHERE student_id = ? LIMIT 1').get(userId); + if (pl) unlockAchievement(userId, 'parent_link'); + } catch (e) { /* table missing */ } + + // ── leaderboard placement (weekly XP) ───────────────────────── + // Compute on the fly: rank user by week XP among all students. + try { + const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString(); + const rank = db.prepare(` + SELECT (1 + ( + SELECT COUNT(*) FROM ( + SELECT u2.id, COALESCE(SUM(xl2.amount), 0) AS wxp + FROM users u2 + LEFT JOIN xp_log xl2 ON xl2.user_id = u2.id AND xl2.created_at >= ? + WHERE u2.role = 'student' AND u2.id != ? + GROUP BY u2.id + HAVING wxp > ( + SELECT COALESCE(SUM(amount), 0) FROM xp_log + WHERE user_id = ? AND created_at >= ? + ) + ) + )) AS r + `).get(weekAgo, userId, userId, weekAgo)?.r; + // Only credit if the user actually contributed XP this week. + const myXp = db.prepare(` + SELECT COALESCE(SUM(amount), 0) AS s FROM xp_log + WHERE user_id = ? AND created_at >= ? + `).get(userId, weekAgo)?.s || 0; + if (myXp > 0) { + if (rank <= 10) unlockAchievement(userId, 'lb_top10'); + if (rank === 1) unlockAchievement(userId, 'lb_top1'); + } + } catch (e) { console.error('[ach] leaderboard:', e.message); } } function checkRedBookAchievements(userId) { diff --git a/backend/src/db/migrations/031_phase3_achievements.sql b/backend/src/db/migrations/031_phase3_achievements.sql new file mode 100644 index 0000000..59b7a7f --- /dev/null +++ b/backend/src/db/migrations/031_phase3_achievements.sql @@ -0,0 +1,68 @@ +-- ═══════════════════════════════════════════════════════════════ +-- 031: Phase 3 — new achievement catalog +-- +-- Adds 38 achievements covering features that landed since the original +-- 36 were seeded: exam-prep (exam9), textbooks, classroom/board, +-- biochemistry, live-quiz, flashcards, hangman/crossword, plus a new +-- 'social' group for class & leaderboard wins and a few stretch goals +-- in 'consistency' (streak_100, goal_30, early-bird / night-owl). +-- +-- Inserts use INSERT OR IGNORE so re-runs are safe; backend +-- seedAchievements() will keep title/icon/desc/sort_order in sync on +-- every boot via UPDATE. +-- +-- A new top-level group 'exam' slots between 'consistency' (400) and +-- 'exploration' (500). Group display metadata is hard-coded in both +-- backend (gamification/_shared.ACHIEVEMENT_GROUPS) and frontend +-- (profile.html ACH_GROUPS); keep them in sync. +-- ═══════════════════════════════════════════════════════════════ + +INSERT OR IGNORE INTO achievements + (slug, title, icon, category, description, group_slug, track, tier, sort_order) +VALUES + -- ── consistency extensions ───────────────────────────────── + ('streak_100', 'Сто дней подряд', 'flame', 'streak', 'Невероятная серия — 100 дней активности подряд', 'consistency', 'streak', 4, 440), + ('goal_30', 'Цель: 30 дней', 'target', 'streak', '30 дней подряд выполнять цель дня', 'consistency', 'goal_streak', 1, 450), + ('early_bird', 'Ранняя пташка', 'sunrise', 'streak', 'Активность до 9 утра', 'consistency', 'time', 1, 460), + ('night_owl', 'Ночная сова', 'moon', 'streak', 'Активность после 23:00', 'consistency', 'time', 2, 470), + + -- ── exam-prep (math9) ────────────────────────────────────── + ('exam_first', 'Старт экзамена', 'flag', 'exam', 'Решена первая задача экзамена 9 класса', 'exam', 'exam_attempts', 1, 700), + ('exam_25_attempts', '25 задач экзамена', 'list-checks', 'exam', '25 решённых попыток в банке экзамена', 'exam', 'exam_attempts', 2, 710), + ('exam_100_attempts', '100 задач экзамена', 'check-square', 'exam', '100 решённых попыток в банке экзамена', 'exam', 'exam_attempts', 3, 720), + ('exam_variant_clear', 'Чистый вариант', 'crown', 'exam', 'Все 10 задач варианта решены верно', 'exam', 'variants', 1, 730), + ('exam_5_variants', '5 вариантов', 'layers', 'exam', '5 разных вариантов закрыто целиком', 'exam', 'variants', 2, 740), + ('exam_topic_master', 'Мастер темы', 'target', 'exam', '≥90% точности на одной из подтем (≥10 попыток)', 'exam', 'topic', 1, 750), + ('exam_mock_done', 'Первый пробник', 'timer', 'exam', 'Завершён первый пробный экзамен', 'exam', 'mock', 1, 760), + ('exam_mock_pass', 'Пробник сдан', 'badge-check', 'exam', 'Балл ≥ 7 на пробном экзамене', 'exam', 'mock', 2, 770), + ('exam_mock_perfect', 'Пробник на 10', 'trophy', 'exam', 'Идеальные 10 баллов на пробном экзамене', 'exam', 'mock', 3, 780), + + -- ── mastery extensions (textbooks + flashcards) ──────────── + ('tb_first_para', 'Открыл параграф', 'book-marked', 'theory', 'Прочитан первый параграф учебника', 'mastery', 'tb_progress', 1, 800), + ('tb_chapter_done','Глава закрыта', 'book-check', 'theory', 'Все параграфы одной главы пройдены', 'mastery', 'tb_progress', 2, 810), + ('tb_book_done', 'Учебник целиком', 'book-up', 'theory', 'Все параграфы учебника пройдены', 'mastery', 'tb_progress', 3, 820), + ('tb_3_books', 'Полка из трёх', 'library-big', 'theory', '3 учебника закрыто полностью', 'mastery', 'tb_progress', 4, 830), + ('fc_first_deck', 'Первая колода', 'layers', 'theory', 'Все карточки одной колоды разок повторены', 'mastery', 'flashcards', 1, 840), + ('fc_100_cards', '100 карточек', 'rotate-cw', 'theory', '100 повторов карточек в сумме', 'mastery', 'flashcards', 2, 850), + ('fc_1000_cards', '1000 карточек', 'sparkles', 'theory', '1000 повторов карточек в сумме', 'mastery', 'flashcards', 3, 860), + + -- ── exploration extensions (biochem / games / pet) ───────── + ('bc_first_molecule', 'Первая молекула', 'flask-round', 'lab', 'Собрана первая молекула в био-конструкторе', 'exploration', 'biochem', 1, 870), + ('bc_5_challenges', '5 био-задач', 'beaker', 'lab', '5 биохимических задач решено', 'exploration', 'biochem', 2, 880), + ('bc_20_challenges', '20 био-задач', 'atom', 'lab', '20 биохимических задач решено', 'exploration', 'biochem', 3, 890), + ('game_win_5', '5 побед в играх', 'gamepad-2', 'volume','5 побед в Виселице или Кроссворде суммарно', 'exploration', 'minigames', 1, 900), + ('game_win_25', '25 побед в играх', 'gamepad', 'volume','25 побед в Виселице или Кроссворде суммарно', 'exploration', 'minigames', 2, 910), + ('pet_streak_7', 'Заботливый', 'heart', 'volume','7 дней подряд гладить питомца', 'exploration', 'pet', 1, 920), + ('pet_streak_30', 'Лучший друг', 'paw-print', 'volume','30 дней подряд гладить питомца', 'exploration', 'pet', 2, 930), + + -- ── social (NEW group) ───────────────────────────────────── + ('lb_top10', 'Топ-10 недели', 'trending-up', 'volume', 'Попасть в топ-10 лидерборда недели', 'social', 'leaderboard', 1, 1000), + ('lb_top1', 'Первое место', 'medal', 'volume', '№1 в лидерборде недели', 'social', 'leaderboard', 2, 1010), + ('lq_first', 'Первый live-quiz', 'radio', 'volume', 'Принять участие в первом live-квизе', 'social', 'live_quiz', 1, 1020), + ('lq_3_quizzes', '3 live-квиза', 'megaphone', 'volume', 'Принять участие в 3 live-квизах', 'social', 'live_quiz', 2, 1030), + ('cr_first_join', 'Первое подключение','door-open', 'volume', 'Первое подключение к уроку в classroom', 'social', 'classroom', 1, 1040), + ('cr_5_lessons', '5 уроков', 'presentation', 'volume', 'Посещено 5 уроков classroom', 'social', 'classroom', 2, 1050), + ('cr_25_lessons', '25 уроков', 'school', 'volume', 'Посещено 25 уроков classroom', 'social', 'classroom', 3, 1060), + ('class_5_members', 'Учитель: 5 учеников','users', 'start', 'Учитель: класс из ≥5 учеников', 'social', 'teacher', 1, 1070), + ('class_25_members', 'Учитель: 25 учеников','users-2', 'start', 'Учитель: класс из ≥25 учеников', 'social', 'teacher', 2, 1080), + ('parent_link', 'Связан с родителем','user-plus', 'start', 'Связь с родителем установлена', 'social', 'family', 1, 1090); diff --git a/frontend/profile.html b/frontend/profile.html index edb6065..3d54e90 100644 --- a/frontend/profile.html +++ b/frontend/profile.html @@ -1724,6 +1724,7 @@ { slug: 'volume', title: 'Объём', icon: 'bar-chart-2' }, { slug: 'mastery', title: 'Качество', icon: 'award' }, { slug: 'consistency', title: 'Постоянство', icon: 'flame' }, + { slug: 'exam', title: 'Экзамен 9', icon: 'clipboard-list' }, { slug: 'exploration', title: 'Исследование', icon: 'compass' }, { slug: 'social', title: 'Социальное', icon: 'users' }, ];