/* ============================================================
   designquest/designquest-data.jsx — независимая подсистема
   «Design Quest: 100 испытаний». 7 актов, 100 задач с индивидуальным
   XP, боссы (нельзя пропустить — блокируют следующий акт), секретные
   ачивки (отмечаются вручную, не проверяются кодом).
   Свой store (localStorage "designquest-state"). Полностью независим
   от остальной части планера и от подсистем «Игровые курсы»/«Академия».
   Exposes: DQ_ACTS, DQ_RANKS, DQ_DAILY, DQ_TROPHIES, DQ_SECRETS,
   DQStore, useDQ, DQActions, DQLookup.
   ============================================================ */
const React = window.React;

/* ---- 7 актов × 100 задач (из «Design Quest: 100 испытаний») ------- */
const DQ_ACTS = [
  { roman: "I", title: "Глаз новичка — учимся видеть", trophy: { id: "act1", name: "Прозревший", desc: "Все 15 задач Акта I пройдены" },
    quests: [
      { num: 1, title: "Охота за уродством", xp: 10, desc: "Сфотографируй 5 плохо свёрстанных объявлений на улице или в подъезде. Запиши, что именно в них раздражает." },
      { num: 2, title: "Музей в кармане", xp: 10, desc: "Собери папку «Вдохновение» и положи туда 20 работ с Behance/Pinterest, которые тебе нравятся. Не анализируй — просто собирай." },
      { num: 3, title: "Детектив шрифтов", xp: 15, desc: "Определи с помощью WhatTheFont или Fontanello шрифты на 5 любимых сайтах." },
      { num: 4, title: "Цветовой вор", xp: 15, desc: "Укради палитру из фотографии заката (пипеткой в любом редакторе) и сохрани 5 цветов с HEX-кодами." },
      { num: 5, title: "Ребус сетки", xp: 20, desc: "Возьми разворот любого журнала и карандашом начерти поверх него линии: где колонки, где поля, где выравнивание." },
      { num: 6, title: "Слепой тест", xp: 15, desc: "Покажи другу два логотипа (хороший и плохой) и попроси объяснить, какой «дороже». Сравни его аргументы со своими." },
      { num: 7, title: "Дневник кернинга", xp: 20, desc: "Сыграй в игру Kerntype (type.method.ac) и набери минимум 80/100." },
      { num: 8, title: "Один день — один принцип", xp: 20, desc: "Прочитай про принцип близости (гештальт) и найди 3 примера его нарушения в реальной жизни." },
      { num: 9, title: "Чёрно-белый мир", xp: 20, desc: "Переведи 5 работ из папки «Вдохновение» в градации серого. Заметь: хорошие работы держатся и без цвета." },
      { num: 10, title: "Копипаст-археология", xp: 25, desc: "Перерисуй от руки (криво — можно!) обложку любимого альбома. Рука запомнит композицию лучше глаз." },
      { num: 11, title: "Игра в квадраты", xp: 25, desc: "На бумаге в клетку составь 10 разных композиций только из чёрных квадратов: напряжение, покой, хаос, ритм..." },
      { num: 12, title: "Шрифтовые пары", xp: 25, desc: "Подбери 3 пары «заголовок + текст» на Google Fonts и объясни себе, почему они дружат." },
      { num: 13, title: "Антибриф", xp: 15, desc: "Опиши словами худший дизайн визитки, который можно представить. Юмор обязателен." },
      { num: 14, title: "60-30-10", xp: 20, desc: "Найди 3 интерьера или плаката, где работает правило 60-30-10 в цвете, и разметь проценты." },
      { num: 15, title: "МИНИ-БОСС: «Критик»", xp: 40, boss: true, desc: "Напиши разбор одной чужой работы на 200 слов: композиция, цвет, шрифт, что бы ты улучшил." },
    ]},
  { roman: "II", title: "Слово и буква — типографика", trophy: { id: "act2", name: "Буквоед", desc: "Все задачи Акта II пройдены" },
    quests: [
      { num: 16, title: "Анатомичка", xp: 20, desc: "Выучи 10 терминов анатомии буквы (засечка, апрош, выносной элемент...) и подпиши их на распечатанной букве «Й»." },
      { num: 17, title: "Дуэль антиквы и гротеска", xp: 20, desc: "Набери один и тот же абзац антиквой и гротеском. Опиши, как меняется «голос» текста." },
      { num: 18, title: "Интерлиньяж-качели", xp: 25, desc: "Возьми абзац текста и сделай 4 версии: интерлиньяж 100%, 120%, 145%, 200%. Найди, где тексту «дышится»." },
      { num: 19, title: "Типографская гигиена", xp: 20, desc: 'Выучи разницу: дефис, среднее тире, длинное тире, минус. И где ставятся «ёлочки» vs "лапки".' },
      { num: 20, title: "Висячие строки", xp: 20, desc: "Найди в любой книге «висячую строку» и «сироту». Сфотографируй как трофей." },
      { num: 21, title: "Один шрифт — сто лиц", xp: 30, desc: "Сделай 6 карточек с одним словом одним шрифтом, меняя только размер, начертание и трекинг." },
      { num: 22, title: "Модульная лесенка", xp: 30, desc: "Построй типографскую шкалу (16 → 20 → 25 → 31 → 39...) с коэффициентом 1.25 и оформи ей условную статью." },
      { num: 23, title: "Плакат из одной буквы", xp: 35, desc: "Создай выразительный плакат, где главный герой — одна гигантская буква." },
      { num: 24, title: "Читаемость на пределе", xp: 25, desc: "Определи экспериментально: при какой длине строки (в знаках) тебе комфортно читать? Проверь на 45, 66, 90 знаках." },
      { num: 25, title: "Шрифт эпохи", xp: 25, desc: "Найди по одному характерному шрифту для 1920-х, 1970-х и 1990-х. Собери мини-таймлайн." },
      { num: 26, title: "Кириллица-капризуля", xp: 30, desc: "Найди 3 шрифта с плохой кириллицей и 3 с отличной. Сформулируй, чем они отличаются." },
      { num: 27, title: "Текст как текстура", xp: 30, desc: "Набери страницу «серым текстом» так, чтобы прищурившись, ты видел ровное серое поле без дыр и пятен." },
      { num: 28, title: "Цитата-звезда", xp: 35, desc: "Оформи одну цитату пятью разными способами: классика, брутализм, минимализм, ретро, журнальный стиль." },
      { num: 29, title: "Иерархия вслепую", xp: 35, desc: "Возьми хаотичный текст (объявление с 10 пунктами) и выстрой иерархию только размером и жирностью. Один шрифт, один цвет." },
      { num: 30, title: "МИНИ-БОСС: «Типографский плакат»", xp: 50, boss: true, desc: "Сверстай плакат мероприятия только текстом: без картинок, максимум 2 шрифта, 2 цвета." },
    ]},
  { roman: "III", title: "Врата InDesign — знакомство с инструментом", trophy: { id: "act3", name: "Ключ от замка", desc: "Босс Акта III повержен" },
    quests: [
      { num: 31, title: "Ритуал первого запуска", xp: 15, desc: "Создай документ А4, настрой поля 20 мм, сохрани. Найди, где живут панели Pages, Layers, Swatches." },
      { num: 32, title: "Заклинание W", xp: 25, desc: "Выучи 10 горячих клавиш: W (превью), Cmd/Ctrl+D (поместить), T (текст), V/A (стрелки), Cmd+; (направляющие)... Пользуйся только ими один день." },
      { num: 33, title: "Фрейм-жонглёр", xp: 25, desc: "Создай 9 фреймов и потренируйся: связать текстовые цепочкой, вписать картинку (Fill/Fit), обтечь текстом." },
      { num: 34, title: "Placeholder-поэзия", xp: 25, desc: "Залей фрейм рыбным текстом (Type → Fill with Placeholder Text) и отформатируй его в 2 колонки с буквицей." },
      { num: 35, title: "Мастер и его страницы", xp: 30, desc: "Создай мастер-страницу (Parent Page) с колонтитулом и автоматической нумерацией. Применить к 8 страницам." },
      { num: 36, title: "Библиотека красок", xp: 25, desc: "Создай 6 своих Swatches, включая один Tint 40% и один градиент. Удали все неиспользуемые." },
      { num: 37, title: "Стили-близнецы", xp: 35, desc: "Создай Paragraph Styles «Заголовок», «Текст», «Подпись» и Character Style «Акцент». Перекрась весь документ за 4 клика." },
      { num: 38, title: "Визитка за 30 минут", xp: 40, desc: "Сверстай себе визитку 90×50 мм с вылетами 3 мм. Экспортируй PDF с метками реза." },
      { num: 39, title: "Сетка-скелет", xp: 35, desc: "Настрой базовую сетку (Baseline Grid) и заставь весь текст сидеть на ней, как солдаты на плацу." },
      { num: 40, title: "Спасение утопающего", xp: 35, desc: "Намеренно «сломай» документ: перекрытый текст (overset), потерянный линк, RGB-картинка. Потом почини всё через Preflight." },
      { num: 41, title: "Меню мечты", xp: 40, desc: "Сверстай меню кофейни на 1 страницу: категории, цены с отточием (таб с точками!), декор." },
      { num: 42, title: "Объектные стили", xp: 30, desc: "Создай Object Style для карточек: скругление, тень, отступ текста внутри. Применить к 6 карточкам." },
      { num: 43, title: "Слои-невидимки", xp: 20, desc: "Разложи макет на 3 слоя (фон / картинки / текст), заблокируй фон, скрой картинки — почувствуй порядок." },
      { num: 44, title: "Экспорт-алхимия", xp: 30, desc: "Один макет — три экспорта: PDF/X-4 для печати, PDF для экрана, PNG для соцсетей. Сравни размеры файлов." },
      { num: 45, title: "БОСС АКТА: «Афиша»", xp: 70, boss: true, desc: "Афиша А3 для вымышленного концерта: сетка, стили, минимум 3 уровня иерархии, экспорт в печать. Дедлайн — 2 часа, таймер честный." },
    ]},
  { roman: "IV", title: "Цвет, образ, композиция", trophy: { id: "act4", name: "Хроматический рыцарь", desc: "Босс Акта IV повержен" },
    quests: [
      { num: 46, title: "CMYK против RGB", xp: 25, desc: "Создай ярко-салатовый и неоново-синий в RGB, переведи в CMYK. Оплачь потери. Запиши вывод." },
      { num: 47, title: "Монохромный подвиг", xp: 35, desc: "Сверстай разворот, используя только оттенки одного цвета + чёрный." },
      { num: 48, title: "Психология в деле", xp: 40, desc: "Сделай 2 версии одной упаковки чая: «успокаивающий» и «бодрящий». Меняй только цвет и шрифт." },
      { num: 49, title: "Фото-кадрирование", xp: 30, desc: "Возьми 1 фотографию и закадрируй её 5 разными способами: крупный план, воздух, диагональ, симметрия, «неправильный» кроп." },
      { num: 50, title: "Правило нечётного", xp: 25, desc: "Составь композицию из 3 и из 4 объектов. Сравни: почему нечётное живее?" },
      { num: 51, title: "Направляющие взгляда", xp: 30, desc: "Разметь на 3 чужих плакатах стрелками, как двигается взгляд зрителя. Найди «точку входа»." },
      { num: 52, title: "Белое — тоже цвет", xp: 35, desc: "Сверстай макет, где 70% площади — пустота, и она работает." },
      { num: 53, title: "Дуотон-магия", xp: 35, desc: "Сделай дуотон-обработку фото (2 цвета) и используй в обложке плейлиста." },
      { num: 54, title: "Контраст-экстрим", xp: 40, desc: "Один макет — максимальный контраст всего (размер, цвет, форма). Второй — почти нулевой. Найди золотую середину третьим макетом." },
      { num: 55, title: "Ритм и брейк", xp: 35, desc: "Построй сетку из 12 одинаковых элементов, затем сломай ритм ровно одним элементом так, чтобы он стал главным." },
      { num: 56, title: "Флаг несуществующей страны", xp: 35, desc: "Придумай страну и её флаг по правилам вексиллологии (простота, 2–3 цвета, читается издалека)." },
      { num: 57, title: "Асимметричный баланс", xp: 35, desc: "Уравновесь большой тёмный объект слева маленьким ярким справа. Добейся ощущения покоя без симметрии." },
      { num: 58, title: "Коллаж-франкенштейн", xp: 45, desc: "Собери коллаж из 5+ вырезанных изображений с единым настроением. В InDesign, с обтравкой." },
      { num: 59, title: "Оптические обманы", xp: 30, desc: "Выровняй круг, треугольник и квадрат в строку «по линейке», потом «по глазам». Почувствуй разницу оптической компенсации." },
      { num: 60, title: "БОСС АКТА: «Обложка книги»", xp: 80, boss: true, desc: "Полная обложка (лицо + корешок + задник) для романа, который ты любишь. Никаких стоковых клише." },
    ]},
  { roman: "V", title: "Многостраничная магия", trophy: { id: "act5", name: "Переплётных дел мастер", desc: "Босс Акта V повержен" },
    quests: [
      { num: 61, title: "Модульная сетка ×12", xp: 40, desc: "Построй 12-колоночную сетку и сверстай на ней 3 разных по ритму разворота." },
      { num: 62, title: "Журнальный разворот", xp: 50, desc: "Статья на 2 полосы: заголовок-шапка, лид, буквица, врезка-цитата, подписи к фото." },
      { num: 63, title: "Автооглавление", xp: 40, desc: "Настрой Table of Contents, которое собирается само из стилей заголовков. Обнови его после перестановки глав." },
      { num: 64, title: "GREP-колдовство", xp: 50, desc: "Напиши GREP-стиль, который сам делает все цифры в тексте другим шрифтом, и второй — который убирает висячие предлоги (неразрывный пробел)." },
      { num: 65, title: "Вложенные стили", xp: 40, desc: "Настрой Nested Style: первые три слова абзаца — капителью, дальше обычный текст. Автоматически." },
      { num: 66, title: "Таблица не из ада", xp: 45, desc: "Сверстай красивую таблицу-прайс: стили таблиц и ячеек, чередование строк, без «тюремной решётки» из линий." },
      { num: 67, title: "Лонгрид на бумаге", xp: 55, desc: "Свёрстка статьи на 6 полос с выдержанным ритмом: чередуй плотные и воздушные развороты." },
      { num: 68, title: "Библиотека CC / сниппеты", xp: 35, desc: "Сохрани повторяющиеся элементы (плашки, врезки) как Snippets или в CC Library. Собери новую полосу из готовых кубиков за 10 минут." },
      { num: 69, title: "Книга из глав", xp: 45, desc: "Освой функцию Book (.indb): собери 3 отдельных файла-главы в одну книгу со сквозной нумерацией." },
      { num: 70, title: "Сноски и колонтитулы-хамелеоны", xp: 45, desc: "Настрой сноски и «живые» колонтитулы, которые сами подставляют название текущей главы (Running Header)." },
      { num: 71, title: "Data Merge: сто именных дипломов", xp: 50, desc: "Подготовь CSV с 10 именами и сгенерируй именные сертификаты автоматически." },
      { num: 72, title: "Каталог товаров", xp: 55, desc: "Мини-каталог на 8 полос: сетка карточек, единые объектные стили, якорные объекты (Anchored Objects)." },
      { num: 73, title: "Интерактивный PDF", xp: 45, desc: "Добавь в документ оглавление-гиперссылки, кнопки навигации и переходы. Экспортируй интерактивный PDF." },
      { num: 74, title: "Спуск полос на пальцах", xp: 45, desc: "Разберись, как страницы буклета А5 из 8 полос ложатся на печатный лист. Собери макет и распечатай дома тестовую «книжечку»." },
      { num: 75, title: "БОСС АКТА: «Зин»", xp: 100, boss: true, desc: "Собери собственный зин на 16 полос на любую странную тему (коллекция люков? словарь бабушкиных слов?). От концепции до сшитого степлером тиража в 3 экземпляра." },
    ]},
  { roman: "VI", title: "Айдентика и смысл", trophy: { id: "act6", name: "Хранитель смысла", desc: "Босс Акта VI повержен" },
    quests: [
      { num: 76, title: "Мудборд-настроение", xp: 35, desc: "Собери мудборд для вымышленного бренда (например, пекарня для сов, работающая только ночью)." },
      { num: 77, title: "Логотип за 100 эскизов", xp: 60, desc: "Нарисуй 100 быстрых карандашных эскизов одного знака. Да, сто. Лучшие идеи придут после сорокового." },
      { num: 78, title: "Фирменные константы", xp: 40, desc: "Определи для своего бренда: 2 шрифта, 4 цвета, характер графики, tone of voice в трёх прилагательных." },
      { num: 79, title: "Гайдлайн на 8 полос", xp: 60, desc: "Сверстай мини-брендбук в InDesign: логотип, охранное поле, цвета, типографика, примеры «можно/нельзя»." },
      { num: 80, title: "Стресс-тест логотипа", xp: 40, desc: "Проверь знак: 16×16 px, чёрно-белый, на фото, вывернутый, на кружке. Почини слабые места." },
      { num: 81, title: "Ребрендинг наоборот", xp: 40, desc: "Возьми известный бренд и сделай ему нарочно плохой редизайн. Затем письменно объясни, почему каждое решение — ошибка." },
      { num: 82, title: "Система, а не картинка", xp: 50, desc: "Придумай графический приём (паттерн, рамку, угол наклона), который узнаётся даже без логотипа. Покажи на 3 носителях." },
      { num: 83, title: "Меню + визитка + вывеска", xp: 55, desc: "Примени айдентику своего бренда к трём носителям разного масштаба. Выдержи единство." },
      { num: 84, title: "Дизайн под запрет", xp: 40, desc: "Сделай афишу без единого шрифта с засечками, без фото и без красного. Ограничения рождают идеи." },
      { num: 85, title: "Локализация", xp: 40, desc: "Адаптируй свой макет под другой язык (текст длиннее на 30%!). Что сломалось? Как чинил?" },
      { num: 86, title: "Доступность", xp: 40, desc: "Проверь контрасты макета по WCAG (минимум 4.5:1 для текста). Исправь провалы, не убив стиль." },
      { num: 87, title: "Презентация — тоже дизайн", xp: 45, desc: "Оформи подачу проекта: мокапы, крупные детали, история решения. Плохая подача убивает хорошую работу." },
      { num: 88, title: "БОСС АКТА: «Фестиваль»", xp: 120, boss: true, desc: "Полная айдентика вымышленного фестиваля: знак, афиша, бейдж, схема площадки, пост для соцсетей. Всё в одной системе." },
    ]},
  { roman: "VII", title: "Испытания Архимага", trophy: null,
    quests: [
      { num: 89, title: "Спринт 25 минут", xp: 50, desc: "Полноценный плакат за один помидор. Таймер. Никакого перфекционизма." },
      { num: 90, title: "Ремейк классики", xp: 60, desc: "Возьми легендарный плакат (Мюллер-Брокманн, Родченко...) и сделай трибьют в своей манере, сохранив дух." },
      { num: 91, title: "Дизайн вслепую", xp: 55, desc: "Попроси друга дать бриф голосом, не показывая референсов. Сдай работу с одной итерации правок." },
      { num: 92, title: "Годовой отчёт", xp: 90, desc: "Сверстай фрагмент годового отчёта на 10 полос: инфографика, диаграммы, таблицы, сухие данные — сделай их красивыми." },
      { num: 93, title: "Шрифтовой плакат-эксперимент", xp: 55, desc: "Разрушь текст: искази, разрежь, наслои — но сохрани читаемость сообщения на 2 секунды взгляда." },
      { num: 94, title: "Реставратор", xp: 60, desc: "Найди реальный плохой макет (объявление ТСЖ подойдёт) и сделай редизайн. До/после — в портфолио." },
      { num: 95, title: "Собери сетку с нуля", xp: 60, desc: "Разработай собственную модульную сетку под конкретную задачу и задокументируй логику: почему столько колонок, такие поля, такой модуль." },
      { num: 96, title: "Печать по-взрослому", xp: 65, desc: "Подготовь макет с пантоном или частичным лаком: отдельный слой, оверпринт, инструкция для типографии." },
      { num: 97, title: "Скорость мастера", xp: 50, desc: "Повтори задачу 45 (афиша), но за 40 минут. Сравни с той работой — измерь свой рост." },
      { num: 98, title: "Наставник", xp: 45, desc: "Объясни новичку (живому или воображаемому, письменно) три принципа дизайна на пальцах. Учишь — понимаешь вдвойне." },
      { num: 99, title: "Портфолио", xp: 110, desc: "Отбери 6–8 лучших работ из этого квеста и сверстай портфолио в InDesign: PDF на 20+ полос, с историями проектов." },
      { num: 100, title: "ФИНАЛЬНЫЙ БОСС: «Настоящий заказ»", xp: 200, boss: true, finalBoss: true,
        desc: "Найди реального «клиента» — знакомого с бизнесом, локальное кафе, волонтёрский проект — и сделай для него работу от брифа до готовых файлов в печать. Бесплатно или нет — неважно. Важно: реальные правки, реальный дедлайн, реальный результат в мире." },
    ]},
];

