/* ============================================================
   unityquest/unityquest-data.jsx — независимая подсистема
   «Unity Quest: Путь от Нуба до Мастера». 10 актов, 300 задач.
   Основные (⚔️) и побочные (🎲) квесты — по 100 XP, боссы (👹) — 200 XP.
   Уровень = floor(xp/1000)+1 (без именных званий, как в источнике).
   Боссов НЕЛЬЗЯ пропустить: всё после каждого босса (вплоть до
   следующего) требует его — реализовано цепочкой requires по всему
   списку квестов (в двух актах — I и IX — по два босса подряд).
   Свой store (localStorage "unityquest-state"). Полностью независим
   от остальной части планера и от подсистем aiquests/designquest.
   Exposes: UQ_ACTS, UQ_DAILY, UQ_TROPHIES, UQStore, useUQ, UQActions, UQLookup.
   ============================================================ */
const React = window.React;

const UQ_ACTS = [
  { roman: 'I', title: 'Пробуждение', trophy: { id: 'act1', name: 'Пробуждённый', desc: 'ты больше не мирный житель' },
    quests: [
      { num: 1, kind: 'main', title: 'Установка Unity Hub', desc: 'Установи Unity Hub и последнюю LTS-версию Unity. Сделай скриншот — это твоё «свидетельство о рождении» как разработчика.' },
      { num: 2, kind: 'main', title: 'Первый проект и студия', desc: 'Создай первый проект и назови его именем несуществующей игровой студии. Придумай ей девиз.' },
      { num: 3, kind: 'main', title: 'Разведка интерфейса редактора', desc: 'Проведи 15 минут, просто таская окна редактора. Сломай layout полностью, а потом верни через Window → Layouts.' },
      { num: 4, kind: 'main', title: 'Пять главных окон редактора', desc: 'Найди в редакторе 5 окон: Scene, Game, Hierarchy, Inspector, Project. Объясни вслух (да, вслух!) воображаемой бабушке, что делает каждое.' },
      { num: 5, kind: 'main', title: 'Первый куб-NPC', desc: 'Создай куб. Поздравляю, это твой первый NPC. Дай ему имя в Hierarchy.' },
      { num: 6, kind: 'main', title: 'Существо из примитивов', desc: 'Построй из примитивов (кубы, сферы, цилиндры) снеговика. Или робота. Или снеговика-робота.' },
      { num: 7, kind: 'main', title: 'Навигация по сцене', desc: 'Освой навигацию в Scene: облети свою постройку со всех сторон, используя Alt+мышь, WASD в flythrough-режиме и клавишу F.' },
      { num: 8, kind: 'side', title: 'Кнопки верхнего тулбара', desc: 'Найди секрет: что произойдёт, если нажать на иконку Unity-персонажа... то есть, изучи, что делают все кнопки тулбара сверху.' },
      { num: 9, kind: 'main', title: 'Трансформация тремя способами', desc: 'Поменяй позицию, поворот и масштаб объекта тремя способами: гизмо в сцене, Inspector, горячие клавиши W/E/R.' },
      { num: 10, kind: 'main', title: 'Материалы и палитра', desc: 'Создай материал кислотно-розового цвета и покрась им всё в сцене. Наслаждайся кошмаром. Потом сделай нормальную палитру.' },
      { num: 11, kind: 'main', title: 'Планетная система в иерархии', desc: 'Разберись с иерархией: сделай «планетную систему» — сфера-солнце, вокруг неё дочерние сферы-планеты. Покрути родителя.' },
      { num: 12, kind: 'side', title: 'Удаление папки Library', desc: 'Испорти проект: удали папку Library и открой проект снова. Пойми, почему ничего страшного не случилось (и что вообще лежит в папках проекта).' },
      { num: 13, kind: 'main', title: 'Ассет из Package Manager', desc: 'Установи из Package Manager любой бесплатный ассет из Asset Store и добавь его в сцену.' },
      { num: 14, kind: 'main', title: 'Три типа света', desc: 'Настрой свет: добавь Point Light, Spot Light и Directional Light. Сделай «сцену допроса» с одной лампой над кубом.' },
      { num: 15, boss: true, title: 'БОСС «Хранитель Сцены»', desc: 'Построй из примитивов комнату мечты: стены, пол, мебель, освещение, материалы. Минимум 20 объектов, грамотная иерархия, ни одного объекта с именем "Cube (13)".' },
      { num: 16, kind: 'main', title: 'Настройка IDE', desc: 'Установи Visual Studio или Rider, привяжи к Unity. Открой любой скрипт двойным кликом — редактор должен запуститься сам.' },
      { num: 17, kind: 'main', title: 'Первый скрипт HelloWorld', desc: 'Создай первый скрипт HelloWorld.cs, повесь на объект и выведи в консоль фразу, которую сказал бы твой куб, если бы ожил.' },
      { num: 18, kind: 'main', title: 'Log, Warning и Error', desc: 'Разберись с Debug.Log: выведи сообщение обычное, предупреждение (LogWarning) и ошибку (LogError). Полюбуйся цветами в консоли.' },
      { num: 19, kind: 'main', title: 'Awake, Start, Update', desc: 'Пойми разницу между Awake, Start и Update: выведи лог из каждого и посчитай, сколько раз Update вызовется за 5 секунд.' },
      { num: 20, kind: 'side', title: 'Спам-апокалипсис в консоли', desc: 'Устрой «спам-апокалипсис»: выведи Debug.Log в Update без ограничений, посмотри на консоль, ужаснись, добавь кнопку Collapse.' },
      { num: 21, kind: 'main', title: 'Вращение через transform.Rotate', desc: 'Заставь куб вращаться через transform.Rotate. Подбери скорость так, чтобы он крутился ровно как гриль-курица.' },
      { num: 22, kind: 'main', title: 'Time.deltaTime', desc: 'Пойми Time.deltaTime: сделай движение куба и объясни (снова вслух!), почему без deltaTime игра будет вести себя по-разному на разных ПК.' },
      { num: 23, kind: 'main', title: 'Движение через Input.GetAxis', desc: 'Считай ввод: заставь объект двигаться стрелками с помощью Input.GetAxis.' },
      { num: 24, kind: 'main', title: 'Телепорт по Random.Range', desc: 'Сделай «телепорт»: по нажатию пробела объект прыгает в случайную точку (Random.Range).' },
      { num: 25, kind: 'side', title: 'Нервный куб', desc: 'Создай «нервный куб»: объект, который дрожит, меняет цвет каждую секунду и убегает от точки (0,0,0).' },
      { num: 26, kind: 'main', title: 'Публичные переменные в Inspector', desc: 'Освой публичные переменные: вынеси скорость движения в Inspector и меняй её прямо во время игры. Заметь, что после остановки Play значение откатывается!' },
      { num: 27, kind: 'main', title: 'SerializeField vs public', desc: 'Разберись с [SerializeField] и private — пойми, почему торчащие наружу public-поля это плохой тон.' },
      { num: 28, kind: 'main', title: 'Счётчик кликов до 100', desc: 'Сделай счётчик: каждый клик мышью по экрану увеличивает число в консоли. Доведи до 100 кликов. Да, вручную. Это урок терпения.' },
      { num: 29, kind: 'side', title: 'Секретный код-пасхалка', desc: 'Пасхалка: сделай так, чтобы при вводе секретного слова (последовательности клавиш) в консоль выводилось «ЧИТ АКТИВИРОВАН».' },
      { num: 30, boss: true, title: 'БОСС «Первое Творение»', desc: 'Сцена-аквариум: 10 рыб-примитивов плавают с разной скоростью, разворачиваются у стенок, каждая со своим цветом. Всё управляется одним скриптом с настройками в Inspector.' },
    ]},
  { roman: 'II', title: 'Магия C#', trophy: { id: 'act2', name: 'Заклинатель', desc: 'ты видишь Матрицу... ну, почти' },
    quests: [
      { num: 31, kind: 'main', title: 'Паспорт персонажа', desc: 'Выучи типы данных: создай «паспорт персонажа» — скрипт с именем (string), возрастом (int), ростом (float) и признаком «жив» (bool). Выведи всё красиво в одну строку через $-интерполяцию.' },
      { num: 32, kind: 'main', title: 'Фейс-контроль клуба (if/else)', desc: 'Освой if/else: напиши «фейс-контроль клуба» — пускает в консоли только персонажей старше 18 и не в кроксах (bool wearsCrocs).' },
      { num: 33, kind: 'main', title: 'Автомат с напитками (switch)', desc: 'Освой switch: сделай «автомат с напитками» — по номеру выдаёт название напитка, на несуществующий номер ругается.' },
      { num: 34, kind: 'main', title: 'Ёлка из звёздочек (циклы)', desc: 'Циклы: выведи в консоль ёлку из звёздочек высотой N строк. N задаётся в Inspector.' },
      { num: 35, kind: 'main', title: 'Список на увольнение (массивы)', desc: 'Массивы: создай массив из 10 имён врагов и выведи их «список на увольнение» в цикле.' },
      { num: 36, kind: 'main', title: 'Инвентарь на List', desc: 'List: сделай «инвентарь» — по клавише добавляется случайный предмет из заготовок, по другой — удаляется последний, по третьей — печатается всё содержимое.' },
      { num: 37, kind: 'side', title: 'Генератор оправданий', desc: 'Напиши «генератор оправданий» — случайно комбинирует начала и концы фраз из двух массивов: «Я не сделал квест, потому что...» + «...мой кот удалил проект».' },
      { num: 38, kind: 'main', title: 'Методы PrintName/PrintStats', desc: 'Методы: разбей код «паспорта» на методы PrintName(), PrintStats(). Пойми параметры и возвращаемые значения — напиши метод, который принимает два числа и возвращает урон.' },
      { num: 39, kind: 'main', title: 'CalculateDamage с критами', desc: 'Напиши метод CalculateDamage с критами: базовый урон, шанс крита 20%, крит умножает на 2. Прогони 20 атак в цикле.' },
      { num: 40, kind: 'main', title: 'Класс Enemy без MonoBehaviour', desc: 'Классы: создай класс Enemy (не MonoBehaviour!) с полями и методом TakeDamage. Создай трёх врагов и избей их в цикле до нуля HP.' },
      { num: 41, kind: 'main', title: 'Конструктор Enemy', desc: 'Конструкторы: добавь Enemy конструктор, чтобы создавать врагов одной строкой: new Enemy("Гоблин", 50).' },
      { num: 42, kind: 'side', title: 'Турнир двух Fighter', desc: 'Устрой «турнир»: два объекта класса Fighter по очереди бьют друг друга случайным уроном, лог боя в консоль, объяви победителя.' },
      { num: 43, kind: 'main', title: 'Наследование Animal→Dog/Cat', desc: 'Наследование: создай базовый класс Animal и наследников Dog, Cat со своим звуком. Пойми virtual/override.' },
      { num: 44, kind: 'main', title: 'Полиморфизм в List<Animal>', desc: 'Полиморфизм: положи собак и котов в один List<Animal> и заставь всех «говорить» в цикле. Осознай мощь.' },
      { num: 45, kind: 'main', title: 'Свойства get/set для HP', desc: 'Свойства (get/set): сделай HP, которое нельзя опустить ниже 0 и поднять выше максимума — сеттер сам обрезает значения.' },
      { num: 46, kind: 'main', title: 'Enum ItemRarity', desc: 'Enum: создай enum ItemRarity (Common, Rare, Epic, Legendary) и метод, возвращающий цвет по редкости.' },
      { num: 47, kind: 'main', title: 'Статический счётчик врагов', desc: 'Разберись со static: создай статический счётчик всех созданных врагов — каждый новый Enemy увеличивает его в конструкторе.' },
      { num: 48, kind: 'side', title: 'Класс «Тамагочи»', desc: 'Напиши класс «Тамагочи»: голод растёт со временем в Update, кнопками кормишь и играешь, при голоде 100 — печальный лог.' },
      { num: 49, kind: 'main', title: 'NullReferenceException намеренно', desc: 'Пойми null: намеренно вызови NullReferenceException, прочитай стектрейс, найди строку, исправь. Это самая частая ошибка в твоей будущей жизни.' },
      { num: 50, kind: 'main', title: 'Dictionary «прайс-лист лавки»', desc: 'Словари: создай Dictionary<string, int> «прайс-лист лавки» и метод покупки, который проверяет наличие товара и хватает ли золота.' },
      { num: 51, kind: 'main', title: 'Светофор на корутине', desc: 'Корутины: сделай светофор — объект меняет цвет красный→жёлтый→зелёный с паузами через yield return WaitForSeconds.' },
      { num: 52, kind: 'main', title: 'Бомба с таймером', desc: 'Сделай «бомбу с таймером»: корутина ведёт обратный отсчёт от 5 в консоли, потом объект исчезает (Destroy).' },
      { num: 53, kind: 'side', title: 'Печатная машинка текста', desc: 'Создай «печатную машинку»: текст появляется в консоли по одной букве через корутину, как диалог в старых RPG.' },
      { num: 54, kind: 'main', title: 'Instantiate префаба по кнопке', desc: 'Instantiate: по нажатию кнопки создавай клон префаба в случайной точке. Сначала создай сам префаб и пойми, что это такое.' },
      { num: 55, kind: 'main', title: 'Связь префаба и инстанса', desc: 'Пойми связь префаба и инстанса: измени префаб — увидь изменения на всех копиях. Сделай Override на одной копии.' },
      { num: 56, kind: 'main', title: 'Destroy с задержкой — дождь из шаров', desc: 'Destroy с задержкой: спавни сферы, которые живут 3 секунды и исчезают. Устрой «дождь из шаров».' },
      { num: 57, kind: 'main', title: 'GetComponent между скриптами', desc: 'GetComponent: из одного скрипта поменяй цвет Renderer другого объекта и выключи его коллайдер.' },
      { num: 58, kind: 'side', title: 'Менеджер хаоса FindObjectsOfType', desc: 'Напиши «менеджер хаоса»: скрипт, который находит ВСЕ объекты в сцене (FindObjectsOfType) и раскрашивает их случайно по нажатию клавиши.' },
      { num: 59, kind: 'main', title: 'Рефакторинг худшего скрипта', desc: 'Отрефактори худший из своих скриптов: понятные имена, методы вместо простыни, комментарии только там, где без них никак.' },
      { num: 60, boss: true, title: 'БОСС «Архимаг Синтаксис»', desc: 'Консольная RPG внутри Unity: класс Player, класс Enemy, инвентарь-List, магазин-Dictionary, бой по нажатию клавиш, криты, лечение зельями, лог боя. Ни одной 3D-модели — только чистый код и консоль.' },
    ]},
  { roman: 'III', title: 'Законы Физики', trophy: { id: 'act3', name: 'Ньютон одобряет', desc: 'яблоко падает по твоим правилам' },
    quests: [
      { num: 61, kind: 'main', title: 'Rigidbody и гравитация', desc: 'Добавь Rigidbody кубу и урони его на плоскость. Поздравляю, ты создал гравитацию.' },
      { num: 62, kind: 'main', title: 'Башня из кубов и снос сферой', desc: 'Построй башню из 15 кубов с Rigidbody и снеси её брошенной сферой (AddForce). Запиши гифку разрушения.' },
      { num: 63, kind: 'main', title: 'Force vs Impulse vs VelocityChange', desc: 'Пойми разницу Force / Impulse / VelocityChange: запусти три шара тремя режимами и сравни поведение.' },
      { num: 64, kind: 'main', title: 'Прыжок с проверкой земли (Raycast)', desc: 'Сделай прыжок «как в играх»: AddForce вверх по пробелу, но только если объект стоит на земле (проверка через Raycast вниз).' },
      { num: 65, kind: 'main', title: 'Update vs FixedUpdate', desc: 'Пойми разницу Update vs FixedUpdate: перенеси физику в FixedUpdate и объясни резиновой уточке, зачем.' },
      { num: 66, kind: 'side', title: 'Пушка-катапульта', desc: 'Создай «пушку-катапульту»: зажал кнопку — сила растёт, отпустил — снаряд летит. Добей мишень на другом конце сцены.' },
      { num: 67, kind: 'main', title: 'Физические материалы: мяч и лёд', desc: 'Физические материалы: сделай прыгучий мяч (Bounciness) и ледяную горку (Friction = 0). Скати мяч с горки.' },
      { num: 68, kind: 'main', title: 'Триггеры OnTriggerEnter/Exit', desc: 'Triggers: сделай зону, при входе в которую объект красится в зелёный (OnTriggerEnter), а при выходе — обратно (OnTriggerExit).' },
      { num: 69, kind: 'main', title: 'Collisions и сила удара', desc: 'Collisions: выведи в консоль имя всего, обо что ударился твой шар, и силу удара (collision.relativeVelocity).' },
      { num: 70, kind: 'main', title: 'Лава-уничтожитель', desc: 'Сделай «лаву»: пол, при касании которого объекты уничтожаются с драматичным логом.' },
      { num: 71, kind: 'side', title: 'Голдберг-машина', desc: 'Собери «Голдберг-машину»: шар катится → сбивает домино → домино толкает рычаг → рычаг запускает второй шар. Минимум 4 этапа цепной реакции.' },
      { num: 72, kind: 'main', title: 'Raycast из камеры в точку клика', desc: 'Raycast: пусти луч из камеры в точку клика мыши и красный Debug.DrawRay. Кликнутый объект пусть подпрыгивает.' },
      { num: 73, kind: 'main', title: 'Лазерная указка', desc: 'Сделай «лазерную указку»: луч из объекта упирается в препятствия, точка попадания подсвечивается маленькой сферой.' },
      { num: 74, kind: 'main', title: 'Layers и LayerMask для луча', desc: 'Layers и LayerMask: сделай так, чтобы луч видел врагов, но игнорировал стеклянные стены.' },
      { num: 75, kind: 'main', title: 'Цепь на HingeJoint', desc: 'Joints: собери цепь из 5 сфер на HingeJoint и подвесь её. Покачай. Помедитируй.' },
      { num: 76, kind: 'side', title: 'Кран-машина с клешнёй', desc: 'Сделай «кран-машину» с игрушками: подвешенная на joints клешня, которую можно двигать и опускать. Вытащи приз.' },
      { num: 77, kind: 'main', title: 'Контроллер персонажа-капсулы', desc: 'Создай контроллер персонажа-капсулы: движение WASD через Rigidbody.velocity, прыжок, поворот в сторону движения.' },
      { num: 78, kind: 'main', title: 'Камера от третьего лица (Lerp)', desc: 'Прикрути камеру от третьего лица, следующую за капсулой плавно (Vector3.Lerp в LateUpdate).' },
      { num: 79, kind: 'main', title: 'Lerp vs MoveTowards на дверях', desc: 'Пойми Lerp vs MoveTowards: сделай две двери — одна открывается «пружинисто» (Lerp), другая равномерно (MoveTowards).' },
      { num: 80, kind: 'main', title: 'Полоса препятствий', desc: 'Собери полосу препятствий: движущиеся платформы, вращающиеся «вентиляторы»-выбивалы, качающийся маятник. Пройди её своей капсулой.' },
      { num: 81, kind: 'side', title: 'Чекпоинты на полосе препятствий', desc: 'Добавь на полосу чекпоинты: упал в пропасть — возродился на последнем триггере-флаге.' },
      { num: 82, kind: 'main', title: 'Взрыв гранаты (OverlapSphere)', desc: 'OverlapSphere: сделай «взрыв гранаты» — все Rigidbody в радиусе разлетаются (AddExplosionForce).' },
      { num: 83, kind: 'main', title: 'Магнит и репеллент', desc: 'Сделай магнит: объект, притягивающий к себе все шары в радиусе. Потом инвертируй — получи «репеллент».' },
      { num: 84, kind: 'main', title: 'Вагонетка на констрейнтах', desc: 'Изучи Rigidbody-констрейнты: сделай «вагонетку», которая ездит только по одной оси и не заваливается.' },
      { num: 85, kind: 'side', title: 'Боулинг с кеглями', desc: 'Построй боулинг: 10 кеглей, шар, бросок мышью, подсчёт сбитых кеглей в консоль. Выбей страйк — получи достижение лично от себя.' },
      { num: 86, kind: 'main', title: 'Интерполяция и Continuous Collision', desc: 'Пойми интерполяцию Rigidbody (Interpolate) и Collision Detection (Continuous): почему быстрая пуля пролетает сквозь стену и как это чинить.' },
      { num: 87, kind: 'main', title: 'Пуля без проскакивания сквозь стену', desc: 'Сделай пулю правильно: быстрый снаряд с Continuous Dynamic, который никогда не проскакивает сквозь тонкую мишень.' },
      { num: 88, kind: 'main', title: 'Character Controller vs Rigidbody', desc: 'Character Controller: пересобери своего персонажа на компоненте CharacterController и сравни ощущения с Rigidbody-версией.' },
      { num: 89, kind: 'side', title: 'Зомби-толпа', desc: 'Создай «зомби-толпу»: 20 капсул медленно ползут к игроку, толкаясь друг с другом. Убеги.' },
      { num: 90, boss: true, title: 'БОСС «Гравитон»', desc: 'Мини-гольф: минимум 3 лунки, удар с силой от зажатия мыши, ветер (постоянная сила), препятствия, счётчик ударов, поздравление при попадании в лунку. Физика должна ощущаться приятно.' },
    ]},
  { roman: 'IV', title: 'Первая Настоящая Игра — 2D', trophy: { id: 'act4', name: 'Геймдевелопер', desc: 'теперь это официально' },
    quests: [
      { num: 91, kind: 'main', title: '2D-проект и ортографическая камера', desc: 'Создай 2D-проект (или переключи режим). Пойми, чем 2D-режим отличается: ортографическая камера, спрайты, ось Z.' },
      { num: 92, kind: 'main', title: '2D-пак ассетов (Kenney)', desc: 'Найди бесплатный 2D-пак ассетов (itch.io, Kenney.nl) и импортируй. Kenney — святой человек, запомни это имя.' },
      { num: 93, kind: 'main', title: 'Sorting Layers для спрайтов', desc: 'Разберись с Sprite Renderer: слои сортировки (Sorting Layers), Order in Layer. Сделай так, чтобы игрок был поверх фона, но за передними кустами.' },
      { num: 94, kind: 'main', title: 'Уровень на Tilemap', desc: 'Освой Tilemap: нарисуй уровень тайлами — земля, платформы, декорации. Почувствуй себя художником уровня.' },
      { num: 95, kind: 'main', title: 'Tilemap Collider + Composite Collider', desc: 'Добавь Tilemap Collider + Composite Collider, чтобы уровень стал твёрдым и без швов.' },
      { num: 96, kind: 'main', title: 'Контроллер 2D-персонажа', desc: 'Собери контроллер 2D-персонажа: бег, прыжок на Rigidbody2D, проверка земли через OverlapCircle.' },
      { num: 97, kind: 'main', title: 'Кайот-тайм и буфер прыжка', desc: 'Добавь «кайот-тайм» (0.1 сек после схода с платформы всё ещё можно прыгнуть) и буфер прыжка. Узнай, что так делают ВСЕ хорошие платформеры.' },
      { num: 98, kind: 'side', title: 'Двойной прыжок и рывок', desc: 'Добавь двойной прыжок и рывок (dash) с перезарядкой. Подбери числа, чтобы было «вкусно».' },
      { num: 99, kind: 'main', title: 'Флип спрайта по направлению', desc: 'Настрой флип спрайта: персонаж смотрит туда, куда бежит.' },
      { num: 100, kind: 'main', title: 'Animator: Idle и Run', desc: 'Освой Animator: сделай анимации Idle и Run из спрайтов, переключай их по параметру Speed.' },
      { num: 101, kind: 'main', title: 'Анимации Jump и Fall', desc: 'Добавь анимации Jump и Fall, переключай по вертикальной скорости. Пойми переходы (Transitions) и условия.' },
      { num: 102, kind: 'side', title: 'Сквош-и-стретч персонажа', desc: 'Сделай «сквош-и-стретч»: персонаж чуть сплющивается при приземлении и вытягивается в прыжке (через масштаб). Мгновенно +100 к сочности.' },
      { num: 103, kind: 'main', title: 'Cinemachine: камера за игроком', desc: 'Cinemachine: поставь виртуальную камеру, следящую за игроком, с мягким демпфированием и Dead Zone.' },
      { num: 104, kind: 'main', title: 'Cinemachine Confiner по границам уровня', desc: 'Ограничь камеру границами уровня (Cinemachine Confiner). Больше никаких взглядов за край мира.' },
      { num: 105, kind: 'main', title: 'Монетки со счётчиком', desc: 'Создай монетки: крутятся (анимация), собираются по триггеру, счётчик в консоли. Разложи 20 штук по уровню.' },
      { num: 106, kind: 'main', title: 'Шипы, урон и неуязвимость', desc: 'Сделай шипы и падение в яму: урон, откидывание игрока, мигание спрайта при неуязвимости (корутина).' },
      { num: 107, kind: 'main', title: 'Система здоровья: 3 сердца', desc: 'Создай систему здоровья игрока: 3 сердца, смерть при нуле, возрождение на старте уровня.' },
      { num: 108, kind: 'main', title: 'Патрулирующий враг', desc: 'Сделай патрулирующего врага: ходит между двумя точками, разворачивается. При прыжке ему на голову — умирает, при касании сбоку — бьёт игрока.' },
      { num: 109, kind: 'side', title: 'Враг-турель', desc: 'Добавь врага-«турель»: сидит на месте и плюётся снарядами в сторону игрока каждые 2 секунды.' },
      { num: 110, kind: 'main', title: 'Движущиеся платформы и парентинг', desc: 'Сделай движущиеся платформы: горизонтальные, вертикальные, и реши классическую проблему «игрок скользит с платформы» (парентинг при контакте).' },
      { num: 111, kind: 'main', title: 'Батут, лестница, одностороння платформа', desc: 'Добавь батут, лестницу и одностороннюю платформу (Platform Effector 2D). Три классических механики за один квест.' },
      { num: 112, kind: 'side', title: 'Своя странная механика', desc: 'Придумай и добавь ОДНУ странную механику, которой ты не видел в туториалах: гравитационные перчатки? липкие стены? монетки-предатели? Твой ход.' },
      { num: 113, kind: 'main', title: 'Ключ и дверь', desc: 'Сделай ключ и дверь: дверь открывается только с ключом в «инвентаре».' },
      { num: 114, kind: 'main', title: 'Финиш уровня (флаг/портал)', desc: 'Собери финиш уровня: флаг/портал, при касании — победная надпись в консоли и остановка управления.' },
      { num: 115, kind: 'main', title: 'Полноценный уровень 1–2 минуты', desc: 'Построй полноценный уровень на 1–2 минуты прохождения: обучение механикам в начале, нарастающая сложность, секретная зона с бонусом.' },
      { num: 116, kind: 'main', title: 'Параллакс-фон в 3 слоя', desc: 'Добавь параллакс-фон: 3 слоя, движущихся с разной скоростью. Глубина из ничего.' },
      { num: 117, kind: 'side', title: 'Живой плейтест уровня', desc: 'Позови живого человека протестировать уровень. Молча смотри, где он застрянет. Запиши 3 наблюдения. НЕ подсказывай — страдай молча.' },
      { num: 118, kind: 'main', title: 'Исправление 3 худших мест уровня', desc: 'По итогам теста исправь 3 худших места уровня.' },
      { num: 119, kind: 'main', title: 'Второй уровень и переход между сценами', desc: 'Добавь второй уровень и переход между сценами (SceneManager.LoadScene) через дверь-портал.' },
      { num: 120, boss: true, title: 'БОСС «Первый Релиз» (мини)', desc: 'Собери билд игры под Windows/Mac, запусти НЕ из редактора, дай сыграть двум людям. Игра не должна крашиться. Это твой первый исполняемый файл — сохрани его навсегда.' },
    ]},
  { roman: 'V', title: 'Сочность — UI, Звук, Полировка', trophy: { id: 'act5', name: 'Шеф-повар', desc: 'ты научился готовить сочно' },
    quests: [
      { num: 121, kind: 'main', title: 'Canvas и режимы рендера', desc: 'Создай Canvas и разберись с режимами рендера (Overlay, Camera, World Space). Помести кнопку в мир как «голограмму».' },
      { num: 122, kind: 'main', title: 'Якоря (Anchors) для HUD', desc: 'Освой якоря (Anchors): сделай HUD, который не разваливается при смене разрешения. Проверь на 5 разных соотношениях сторон.' },
      { num: 123, kind: 'main', title: 'Счётчик монет на TextMeshPro', desc: 'Выведи счётчик монет на экран через TextMeshPro. Забудь про старый Text навсегда.' },
      { num: 124, kind: 'main', title: 'Полоска здоровья (Fill Amount)', desc: 'Сделай полоску здоровья (Image с Fill Amount), плавно убывающую при уроне.' },
      { num: 125, kind: 'side', title: 'Полоска-призрак HP', desc: 'Добавь «полоску-призрак»: белый след за полоской HP, который догоняет её с задержкой — как в файтингах.' },
      { num: 126, kind: 'main', title: 'Главное меню', desc: 'Собери главное меню: заголовок, кнопки «Играть» и «Выход», переход в игровую сцену.' },
      { num: 127, kind: 'main', title: 'Меню паузы и Time.timeScale', desc: 'Сделай меню паузы: Time.timeScale = 0, затемнение, кнопки «Продолжить» и «В меню». Пойми, что ломается при timeScale = 0 (спойлер: анимации UI и корутины с WaitForSeconds).' },
      { num: 128, kind: 'main', title: 'Экраны смерти и победы', desc: 'Создай экран смерти и экран победы со статистикой: монеты, время, количество смертей.' },
      { num: 129, kind: 'side', title: 'Анимированная кнопка', desc: 'Сделай анимированную кнопку: увеличивается при наведении, «проседает» при нажатии, издаёт звук. Кнопка, которую хочется трогать.' },
      { num: 130, kind: 'main', title: 'Всплывающие цифры урона', desc: 'Добавь всплывающие цифры урона над врагами: спавнятся, летят вверх, тают (World Space Canvas или TextMeshPro в мире).' },
      { num: 131, kind: 'main', title: 'AudioSource и звуки действий', desc: 'Освой AudioSource и AudioClip: добавь звуки прыжка, монетки, урона, смерти.' },
      { num: 132, kind: 'main', title: 'Фоновая музыка и DontDestroyOnLoad', desc: 'Добавь фоновую музыку с зацикливанием и НЕ дай ей перезапускаться при смене сцены (первое знакомство с DontDestroyOnLoad).' },
      { num: 133, kind: 'main', title: 'Рандомизация pitch звуков', desc: 'Рандомизируй звуки: каждый прыжок — чуть разный pitch (0.9–1.1). Услышь, насколько живее стало.' },
      { num: 134, kind: 'main', title: 'AudioMixer с ползунками громкости', desc: 'Сделай AudioMixer: раздельные ползунки громкости музыки и эффектов в меню настроек.' },
      { num: 135, kind: 'side', title: 'Свои звуковые эффекты с телефона', desc: 'Запиши 3 звуковых эффекта САМ на телефон: прыжок (хлопок?), монетка (ложка о кружку?), смерть (драматичный вздох). Вставь в игру.' },
      { num: 136, kind: 'main', title: 'Particle System: сбор монетки', desc: 'Освой Particle System: сделай эффект сбора монетки — золотые искры.' },
      { num: 137, kind: 'main', title: 'Пыль из-под ног', desc: 'Сделай пыль из-под ног при беге и приземлении. Частицы — 80% сочности любого платформера.' },
      { num: 138, kind: 'main', title: 'Эффект смерти врага частицами', desc: 'Создай эффект смерти врага: взрыв частиц + исчезновение. Никто не должен умирать скучно.' },
      { num: 139, kind: 'side', title: 'Фейерверк-машина', desc: 'Собери «фейерверк-машину»: по клику в точке экрана взрывается праздничный залп из 3 разных систем частиц.' },
      { num: 140, kind: 'main', title: 'Тряска камеры при уроне', desc: 'Добавь тряску камеры (Cinemachine Impulse или свой скрипт) при уроне и взрывах. Аккуратно: тряска — как соль.' },
      { num: 141, kind: 'main', title: 'Hit-stop при ударе', desc: 'Освой hit-stop: микро-заморозка времени (0.05 сек) в момент удара по врагу. Секретное оружие всех экшенов.' },
      { num: 142, kind: 'main', title: 'Trail Renderer для рывка и снарядов', desc: 'Изучи Trail Renderer: добавь шлейф рывку персонажа и снарядам.' },
      { num: 143, kind: 'main', title: 'Сохранения через PlayerPrefs', desc: 'Сделай простую систему сохранений через PlayerPrefs: рекорд монет, номер пройденного уровня, громкость.' },
      { num: 144, kind: 'main', title: 'Выбор уровня в меню', desc: 'Добавь выбор уровня в меню: пройденные открыты, будущие — с замком.' },
      { num: 145, kind: 'side', title: 'Сундук с наградой', desc: 'Сделай «сундук с наградой»: тряска, пауза-интрига, взрыв частиц, звук фанфар, вылет предмета. Отточи тайминг до мурашек.' },
      { num: 146, kind: 'main', title: 'Твины (DOTween) для UI', desc: 'Освой твины (DOTween или свой Lerp): плавные появления окон UI, подпрыгивание счётчика при сборе монет.' },
      { num: 147, kind: 'main', title: 'Fade-переход между сценами', desc: 'Сделай переход между сценами с затемнением (fade in/out). Резкие переходы — для дикарей.' },
      { num: 148, kind: 'main', title: 'Аудит сочности платформера', desc: 'Проведи «аудит сочности» своего платформера: пройди по чек-листу (звук? частицы? тряска? анимации UI?) и добавь недостающее в 5 местах.' },
      { num: 149, kind: 'side', title: 'Скриншот до/после полировки', desc: 'Сделай скриншот «до/после полировки» одного и того же момента игры. Сохрани — будешь показывать ученикам.' },
      { num: 150, boss: true, title: 'БОСС «Мастер Сочности»', desc: 'Возьми механику «сбор монетки» и доведи до абсолюта: анимация притяжения к игроку, частицы, звук со случайным pitch, подпрыгивание счётчика, комбо-множитель при быстром сборе, лёгкий hit-stop на 10-й монете подряд. Одна механика — но идеальная.' },
    ]},
  { roman: 'VI', title: 'Третье Измерение', trophy: { id: 'act6', name: 'Стереоскоп', desc: 'плоский мир остался позади' },
    quests: [
      { num: 151, kind: 'main', title: '3D-проект на URP', desc: 'Создай 3D-проект на URP. Разберись, что такое Render Pipeline и почему розовые материалы = неправильный шейдер.' },
      { num: 152, kind: 'main', title: 'ProBuilder: дом с окнами', desc: 'Освой ProBuilder: построй дом с дверным проёмом и окнами прямо в редакторе.' },
      { num: 153, kind: 'main', title: 'Импорт 3D-модели извне', desc: 'Импортируй 3D-модель извне (Sketchfab, Kenney): разберись с материалами, текстурами и масштабом при импорте.' },
      { num: 154, kind: 'main', title: 'FPS-контроллер с нуля', desc: 'Собери FPS-контроллер с нуля: движение, мышиный обзор с зажимом угла камеры, прыжок. Без ассетов — руками.' },
      { num: 155, kind: 'main', title: 'Cursor.lockState', desc: 'Разберись с Cursor.lockState: спрячь и заблокируй курсор в игре, освобождай по Escape.' },
      { num: 156, kind: 'side', title: 'Наклон камеры и head bob', desc: 'Добавь наклон камеры при стрейфе и покачивание при ходьбе (head bob). Тонко! Не вызови морскую болезнь.' },
      { num: 157, kind: 'main', title: 'Взаимодействие «глазами» (Raycast+E)', desc: 'Сделай взаимодействие «глазами»: Raycast из центра экрана, подсветка предмета под прицелом, подбор по клавише E.' },
      { num: 158, kind: 'main', title: 'Дверь с анимацией открытия', desc: 'Создай дверь, которая открывается поворотом по анимации/твину при взаимодействии. Добавь скрип.' },
      { num: 159, kind: 'main', title: 'NavMesh: враг преследует игрока', desc: 'Освой NavMesh: запеки навигацию, заставь агента-врага преследовать игрока в лабиринте.' },
      { num: 160, kind: 'main', title: 'Конус зрения врага', desc: 'Сделай врагу «зрение»: конус обзора (угол + Raycast) — видит игрока только перед собой и не сквозь стены.' },
      { num: 161, kind: 'main', title: 'Стейт-машина врага (Patrol/Chase/Attack)', desc: 'Построй стейт-машину врага на enum: Patrol → Chase → Attack → ReturnToPost. Переходы по расстоянию и видимости.' },
      { num: 162, kind: 'side', title: 'Слух врага (OverlapSphere)', desc: 'Добавь врагу «слух»: приседающий игрок бесшумен, бегущий — слышен через OverlapSphere. Первый шаг к стелсу.' },
      { num: 163, kind: 'main', title: 'Animator 3D + Mixamo Blend Tree', desc: 'Освой Animator для 3D: скачай персонажа и анимации с Mixamo, настрой Idle/Walk/Run через Blend Tree.' },
      { num: 164, kind: 'main', title: 'Root Motion vs скриптовое движение', desc: 'Разберись с Root Motion vs скриптовое движение: попробуй оба, пойми, когда какой лучше.' },
      { num: 165, kind: 'main', title: 'Animation Events для шагов', desc: 'Сделай Animation Events: шаги персонажа звучат точно в момент касания ноги земли — событие прямо в клипе анимации.' },
      { num: 166, kind: 'main', title: 'Terrain: остров с горами', desc: 'Освой Terrain: слепи остров — горы, низины, текстуры, трава, деревья.' },
      { num: 167, kind: 'side', title: 'Вода и туман на острове', desc: 'Добавь на остров воду (простая плоскость с шейдером из URP-сэмплов или ассета) и туман (Fog). Атмосфера!' },
      { num: 168, kind: 'main', title: 'Skybox и закатное освещение', desc: 'Настрой Skybox: смени небо, поставь закатное освещение, подгони цвет Directional Light под небо.' },
      { num: 169, kind: 'main', title: 'Lightmapping и запечённый свет', desc: 'Разберись с запечённым светом: Lightmapping — пометь статику, запеки, сравни FPS и картинку с реалтаймом.' },
      { num: 170, kind: 'main', title: 'Light Probes и Reflection Probe', desc: 'Добавь Light Probes для динамических объектов и Reflection Probe для блестящего шара в комнате.' },
      { num: 171, kind: 'main', title: 'Post-Processing: уют vs хоррор', desc: 'Освой Post-Processing (URP Volume): Bloom, Vignette, Color Grading. Сделай два пресета: «уютный вечер» и «тревожный хоррор». Переключай по клавише.' },
      { num: 172, kind: 'side', title: 'Мини-хоррор-коридор со скримером', desc: 'Собери мини-хоррор-коридор: темнота, фонарик (Spot Light на камере) с садящейся батарейкой, скрипящая дверь, ОДИН скример. Испытай на друге.' },
      { num: 173, kind: 'main', title: 'Стрельба хитскан-рейкастом', desc: 'Сделай стрельбу: Raycast-хитскан, дульная вспышка, след попадания на стене (decal или частицы), отдача камеры.' },
      { num: 174, kind: 'main', title: 'Стрельба физическими снарядами', desc: 'Альтернатива: стрельба физическими снарядами. Сравни хитскан и projectile, пойми плюсы обоих.' },
      { num: 175, kind: 'main', title: 'HP врагов и рэгдолл при смерти', desc: 'Добавь врагам HP, реакцию на попадание (вздрагивание) и рэгдолл при смерти. Разберись, как включать рэгдолл поверх Animator.' },
      { num: 176, kind: 'main', title: 'Подбираемое оружие и перезарядка', desc: 'Сделай подбираемое оружие и боеприпасы: пустой магазин — перезарядка по R с анимацией и звуком.' },
      { num: 177, kind: 'side', title: 'Тир с мишенями', desc: 'Сделай тир: мишени выскакивают в случайных местах на 2 секунды, счёт за меткость, таблица рекордов через PlayerPrefs.' },
      { num: 178, kind: 'main', title: 'LOD Groups для дерева', desc: 'Освой LOD Groups: настрой 3 уровня детализации для дерева, посмотри переключение в Scene.' },
      { num: 179, kind: 'main', title: 'Occlusion Culling в комнате', desc: 'Добавь Occlusion Culling в свою комнатную сцену и увидь в Stats, как перестают рисоваться невидимые объекты.' },
      { num: 180, boss: true, title: 'БОСС «Хранитель Глубины»', desc: 'Мини-игра «Ограбление музея»: 3D-помещение, 2 патрулирующих охранника со зрением и слухом, ценный артефакт, сигнализация с лазерами, выход. Укради артефакт и сбеги незамеченным. Стелс, свет, звук, AI — всё, что выучил.' },
    ]},
  { roman: 'VII', title: 'Архитектор Систем', trophy: { id: 'act7', name: 'Строитель Соборов', desc: 'сеньоры одобрительно кивают' },
    quests: [
      { num: 181, kind: 'main', title: 'Ревизия своего старого кода', desc: 'Прочитай свой код двухмесячной давности. Ужаснись. Выпиши 5 вещей, которые сделал бы иначе. Это и есть рост.' },
      { num: 182, kind: 'main', title: 'События C# (event/Action)', desc: 'Освой события C# (event/Action): монетка не знает про UI — она просто кричит OnCoinCollected, а UI сам подписан и обновляется.' },
      { num: 183, kind: 'main', title: 'Здоровье на событиях OnDamaged/OnDied', desc: 'Переведи систему здоровья на события: OnDamaged, OnDied. Отпишись правильно (OnDisable), избеги утечки.' },
      { num: 184, kind: 'main', title: 'Singleton GameManager', desc: 'Сделай Singleton GameManager: очки, состояние игры, доступ отовсюду. А потом прочитай, почему синглтоны ругают, и сформулируй своё мнение.' },
      { num: 185, kind: 'main', title: 'ScriptableObject-конфиги оружия', desc: 'Освой ScriptableObject: создай ассеты-конфиги оружия (урон, скорострельность, звук, иконка). Новое оружие = новый ассет без кода.' },
      { num: 186, kind: 'main', title: 'База предметов на ScriptableObject', desc: 'Построй базу данных предметов на ScriptableObject: 10 предметов с редкостью, ценой, описанием. Инвентарь теперь хранит ссылки на них.' },
      { num: 187, kind: 'side', title: 'Ивент-канал на ScriptableObject', desc: 'Сделай «ивент-канал» на ScriptableObject: событие-ассет, на которое подписываются объекты из разных сцен. Мозг взорвётся, но это того стоит.' },
      { num: 188, kind: 'main', title: 'Интерфейс IDamageable', desc: 'Интерфейсы: создай IDamageable и научи пулю бить ВСЁ — врагов, бочки, стёкла — без единого if по тегу.' },
      { num: 189, kind: 'main', title: 'Интерфейс IInteractable', desc: 'Создай IInteractable: двери, сундуки, рычаги, NPC — всё открывается одной кнопкой E через один скрипт взаимодействия.' },
      { num: 190, kind: 'main', title: 'Object Pooling пуль', desc: 'Освой Object Pooling: пул пуль вместо Instantiate/Destroy. Замерь разницу в Profiler при 200 выстрелах в секунду.' },
      { num: 191, kind: 'main', title: 'Generic ObjectPool<T>', desc: 'Напиши универсальный generic-пул ObjectPool<T> или освой встроенный UnityEngine.Pool. Переведи на него частицы и врагов.' },
      { num: 192, kind: 'main', title: 'Композиция вместо god-object игрока', desc: 'Пойми композицию против наследования: разбери «god-object» игрока на компоненты — Health, Mover, Shooter, InputReader.' },
      { num: 193, kind: 'side', title: 'Враг-конструктор из компонентов', desc: 'Собери врага-«конструктор»: комбинируя компоненты (летает/ходит, стреляет/кусает, трус/агрессор), создай 4 разных врага без нового кода.' },
      { num: 194, kind: 'main', title: 'Новая Input System и геймпад', desc: 'Освой новую Input System: переведи управление на Input Actions, добавь поддержку геймпада за 10 минут и офигей, как это просто.' },
      { num: 195, kind: 'main', title: 'Переназначение клавиш (rebinding)', desc: 'Сделай переназначение клавиш в меню настроек через Input System rebinding.' },
      { num: 196, kind: 'main', title: 'Сохранения в JSON', desc: 'Напиши сохранения в JSON: сериализуй класс SaveData (позиция, инвентарь, прогресс) в файл через JsonUtility, загрузи обратно.' },
      { num: 197, kind: 'main', title: 'Слоты сохранений и автосейв', desc: 'Добавь три слота сохранений и автосейв на чекпоинтах. Подумай: что случится, если игрок выключит игру во время записи файла? Защитись.' },
      { num: 198, kind: 'side', title: 'Пасхалка в файле сохранения', desc: 'Сделай «пасхалку для археологов»: спрячь в файле сохранения зашифрованное послание будущему себе.' },
      { num: 199, kind: 'main', title: 'Async/await и LoadSceneAsync', desc: 'Освой async/await в Unity (или UniTask): загрузка сцены с прогресс-баром через LoadSceneAsync.' },
      { num: 200, kind: 'main', title: 'Система диалогов на ScriptableObject', desc: 'Построй систему диалогов: ScriptableObject с репликами, окно с печатающимся текстом, варианты ответов, ветвление.' },
      { num: 201, kind: 'main', title: 'Система квестов на данных', desc: 'Создай систему квестов: цели «убей N», «собери N», «дойди до точки», трекер на экране, награда. Всё на данных, не хардкодом.' },
      { num: 202, kind: 'main', title: 'Простая система прокачки', desc: 'Сделай простую систему прокачки: опыт за врагов, уровни, 3 характеристики на выбор при левелапе.' },
      { num: 203, kind: 'side', title: 'Магазинчик торговца', desc: 'Собери «магазинчик торговца»: покупка/продажа из общей базы предметов, у торговца конечное золото, цены зависят от редкости.' },
      { num: 204, kind: 'main', title: 'Assembly Definitions', desc: 'Освой Assembly Definitions: раздели проект на сборки (Core, Gameplay, UI) и заметь, как ускорилась компиляция.' },
      { num: 205, kind: 'main', title: 'Git в Unity-проекте', desc: 'Подключи Git к Unity-проекту: правильный .gitignore, настрой сериализацию ассетов в текст, сделай 5 осмысленных коммитов.' },
      { num: 206, kind: 'main', title: 'Катастрофа и откат через Git', desc: 'Устрой «катастрофу»: сломай проект (удали важный скрипт, испорти сцену) и откатись через Git. Прочувствуй спасение.' },
      { num: 207, kind: 'main', title: 'Кастомный инспектор/кнопка', desc: 'Напиши кастомный атрибут-кнопку или простой кастомный инспектор: кнопка «Убить всех врагов» прямо в Inspector для дебага.' },
      { num: 208, kind: 'main', title: 'Дебаг-панель в игре', desc: 'Сделай дебаг-панель в игре: показ FPS, телепорт к чекпоинтам, бессмертие, выдача предметов. Открывается по тильде.' },
      { num: 209, kind: 'side', title: 'Editor-скрипт расстановки деревьев', desc: 'Напиши Editor-скрипт, который одним пунктом меню расставляет 100 деревьев по террейну со случайным поворотом и масштабом.' },
      { num: 210, boss: true, title: 'БОСС «Архитектор»', desc: 'Возьми свой платформер из Акта IV и отрефактори по-взрослому: события вместо прямых ссылок, конфиги на ScriptableObject, пулинг снарядов, новая Input System, JSON-сейвы, Git-история рефакторинга. Игра выглядит так же — но внутри теперь кафедральный собор, а не сарай.' },
    ]},
  { roman: 'VIII', title: 'Тайные Искусства', trophy: { id: 'act8', name: 'Техномант', desc: 'ты видишь сквозь движок' },
    quests: [
      { num: 211, kind: 'main', title: 'Первый шейдер в Shader Graph', desc: 'Открой Shader Graph и сделай свой первый шейдер: материал, плавно переливающийся двумя цветами через Lerp и Time.' },
      { num: 212, kind: 'main', title: 'Шейдер растворения (dissolve)', desc: 'Сделай шейдер растворения (dissolve): враг при смерти «сгорает» по текстуре шума с раскалённой кромкой. Классика, которую спрашивают все.' },
      { num: 213, kind: 'main', title: 'Шейдер воды', desc: 'Создай шейдер воды: смещение вершин синусоидой + прозрачность + блики. Пусти по своему острову.' },
      { num: 214, kind: 'main', title: 'Hit-flash шейдером', desc: 'Сделай hit-flash: спрайт/модель вспыхивает белым при уроне (через шейдер, а не замену материала).' },
      { num: 215, kind: 'side', title: 'Голограмма (fresnel + развёртка)', desc: 'Создай «голограмму»: полосы развёртки, мерцание, свечение по краям (fresnel). Помести в сцену как призрака.' },
      { num: 216, kind: 'main', title: 'Шейдер колыхания травы', desc: 'Освой шейдер колыхания травы/флагов: вершины качаются от «ветра». Оживи растительность на острове.' },
      { num: 217, kind: 'main', title: 'Fresnel-щит вокруг персонажа', desc: 'Разберись с Fresnel-эффектом и сделай силовой щит вокруг персонажа, светящийся по краям и вспыхивающий в точке попадания.' },
      { num: 218, kind: 'side', title: 'Шейдер сканера как в Метроиде', desc: 'Сделай шейдер «сканера из Метроида»: волна проходит по миру от игрока, подсвечивая контуры объектов.' },
      { num: 219, kind: 'main', title: 'Profiler: самое дорогое место', desc: 'Открой Profiler и найди самое дорогое место в своей игре. CPU или GPU? Rendering или Scripts? Научись читать этот граф.' },
      { num: 220, kind: 'main', title: 'Стресс-тест FPS и оптимизация', desc: 'Устрой стресс-тест: спавни объекты, пока FPS не упадёт до 30. Запиши число. Теперь оптимизируй и побей рекорд вдвое.' },
      { num: 221, kind: 'main', title: 'Frame Debugger по draw calls', desc: 'Изучи Frame Debugger: пройди по draw calls своей сцены кадр за кадром. Найди, что рисуется зря.' },
      { num: 222, kind: 'main', title: 'Батчинг: static batching и GPU instancing', desc: 'Освой батчинг: static batching, GPU instancing на материалах. Сравни число draw calls до и после.' },
      { num: 223, kind: 'main', title: 'Поиск утечки и пиков GC', desc: 'Найди утечку: намеренно создай мусор (конкатенация строк в Update, LINQ в цикле), увидь пики GC в Profiler, исправь.' },
      { num: 224, kind: 'side', title: 'Разбор чужого проекта на проблемы', desc: 'Сыграй в «оптимизатора-детектива»: скачай чужой открытый Unity-проект с GitHub и найди в нём 3 проблемы производительности.' },
      { num: 225, kind: 'main', title: 'Процедурный лабиринт из префабов', desc: 'Освой процедурную генерацию: скрипт, который строит лабиринт из префабов при запуске. Каждый запуск — новый лабиринт.' },
      { num: 226, kind: 'main', title: 'Ландшафт через Perlin Noise', desc: 'Сгенерируй ландшафт через Perlin Noise: карта высот на Terrain или сетке. Поиграй с octaves — от холмов до гор.' },
      { num: 227, kind: 'main', title: 'Генератор подземелий', desc: 'Сделай генератор подземелий: комнаты + коридоры, гарантированный путь от входа до выхода, спавн врагов и лута по правилам.' },
      { num: 228, kind: 'side', title: 'Генератор имён и лора предметов', desc: 'Напиши «генератор имён» для NPC из слогов и «генератор лора» предметов: «Меч [прилагательное] [существительное] +[число]». Легендарный Меч Вопиющей Селёдки +3 ждёт.' },
      { num: 229, kind: 'main', title: 'Mesh API: квадрат из кода', desc: 'Освой Mesh API: создай меш из кода — сначала квадрат из двух треугольников, поняв вершины, треугольники и UV.' },
      { num: 230, kind: 'main', title: 'Процедурный меш-ландшафт', desc: 'Сгенерируй процедурный меш-ландшафт из сетки вершин с высотами из шума. Раскрась вершины по высоте: песок→трава→снег.' },
      { num: 231, kind: 'main', title: 'Оптимизация 2D/UI (Sprite Atlas)', desc: 'Разберись с производительностью 2D/UI: атласы спрайтов (Sprite Atlas), почему один Canvas на всё — плохо, раздели UI на статичный и динамичный Canvas.' },
      { num: 232, kind: 'main', title: 'Юнит-тесты для инвентаря', desc: 'Сделай юнит-тест: Test Runner, напиши 5 тестов на свой класс инвентаря (добавление, удаление, переполнение). Сломай код — увидь красный тест.' },
      { num: 233, kind: 'side', title: 'Play Mode тест-гремлин', desc: 'Напиши тест-«гремлина»: Play Mode тест, где виртуальный игрок 60 секунд жмёт случайные кнопки. Игра не должна упасть.' },
      { num: 234, kind: 'main', title: 'Behaviour Tree вместо enum-стейтов', desc: 'Освой поведение через Behaviour Tree (ассет или свой простой): переведи AI охранника с enum-стейтов на дерево. Сравни гибкость.' },
      { num: 235, kind: 'main', title: 'Боссфайт с фазами', desc: 'Сделай боссфайт с фазами: на 100% HP — одни атаки, ниже 50% — ярость с новыми паттернами, предупреждающие «телеграфы» перед ударами.' },
      { num: 236, kind: 'main', title: 'Timeline: катсцена с боссом', desc: 'Изучи Timeline: сделай катсцену — камера облетает босса, тот рычит, титр с именем, возврат управления игроку.' },
      { num: 237, kind: 'side', title: 'Режиссёрская смерть босса', desc: 'Сделай «режиссёрскую» смерть босса: замедление времени, облёт камеры, серия взрывов, вспышка. Голливуд.' },
      { num: 238, kind: 'main', title: 'Addressables для тяжёлых ассетов', desc: 'Освой Addressables: вынеси тяжёлые ассеты, загружай уровень по требованию, выгружай из памяти.' },
      { num: 239, kind: 'main', title: 'Своя библиотека утилит-пакет', desc: 'Собери свою библиотеку: отдельный репозиторий/пакет с любимыми утилитами (пул, тряска камеры, твины, расширения). Подключи в проект как package. Это твой личный набор инструментов на годы.' },
      { num: 240, boss: true, title: 'БОСС «Хранитель Тайн»', desc: 'Рогалик-арена: процедурная арена, волны врагов с растущей сложностью, случайные апгрейды на выбор между волнами (как в Vampire Survivors), dissolve-смерти, стабильные 60 FPS при 100+ объектах (докажи Profiler’ом), финальный босс с фазами на 10-й волне.' },
    ]},
  { roman: 'IX', title: 'Выход в Мир', trophy: { id: 'act9', name: 'Изданный автор', desc: 'у тебя есть портфолио' },
    quests: [
      { num: 241, kind: 'main', title: 'Концепт игры на 2–4 недели', desc: 'Придумай концепт игры, которую реально доделаешь за 2–4 недели. Правило: опиши её одним предложением. Не влезает — режь.' },
      { num: 242, kind: 'main', title: 'Одностраничный дизайн-документ', desc: 'Напиши одностраничный дизайн-документ: механика, цикл игры, что чувствует игрок, список фич «обязательно» и список «если успею».' },
      { num: 243, kind: 'main', title: 'Урезание фич наполовину', desc: 'Убей 50% фич из списка «обязательно». Серьёзно. Это самый важный навык в геймдеве.' },
      { num: 244, kind: 'main', title: 'Ugly-прототип за 3 дня', desc: 'Собери «ugly-прототип» ядра игры за 3 дня: кубы вместо графики, главное — механика. Ответь честно: весело ли?' },
      { num: 245, kind: 'side', title: 'Прототип без объяснений другу', desc: 'Дай прототип другу без единого слова объяснений. Понял ли он, что делать, за 30 секунд? Нет — переделай онбординг.' },
      { num: 246, kind: 'main', title: 'План на 2 недели по вехам', desc: 'Составь план на 2 недели по вехам: «играбельно», «полный контент», «полировка», «релиз». Веха без даты — это мечта.' },
      { num: 247, kind: 'main', title: 'Вертикальный срез в финальном качестве', desc: 'Сделай вертикальный срез: один уровень/сегмент игры в финальном качестве — арт, звук, UI, всё.' },
      { num: 248, kind: 'main', title: 'Наполнение игры контентом', desc: 'Наполни контентом: остальные уровни/волны/предметы. Скучная часть. Включи подкаст и паши.' },
      { num: 249, kind: 'main', title: 'Плейтест минимум с 3 людьми', desc: 'Устрой плейтест минимум с 3 людьми. Собери фидбек письменно. Найди в жалобах паттерн, а не единичные мнения.' },
      { num: 250, kind: 'main', title: 'Починка топ-5 проблем плейтеста', desc: 'Почини топ-5 проблем из плейтеста. Остальное — в список «после релиза».' },
      { num: 251, kind: 'side', title: 'Личная деталь/пасхалка в игре', desc: 'Добавь в игру одну деталь чисто для себя: пасхалку, шутку, отсылку. Игры без души видно сразу.' },
      { num: 252, kind: 'main', title: 'Иконка, название и логотип-заставка', desc: 'Сделай иконку игры, название и логотип-заставку своей «студии» из задачи 2. Круг замкнулся.' },
      { num: 253, kind: 'main', title: 'Настройка билда и версия 1.0.0', desc: 'Настрой билд правильно: Player Settings, сжатие текстур, разрешения, splash screen, версия 1.0.0.' },
      { num: 254, kind: 'main', title: 'WebGL-билд и его подводные камни', desc: 'Собери WebGL-билд и разберись с его подводными камнями: размер, память, отсутствие потоков.' },
      { num: 255, kind: 'main', title: 'Страница на itch.io', desc: 'Зарегистрируйся на itch.io, оформи страницу: обложка, скриншоты, гифка геймплея, описание, которое продаёт за 5 секунд.' },
      { num: 256, boss: true, title: 'БОСС «Релиз»', desc: 'Опубликуй игру на itch.io. Публично. Со ссылкой, которую можно кинуть маме. Это главный квест всей игры — большинство «разработчиков» не доходят сюда НИКОГДА.' },
      { num: 257, kind: 'main', title: 'Анонс игры в трёх местах', desc: 'Расскажи об игре минимум в трёх местах: соцсети, тематические сообщества, друзья. Пережить страх показа — тоже навык.' },
      { num: 258, kind: 'main', title: 'Аналитика плеев вручную', desc: 'Собери аналитику руками: сколько плеев, откуда пришли, что пишут. Сделай 3 вывода для следующей игры.' },
      { num: 259, kind: 'side', title: 'Патч 1.0.1 по фидбеку', desc: 'Выпусти патч 1.0.1 по фидбеку игроков в течение недели. Поддержка после релиза — признак взрослого разработчика.' },
      { num: 260, kind: 'main', title: 'Участие в геймджеме за 48–72 часа', desc: 'Прими участие в геймджеме (Ludum Dare, GMTK, любой на itch.io). Сделай игру за 48–72 часа. Это ускоренный курс всего.' },
      { num: 261, kind: 'main', title: 'Оценка 20 чужих игр с джема', desc: 'После джема сыграй и оцени 20 чужих игр, оставь осмысленные комментарии. Разбор чужого — прокачка своего.' },
      { num: 262, kind: 'side', title: 'Игра в нелюбимом жанре на джеме', desc: 'Сделай игру на джеме в жанре, который терпеть не можешь. Дискомфорт = рост.' },
      { num: 263, kind: 'main', title: 'Основы мультиплеера (Netcode)', desc: 'Освой основы мультиплеера: подними Netcode for GameObjects, два игрока-куба бегают в одной сцене (host + client на одной машине).' },
      { num: 264, kind: 'main', title: 'NetworkVariable и RPC', desc: 'Разберись с сетевой синхронизацией: NetworkVariable, RPC. Сделай синхронизированный счётчик и чат из двух кнопок-«эмоций».' },
      { num: 265, kind: 'main', title: 'Client vs server authority', desc: 'Пойми client authority vs server authority: сделай стрельбу, где сервер проверяет попадания. Почитай, как читеры ломают клиентскую логику.' },
      { num: 266, kind: 'side', title: 'Локальный мультиплеер на одном экране', desc: 'Сделай локальный мультиплеер на одном экране: два игрока, две схемы управления (клавиатура + геймпад), режим «кто быстрее соберёт монеты».' },
      { num: 267, kind: 'main', title: 'Билд под Android/iOS', desc: 'Собери билд под Android (или iOS): тач-управление, ориентация экрана, тест на реальном телефоне.' },
      { num: 268, kind: 'main', title: 'Мобильное управление (тач-зоны)', desc: 'Адаптируй управление под мобилку по-человечески: виртуальный стик или тап-зоны, крупные кнопки, проверь на маленьком экране.' },
      { num: 269, kind: 'main', title: 'Локализация на два языка', desc: 'Добавь в игру локализацию: минимум два языка через Unity Localization или свою таблицу. Переключение на лету.' },
      { num: 270, boss: true, title: 'БОСС «Марафонец»', desc: 'Сделай и выпусти вторую игру на itch.io — за вдвое меньший срок, чем первую, и качественнее. Скорость + качество = опыт. Сравни страницы обеих игр и напиши себе ретроспективу: 3 вещи, которые улучшил, 3 — над которыми работать.' },
    ]},
  { roman: 'X', title: 'Путь Мастера', trophy: { id: 'act10', name: 'МАСТЕР', desc: 'теперь составь свой список из 300 задач. Для своих учеников.' },
    quests: [
      { num: 271, kind: 'main', title: 'Разбор чужой архитектуры (open-source)', desc: 'Прочитай исходники: скачай открытый Unity-проект уровня Celeste-подобного (например, открытые клоны на GitHub) и разбери архитектуру. Нарисуй схему её на бумаге.' },
      { num: 272, kind: 'main', title: 'Воспроизведение прыжка Celeste/Hollow Knight', desc: 'Разбери движение Celeste/Hollow Knight по видео и статьям и воспроизведи «на глаз» их прыжок. Почувствуй, сколько магии в одной механике.' },
      { num: 273, kind: 'main', title: 'State Machine как классы-состояния', desc: 'Освой один продвинутый паттерн глубоко: State Machine как полноценные классы-состояния. Перепиши контроллер игрока на нём.' },
      { num: 274, kind: 'main', title: 'Command-паттерн и реплей', desc: 'Изучи Command-паттерн: сделай реплей — записывай ввод игрока и воспроизводи прохождение уровня «призраком».' },
      { num: 275, kind: 'side', title: 'Отмотка времени (Prince of Persia)', desc: 'Сделай «отмотку времени» как в Prince of Persia: 3 секунды назад по зажатию клавиши. Command + буфер состояний.' },
      { num: 276, kind: 'main', title: 'Вводный туториал по DOTS/ECS', desc: 'Погрузись в ECS: пройди вводный туториал по DOTS, сделай симуляцию 10 000 движущихся объектов. Сравни с 10 000 GameObject’ов.' },
      { num: 277, kind: 'main', title: 'Job System + Burst', desc: 'Напиши Job System + Burst-версию чего-то тяжёлого (расчёт стаи боидов). Увидь числа и оцени, когда это оправдано.' },
      { num: 278, kind: 'side', title: 'Симуляция стаи птиц (boids)', desc: 'Сделай симуляцию стаи птиц (boids): разделение, выравнивание, сплочение. Медитативно и красиво.' },
      { num: 279, kind: 'main', title: 'Своё EditorWindow — панель левел-дизайнера', desc: 'Освой продвинутый инструментарий редактора: собери своё EditorWindow — «панель левел-дизайнера» с кистями расстановки префабов.' },
      { num: 280, kind: 'main', title: 'Кастомный PropertyDrawer', desc: 'Напиши кастомный PropertyDrawer, который делает твои ScriptableObject-конфиги удобными: превью иконки, слайдеры, валидация значений.' },
      { num: 281, kind: 'main', title: 'Инструмент-валидатор сцены', desc: 'Создай полноценный инструмент для команды: например, валидатор сцены («найди все объекты без коллайдеров / с missing-скриптами») с отчётом.' },
      { num: 282, kind: 'side', title: 'Автоматизация сборки под 3 платформы', desc: 'Автоматизируй сборку: скрипт, который по одной кнопке собирает билды под 3 платформы с правильными именами и версией.' },
      { num: 283, kind: 'main', title: 'CI через GitHub Actions', desc: 'Изучи CI: настрой GitHub Actions, который запускает твои тесты при каждом пуше. Красный крестик на коммите — лучший учитель.' },
      { num: 284, kind: 'main', title: 'Разбор экономики F2P-игры', desc: 'Разберись в экономике F2P-игр на аналитическом уровне: сессии, ретеншн, монетизация. Напиши одностраничный разбор экономики любой известной мобилки.' },
      { num: 285, kind: 'main', title: 'Система достижений в своей игре', desc: 'Реализуй систему достижений в своей игре — с сохранением, всплывашками и «редким» ачивментом, который получат 5% игроков.' },
      { num: 286, kind: 'main', title: 'Доступность игры (accessibility)', desc: 'Сделай игру доступнее: режим для дальтоников, переназначение клавиш, масштаб UI, субтитры. Проверь по чек-листу Game Accessibility Guidelines.' },
      { num: 287, kind: 'side', title: 'Своя игра с закрытыми глазами / одной рукой', desc: 'Пройди свою игру с закрытыми глазами (только звук) или одной рукой. Запиши, что оказалось невозможным. Почини хоть что-то.' },
      { num: 288, kind: 'main', title: 'Техническая статья о своей системе', desc: 'Напиши техническую статью или пост: разбери одну свою систему (пулинг, генерация, сочность) с кодом и гифками. Опубликуй.' },
      { num: 289, kind: 'main', title: 'Видео-девлог или туториал', desc: 'Запиши видео-девлог или туториал на 5–10 минут по теме, в которой разбираешься. Объяснить = понять вдвое глубже.' },
      { num: 290, kind: 'main', title: 'Помощь новичкам на форумах', desc: 'Стань ревьюером: найди на форумах/в Discord новичка с вопросом по коду и помоги развёрнуто. Минимум 5 раз за месяц.' },
      { num: 291, kind: 'side', title: 'Ревью себе из прошлого (босс Акта II)', desc: 'Проведи «код-ревью себе из прошлого»: возьми БОССА из Акта II и перепиши его же за час, красиво. Сравнение приложи к ретроспективе.' },
      { num: 292, kind: 'main', title: 'Ремейк экрана любимой игры детства', desc: 'Сделай ремейк первого экрана любимой игры детства: один экран, точное воспроизведение ощущений. Ностальгия — лучший учитель тонкостей.' },
      { num: 293, kind: 'main', title: 'Смежный навык: Blender/Aseprite/DAW', desc: 'Освой смежный навык на базовом уровне: Blender (простая модель с текстурой в Unity) ИЛИ Aseprite (спрайт с анимацией) ИЛИ трекер/DAW (свой звук). Инди-разработчик — это оркестр.' },
      { num: 294, kind: 'main', title: 'Игра за один вечер (4 часа)', desc: 'Собери «игру за вечер»: 4 часа, таймер, от пустого проекта до играбельного билда. Скорость прототипирования — суперсила.' },
      { num: 295, kind: 'side', title: 'Джем «одна механика — три игры»', desc: 'Устрой личный джем «одна механика — три игры»: возьми механику (например, крюк-кошку) и сделай 3 разных прототипа вокруг неё за неделю.' },
      { num: 296, kind: 'main', title: 'Свой стайлгайд для студии', desc: 'Разработай и напиши свой стайлгайд: правила именования, структура папок, паттерны, чек-лист сочности. Документ, который дал бы джуну в своей студии.' },
      { num: 297, kind: 'main', title: 'Дизайн-док мечты без разработки', desc: 'Спроектируй большую игру НЕ делая её: полный дизайн-док мечты на 10+ страниц, разбитый на этапы. Пойми, что мечта — это 40 маленьких игр из этого списка.' },
      { num: 298, kind: 'main', title: 'Портфолио-страница', desc: 'Собери портфолио-страницу: лучшие игры, гифки, ссылки, пара технических статей. То, что не стыдно приложить к отклику в студию или показать издателю.' },
      { num: 299, kind: 'main', title: 'Наставничество: провести кого-то по Акту I', desc: 'Наставничество: возьми одного человека «от нуля» и проведи по Акту I этого списка. Когда его куб упадёт на плоскость — ты поймёшь, зачем это всё было.' },
      { num: 300, boss: true, finalBoss: true, title: 'ФИНАЛЬНЫЙ БОСС «Мастер»', desc: 'Сделай СВОЮ игру. Не по туториалу, не по этому списку — по собственному замыслу, со своим стилем, доведённую до релиза на itch.io или в сторе, с постмортемом-статьёй о разработке. Когда незнакомый человек напишет тебе «залип на час» — квест закрыт.' },
    ]},
];