/* акты II–VII открываются только после босса предыдущего акта
   (правило источника: «Босса нельзя обойти») */
const __actBossId = {};
DQ_ACTS.forEach((act) => { const boss = act.quests.find((q) => q.boss); if (boss) __actBossId[act.roman] = "dq" + boss.num; });
DQ_ACTS.forEach((act, i) => {
  act.quests.forEach((q) => {
    q.id = "dq" + q.num;
    q.actRoman = act.roman;
    if (i > 0) q.requires = [__actBossId[DQ_ACTS[i - 1].roman]];
  });
});
const DQ_INDEX = {};
DQ_ACTS.forEach((act) => act.quests.forEach((q) => { DQ_INDEX[q.id] = { act, quest: q }; }));
const DQ_TOTAL_XP = DQ_ACTS.reduce((s, a) => s + a.quests.reduce((ss, q) => ss + q.xp, 0), 0);
const DQ_TOTAL_QUESTS = 100;

/* ---- звания по XP (шкала уровней из источника) ---------------------- */
const DQ_RANKS = [
  { at: 0,    name: "🥉 Подмастерье" },
  { at: 500,  name: "🥈 Ремесленник" },
  { at: 1500, name: "🥇 Мастер" },
  { at: 3000, name: "💎 Архимаг Сеток" },
];
function dqRankFor(xp) {
  let i = 0;
  for (let k = 0; k < DQ_RANKS.length; k++) if (xp >= DQ_RANKS[k].at) i = k;
  const rank = DQ_RANKS[i], next = DQ_RANKS[i + 1] || null;
  return { rank, next, idx: i, into: next ? xp - rank.at : 1, span: next ? next.at - rank.at : 1 };
}

/* ---- задачи дня -------------------------------------------------------- */
const DQ_DAILY = [
  { id: "d1", label: "Закрыть 1 задачу", metric: "count", target: 1, bonus: 15 },
  { id: "d3", label: "Закрыть 3 задачи", metric: "count", target: 3, bonus: 35 },
  { id: "d5", label: "Закрыть 5 задач",  metric: "count", target: 5, bonus: 60 },
];

/* ---- достижения: вехи + звания (авто-проверка) ------------------------ */
const DQ_MILESTONES = [
  { id: "first", name: "Первый эскиз", desc: "Первая задача закрыта", icon: "spark", test: (s) => Object.keys(s.done).length >= 1 },
  { id: "q25",   name: "Четверть пути", desc: "25 задач позади", icon: "scroll", test: (s) => Object.keys(s.done).length >= 25 },
  { id: "q50",   name: "Полпути",       desc: "50 задач позади", icon: "scroll", test: (s) => Object.keys(s.done).length >= 50 },
  { id: "q75",   name: "Три четверти",  desc: "75 задач позади", icon: "scroll", test: (s) => Object.keys(s.done).length >= 75 },
  ...DQ_ACTS.filter((a) => a.trophy).map((a) => ({
    id: a.trophy.id, name: a.trophy.name, desc: a.trophy.desc, icon: a.quests.some((q) => q.boss) ? "crown" : "book",
    test: (s) => {
      const boss = a.quests.find((q) => q.boss);
      return boss ? !!s.done[boss.id] : a.quests.every((q) => !!s.done[q.id]);
    },
  })),
  { id: "magnum", name: "Магнум опус", desc: "Финальный босс «Настоящий заказ» повержен — все 100 задач пройдены", icon: "crown", test: (s) => !!s.done.dq100 },
  { id: "streak3", name: "Три дня в мастерской", desc: "Серия занятий: 3 дня", icon: "flame", test: (s) => s.streak.count >= 3 },
  { id: "streak7", name: "Неделя без пауз", desc: "Серия занятий: 7 дней", icon: "flame", test: (s) => s.streak.count >= 7 },
  { id: "streak30", name: "Месяц дисциплины", desc: "Серия занятий: 30 дней", icon: "flame", test: (s) => s.streak.count >= 30 },
];
const DQ_RANK_TROPHIES = DQ_RANKS.slice(1).map((r) => ({
  id: "rank_" + r.name, name: "Новое звание: " + r.name, desc: "Достигнут ранг «" + r.name + "»", icon: "seal",
  test: (s) => s.xp >= r.at,
}));
const DQ_TROPHIES = [...DQ_MILESTONES, ...DQ_RANK_TROPHIES];