/* id, xp по умолчанию (боссы 200, остальные 100), actRoman */
UQ_ACTS.forEach((act) => {
  act.quests.forEach((q) => {
    q.id = "uq" + q.num;
    q.actRoman = act.roman;
    q.xp = q.boss ? 200 : 100;
  });
});
/* боссов нельзя пропустить: всё после каждого босса требует его —
   цепочка по всему списку (в Актах I и IX по два босса подряд) */
let __lastBoss = null;
UQ_ACTS.forEach((act) => {
  act.quests.forEach((q) => {
    if (__lastBoss) q.requires = [__lastBoss];
    if (q.boss) __lastBoss = q.id;
  });
});

const UQ_INDEX = {};
UQ_ACTS.forEach((act) => act.quests.forEach((q) => { UQ_INDEX[q.id] = { act, quest: q }; }));
const UQ_TOTAL_XP = UQ_ACTS.reduce((s, a) => s + a.quests.reduce((ss, q) => ss + q.xp, 0), 0);
const UQ_TOTAL_QUESTS = 300;
const UQ_MAX_LEVEL = Math.ceil(UQ_TOTAL_XP / 1000);

/* ---- уровень: просто floor(xp/1000)+1, без именных званий --------- */
function uqLevelFor(xp) {
  const level = Math.floor(xp / 1000) + 1;
  const into = xp % 1000;
  return { level, into, span: 1000 };
}