/* ---- секретные ачивки: отмечаются вручную, код их проверить не может --- */
const DQ_SECRETS = [
  { id: "night_elf",      name: "Ночной эльф",         icon: "moon",   desc: "Заверши любую задачу после полуночи и не пожалей утром." },
  { id: "kill_darlings",  name: "Убийца любимчиков",    icon: "close",  desc: "Удали элемент, который обожаешь, потому что он мешает макету." },
  { id: "phoenix",        name: "Феникс",               icon: "flame",  desc: "Переделай работу с нуля после жёсткой критики." },
  { id: "pixel_paranoid", name: "Пиксель-параноик",     icon: "target", desc: "Заметь смещение в 1 мм в чужой печатной продукции." },
  { id: "guild",          name: "Гильдия",              icon: "seal",   desc: "Получи разбор своей работы от живого дизайнера." },
  { id: "print_smell",    name: "Запах типографии",     icon: "book",   desc: "Напечатай хоть одну работу в настоящей типографии." },
];

/* ---- правила выживания (из источника, для инфо-карточки) -------------- */
const DQ_RULES = [
  "Ритм важнее рывков. Лучше 3 задачи в неделю, чем 20 за выходные и месяц тишины.",
  "Всё в архив. Каждую работу сохраняй в папку quest/№_название — в конце увидишь свою эволюцию.",
  "Сделано лучше идеального. Задача закрыта, когда работа существует, а не когда она безупречна.",
  "Босса нельзя обойти. Не переходи к следующему акту, пока не победил босса текущего.",
  "Разрешено гуглить, запрещено сдаваться. Туториал — это свиток знаний, а не читерство.",
];

/* ---- время -------------------------------------------------------------- */
const dqDayKey = (t) => new Date(t || Date.now()).toISOString().slice(0, 10);
const dqYesterKey = () => dqDayKey(Date.now() - 86400000);

/* ---- store -------------------------------------------------------------- */
const DQ_VERSION = "designquest-v1";
function dqMakeSeed() {
  return { xp: 0, done: {}, streak: { count: 0, last: null }, day: { key: dqDayKey(), count: 0, claimed: [] }, trophies: [], secrets: [] };
}
/* хранится на сервере, localStorage — кэш (см. makeQuestStore в data.jsx) */
window.DQStore = window.makeQuestStore("designquest", DQ_VERSION, dqMakeSeed);
window.useDQ = function () {
  const store = window.DQStore;
  const [, force] = React.useState(0);
  React.useEffect(() => store.subscribe(() => force((n) => n + 1)), []);
  return store.get();
};

/* ---- derived ------------------------------------------------------------- */
const dqUnlocked = (s, q) => (q.requires || []).every((r) => s.done[r]);
const dqActStats = (s, act) => {
  const done = act.quests.filter((q) => s.done[q.id]);
  return { total: act.quests.length, done: done.length, xpTotal: act.quests.reduce((s2, q) => s2 + q.xp, 0), xpGain: done.reduce((s2, q) => s2 + q.xp, 0) };
};
const dqQuestProgress = (s, d) => Math.min(s.day.count, d.target);