/* ---- задачи дня -------------------------------------------------------- */
const UQ_DAILY = [
  { id: "d1", label: "Закрыть 1 квест",  metric: "count", target: 1, bonus: 30 },
  { id: "d3", label: "Закрыть 3 квеста", metric: "count", target: 3, bonus: 70 },
  { id: "d5", label: "Закрыть 5 квестов", metric: "count", target: 5, bonus: 120 },
];

/* ---- достижения: вехи по актам + стрики (авто-проверка) ---------------- */
const UQ_MILESTONES = [
  { id: "first", name: "Пробуждение началось", desc: "Первый квест закрыт", icon: "spark", test: (s) => Object.keys(s.done).length >= 1 },
  ...UQ_ACTS.map((a) => ({
    id: a.trophy.id, name: a.trophy.name, desc: a.trophy.desc, icon: a.quests.some((q) => q.finalBoss) ? "crown" : a.quests.some((q) => q.boss) ? "seal" : "book",
    test: (s) => a.quests.every((q) => !!s.done[q.id]),
  })),
  { 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 UQ_TROPHIES = UQ_MILESTONES;

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

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

/* ---- derived ------------------------------------------------------------- */
const uqUnlocked = (s, q) => (q.requires || []).every((r) => s.done[r]);
const uqActStats = (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 uqQuestProgress = (s, d) => Math.min(s.day.count, d.target);

window.UQLookup = {
  quest: (id) => UQ_INDEX[id], unlocked: uqUnlocked, actStats: uqActStats,
  dailyProgress: uqQuestProgress, levelFor: uqLevelFor, dayKey: uqDayKey,
  totalXp: UQ_TOTAL_XP, totalQuests: UQ_TOTAL_QUESTS, maxLevel: UQ_MAX_LEVEL,
};

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

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

window.UQActions = {
  toggleQuest(id) {
    const e = UQ_INDEX[id];
    if (!e) return;
    window.UQStore.set((s0) => {
      let s = uqEnsureDay(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: uqDayKey() } },
        day: { ...s.day, count: s.day.count + 1 },
      };
      s = uqBumpStreak(s);
      s = uqCheckTrophies(s);
      return s;
    });
    const nowDone = window.UQStore.get().done[id];
    if (nowDone) uqToast("Квест закрыт", e.quest.xp);
  },
  claimDaily(did) {
    const d = UQ_DAILY.find((x) => x.id === did);
    if (!d) return;
    window.UQStore.set((s0) => {
      let s = uqEnsureDay(s0);
      if (s.day.claimed.includes(did) || uqQuestProgress(s, d) < d.target) return s;
      s = { ...s, xp: s.xp + d.bonus, day: { ...s.day, claimed: [...s.day.claimed, did] } };
      return uqCheckTrophies(s);
    });
    uqToast("Квест дня выполнен", d.bonus);
  },
  reset() {
    /* set() ниже сам перезапишет кэш и серверную запись */
    window.UQStore.set(uqMakeSeed());
    uqToast("Unity Quest сброшен");
  },
};

window.UQActs = UQ_ACTS;
window.UQDaily = UQ_DAILY;
window.UQTrophies = UQ_TROPHIES;