window.DQLookup = {
  quest: (id) => DQ_INDEX[id], unlocked: dqUnlocked, actStats: dqActStats,
  dailyProgress: dqQuestProgress, rankFor: dqRankFor, dayKey: dqDayKey,
  totalXp: DQ_TOTAL_XP, totalQuests: DQ_TOTAL_QUESTS,
};

/* ---- toast -------------------------------------------------------------- */
let dqToastSeq = 29000;
const dqToast = (text, pts) => { if (window.pushToast) window.pushToast({ id: ++dqToastSeq, text, pts: pts || 0 }); };

/* ---- actions -------------------------------------------------------------- */
function dqEnsureDay(s) {
  const k = dqDayKey();
  if (s.day && s.day.key === k) return s;
  return { ...s, day: { key: k, count: 0, claimed: [] } };
}
function dqBumpStreak(s) {
  const k = dqDayKey();
  if (s.streak.last === k) return s;
  const count = s.streak.last === dqYesterKey() ? s.streak.count + 1 : 1;
  return { ...s, streak: { count, last: k } };
}
function dqCheckTrophies(s) {
  const have = new Set(s.trophies.map((t) => t.id));
  const won = [];
  DQ_TROPHIES.forEach((t) => { if (!have.has(t.id) && t.test(s)) won.push({ id: t.id, at: dqDayKey() }); });
  if (!won.length) return s;
  won.forEach((w) => { const def = DQ_TROPHIES.find((t) => t.id === w.id); dqToast("Достижение: " + def.name); });
  return { ...s, trophies: [...s.trophies, ...won] };
}

window.DQActions = {
  toggleQuest(id) {
    const e = DQ_INDEX[id];
    if (!e) return;
    window.DQStore.set((s0) => {
      let s = dqEnsureDay(s0);
      if (s.done[id]) {
        const { [id]: _drop, ...rest } = s.done;
        return { ...s, done: rest, xp: Math.max(0, s.xp - e.quest.xp) };
      }
      s = {
        ...s, xp: s.xp + e.quest.xp,
        done: { ...s.done, [id]: { at: dqDayKey() } },
        day: { ...s.day, count: s.day.count + 1 },
      };
      s = dqBumpStreak(s);
      s = dqCheckTrophies(s);
      return s;
    });
    const nowDone = window.DQStore.get().done[id];
    if (nowDone) dqToast("Задача закрыта", e.quest.xp);
  },
  claimDaily(did) {
    const d = DQ_DAILY.find((x) => x.id === did);
    if (!d) return;
    window.DQStore.set((s0) => {
      let s = dqEnsureDay(s0);
      if (s.day.claimed.includes(did) || dqQuestProgress(s, d) < d.target) return s;
      s = { ...s, xp: s.xp + d.bonus, day: { ...s.day, claimed: [...s.day.claimed, did] } };
      return dqCheckTrophies(s);
    });
    dqToast("Задача дня выполнена", d.bonus);
  },
  claimSecret(id) {
    const def = DQ_SECRETS.find((x) => x.id === id);
    if (!def) return;
    window.DQStore.set((s0) => {
      if (s0.secrets.some((x) => x.id === id)) return s0;
      return { ...s0, secrets: [...s0.secrets, { id, at: dqDayKey() }] };
    });
    dqToast("Секретная ачивка: " + def.name);
  },
  reset() {
    /* set() ниже сам перезапишет кэш и серверную запись */
    window.DQStore.set(dqMakeSeed());
    dqToast("Design Quest сброшен");
  },
};

window.DQActs = DQ_ACTS;
window.DQRanks = DQ_RANKS;
window.DQDaily = DQ_DAILY;
window.DQTrophies = DQ_TROPHIES;
window.DQSecrets = DQ_SECRETS;
window.DQRules = DQ_RULES;
