import React from 'react';
import { Code, ScriptKind, Step, Note, Try, Shot } from './docsData';
import { LangTabs } from './docsLang';
import { LUA_OVERRIDES } from './docsGamesBuildersLua';

/**
 * Хелпер: оборачивает JS-код в LangTabs с Lua-параллелью из LUA_OVERRIDES.
 * <CodeBoth game="collect-coins" script="g1_main">{`// JS код...`}</CodeBoth>
 * → если открыт JS — показывает JS код
 * → если открыт Lua — показывает Lua-код из LUA_OVERRIDES[game][script]
 */
function CodeBoth({ game, script, children }) {
    const luaCode = LUA_OVERRIDES[game]?.[script];
    const luaResolved = typeof luaCode === 'function' ? luaCode({ id: script }) : luaCode;
    return (
        <LangTabs
            js={<Code>{children}</Code>}
            lua={luaResolved ? <Code lang="lua">{luaResolved}</Code> : null}
        />
    );
}

/**
 * docsLessons.jsx — тексты уроков для 50 мини-игр (раздел K вики).
 *
 * LESSONS — объект, ключ = id игры из docsGames.js, значение —
 * { intro, mechanics, body } где body — JSX подробной инструкции.
 * Хелперы оформления (Code, ScriptKind, Step, Note, Try, Shot) общие
 * с docsData.jsx.
 *
 * Уроки наполняются по одному. Если урока для игры ещё нет — карточка
 * в каталоге показывает «Урок скоро».
 */

export const LESSONS = {

    // ════════════════════════════════════════════════════
    // ИГРА 1 — «Собери монетки»
    // ════════════════════════════════════════════════════
    'collect-coins': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Небольшая площадка, по которой раскиданы золотые монетки.
                    Игрок ходит и собирает их — за каждую монетку счёт растёт
                    на единицу. Когда собраны все 8 монеток — победа, летит
                    конфетти.
                </p>
                <Shot src="lesson1-result.png"
                      caption="Так выглядит готовая игра «Собери монетки»" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Событие касания</b> — как объект понимает, что игрок
                        его коснулся (<code>onTouch</code>);</li>
                    <li><b>Счётчик очков</b> — как считать собранное и показывать
                        число на экране (<code>game.ui.score</code>);</li>
                    <li><b>Удаление объекта</b> — как убрать собранную монетку
                        со сцены (<code>game.self.delete</code>);</li>
                    <li><b>Связь скриптов</b> — как монетки сообщают главному
                        скрипту, что их собрали.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Площадка</h3>
                <p>
                    Сначала построим пол, по которому будет ходить игрок.
                </p>
                <Step n="1">
                    На вкладке <b>Главная</b> выбери инструмент
                    <kbd className="kbd">Блок</kbd>, в палитре слева — блок
                    <b> травы</b>.
                </Step>
                <Step n="2">
                    Кликай по сцене и собери квадратную площадку примерно
                    14×14 блоков. Это игровое поле.
                </Step>
                <Step n="3">
                    По краю площадки поставь стенку из камня в один блок
                    высотой — чтобы игрок не убежал за край и не упал.
                </Step>
                <Shot src="lesson1-scene.png"
                      caption="Площадка из травы с каменным бортиком" />
                <Note>
                    Не обязательно делать ровно 14×14 — главное, чтобы было
                    где разгуляться. Бортик можно сделать и повыше.
                </Note>

                <h3 className="lessonH">Шаг 2. Монетки</h3>
                <p>
                    Монетка — это <b>примитив-сфера</b> жёлтого цвета.
                    Поставим 8 штук.
                </p>
                <Step n="1">
                    Выбери инструмент <kbd className="kbd">Примитив</kbd>,
                    в палитре — <b>Сфера</b>.
                </Step>
                <Step n="2">
                    Поставь сферу на площадку. Выдели её и в инспекторе
                    справа задай: <b>размер</b> 0.6 по всем осям (монетка
                    небольшая), <b>цвет</b> жёлтый <code>#ffd700</code>,
                    <b> материал</b> «Неон» — чтобы монетка светилась.
                </Step>
                <Step n="3">
                    Подними монетку чуть над полом — высота Y около 1.2,
                    чтобы игрок «врезался» в неё телом.
                </Step>
                <Step n="4">
                    В инспекторе <b>выключи «Столкновение»</b> — тогда игрок
                    проходит сквозь монетку, а не упирается в неё. Касание
                    мы поймаем скриптом.
                </Step>
                <Step n="5">
                    Дай монетке понятное имя — например «Монетка_1». Потом
                    выдели её и нажми <kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>,
                    чтобы дублировать. Расставь 8 монеток по площадке.
                </Step>
                <Shot src="lesson1-coins.png"
                      caption="Восемь жёлтых монеток расставлены по полю" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <p>
                    Главный скрипт считает монетки и проверяет победу.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="collect-coins" script="g1_main">{`// === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт ===
// Этот скрипт глобальный: считает собранные монетки и проверяет победу.

let score = 0;       // сколько монеток собрано
const TOTAL = 8;     // всего монеток на уровне

game.ui.score = score;                       // показать счёт 0 в углу
game.ui.showText('Собери все монетки!', 2);  // подсказка на старте

// Монетки сообщают сюда о сборе через game.broadcast('coin').
// Каждый скрипт работает в своей «песочнице» — переменные одного
// скрипта не видны другому. Поэтому скрипты общаются сообщениями:
// один шлёт game.broadcast('имя'), другой ловит game.onMessage('имя').
game.onMessage('coin', () => {
  score = score + 1;          // +1 к счёту
  game.ui.score = score;      // обновить число на экране
  game.sound.play('coin');    // звон монетки

  if (score >= TOTAL) {
    // собраны все — победа!
    game.ui.showText('Победа! Все монетки твои!', 4);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z },
      { duration: 3, count: 3 });
  }
});`}</CodeBoth>
                <p>
                    Разберём построчно:
                </p>
                <ul>
                    <li><code>let score = 0</code> — переменная-счётчик,
                        в начале монеток собрано 0;</li>
                    <li><code>game.ui.score = score</code> — выводит счёт
                        в угол экрана;</li>
                    <li><code>game.onMessage('coin', ...)</code> — подписка
                        на сообщение «coin». Каждый раз, когда монетка
                        пришлёт это сообщение, выполнится наша функция —
                        прибавит счёт;</li>
                    <li><code>if (score {'>'}= TOTAL)</code> — когда счёт дошёл
                        до 8, показываем победу и запускаем конфетти.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Скрипт монетки</h3>
                <p>
                    Теперь повесим скрипт на <b>каждую</b> монетку. Он ловит
                    касание и сообщает главному скрипту: меня собрали.
                </p>
                <ScriptKind kind="object" on="каждую монетку" />
                <CodeBoth game="collect-coins" script="g1_coin_1">{`// === Скрипт монетки ===
// game.self — это сама монетка, на которой висит скрипт.

game.self.onTouch(() => {
  // игрок коснулся монетки — сообщаем главному скрипту
  game.broadcast('coin');
  game.self.delete();   // монетка исчезает со сцены
});`}</CodeBoth>
                <p>
                    Что происходит: <code>onTouch</code> срабатывает, когда
                    игрок дотронулся до монетки. Внутри мы шлём
                    <code> game.broadcast('coin')</code> — главный скрипт
                    ловит это сообщение и прибавляет очко, а
                    <code> game.self.delete()</code> убирает монетку.
                </p>
                <Note>
                    Скрипт монетки <b>одинаковый</b> для всех 8 монеток.
                    Чтобы не писать его 8 раз: создай скрипт на первой монетке,
                    а когда дублируешь монетку (<kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>) —
                    скрипт копируется вместе с ней.
                </Note>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <p>
                    Нажми <kbd className="kbd">Запустить</kbd>. Походи по
                    площадке и собери монетки:
                </p>
                <ul>
                    <li>при касании монетка исчезает и звенит;</li>
                    <li>счётчик в углу растёт: 1, 2, 3...;</li>
                    <li>когда собрал все 8 — надпись «Победа!» и конфетти.</li>
                </ul>
                <p>
                    Если что-то не работает — открой <b>Консоль</b> внизу
                    справа, там будут ошибки скриптов.
                </p>

                <Try>
                    добавь монеткам вращение — в главном скрипте через
                    <code> game.onTick</code> поворачивай все монетки, чтобы
                    они крутились и были заметнее. Поставь больше монеток.
                    Сделай одну монетку красной и «секретной» — за неё
                    давай сразу +5 очков.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 2 — «Прыгай по платформам»
    // ════════════════════════════════════════════════════
    'platform-jump': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Паркур: в воздухе висит дорожка из платформ с разрывами.
                    Игрок прыгает с платформы на платформу и должен добраться
                    до зелёной финишной площадки. Если упал вниз — игра
                    возвращает на старт.
                </p>
                <Shot src="lesson2-result.png"
                      caption="Паркур из платформ — допрыгай до зелёного финиша" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Примитивы-платформы</b> — как из растянутых кубов
                        строить дорожку для паркура;</li>
                    <li><b>Точка спавна</b> — откуда игрок начинает игру;</li>
                    <li><b>Падение и респаун</b> — как вернуть игрока на старт,
                        если он свалился (<code>onTick</code> +
                        <code> game.player.respawn</code>);</li>
                    <li><b>Финиш</b> — как сделать победную зону, на которую
                        нужно встать.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Старт и платформы</h3>
                <Step n="1">
                    Поставь небольшую <b>стартовую площадку</b> из блоков
                    травы (4×4) — отсюда игрок начнёт.
                </Step>
                <Step n="2">
                    Возьми инструмент <kbd className="kbd">Примитив</kbd> →
                    <b> Куб</b>. Поставь куб перед стартом. В инспекторе задай
                    размер: ширина 2, <b>высота 0.5</b> (платформа плоская),
                    глубина 2. Цвет — коричневый.
                </Step>
                <Step n="3">
                    Подними платформу в воздух — высота Y около 1.5. Убедись,
                    что <b>«Столкновение» включено</b> (на платформу надо
                    вставать) и <b>«Закреплён» включён</b> (платформа не падает).
                </Step>
                <Step n="4">
                    Дублируй платформу (<kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>)
                    и расставь 6-7 штук дорожкой вперёд — каждую следующую чуть
                    выше и со сдвигом, чтобы между ними был прыжок. Не делай
                    разрывы слишком большими — игрок должен допрыгнуть.
                </Step>
                <Shot src="lesson2-scene.png"
                      caption="Дорожка из платформ-кубов в воздухе" />
                <Note>
                    Платформа — это куб с маленькой высотой (0.5). Так делают
                    все паркур-площадки: берёшь куб и сплющиваешь его по Y.
                </Note>

                <h3 className="lessonH">Шаг 2. Финиш</h3>
                <Step n="1">
                    Поставь в конце дорожки ещё один куб, побольше (4×0.5×4) —
                    это финишная площадка.
                </Step>
                <Step n="2">
                    Покрась его в зелёный, материал «Неон» — чтобы финиш
                    светился и был заметен. Дай имя «Финиш».
                </Step>

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <p>
                    Главный скрипт следит за падением и обрабатывает победу.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="platform-jump" script="g2_main">{`// === ИГРА «ПРЫГАЙ ПО ПЛАТФОРМАМ» — главный скрипт ===

let won = false;   // победа уже была?

game.ui.showText('Допрыгай до зелёной площадки!', 3);

// Каждый кадр проверяем высоту игрока.
game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  // упал ниже всех платформ — вернуть на старт
  if (p && p.y < -3) {
    game.player.respawn();
    game.ui.showText('Упал! Пробуй снова.', 1.5);
    game.sound.play('lose');
  }
});

// Финиш сообщает сюда о победе через broadcast/onMessage:
// скрипты живут в РАЗНЫХ песочницах, общая переменная между ними
// не видна — связь только сообщениями.
game.onMessage('finish', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты дошёл до финиша!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z },
    { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Что тут важно:</p>
                <ul>
                    <li><code>game.onTick(...)</code> — функция внутри
                        выполняется <b>каждый кадр</b>. Так мы постоянно
                        следим за игроком;</li>
                    <li><code>if (p.y {'<'} -3)</code> — если игрок опустился
                        ниже -3 по высоте, значит свалился с платформ;</li>
                    <li><code>game.player.respawn()</code> — возвращает игрока
                        на точку спавна;</li>
                    <li><code>won</code> — флажок, чтобы победа сработала
                        только один раз.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Скрипт финиша</h3>
                <ScriptKind kind="object" on="зелёную финишную площадку" />
                <CodeBoth game="platform-jump" script="g2_finish">{`// === Скрипт финиша ===
// Висит на невидимой зоне над зелёной площадкой.
// Игрок встал на площадку — его тело внутри зоны — победа.

game.self.onTouch(() => {
  game.broadcast('finish');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <p>
                    Когда игрок касается финиша, скрипт шлёт
                    <code> game.broadcast('finish')</code>. Главный скрипт ловит
                    это сообщение через <code>game.onMessage('finish', ...)</code> —
                    там показывается «Победа» и летит конфетти. Скрипты в разных
                    «песочницах» общаются только сообщениями.
                </p>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <ul>
                    <li>прыгай <kbd className="kbd">Space</kbd> с платформы
                        на платформу;</li>
                    <li>упал вниз — игра вернёт на старт со звуком;</li>
                    <li>встал на зелёную площадку — «Победа» и конфетти.</li>
                </ul>

                <Try>
                    добавь движущуюся платформу — пусть одна платформа ездит
                    туда-сюда через <code>game.tween</code> с
                    <code> yoyo: true</code>. Сделай платформы поменьше —
                    прыгать будет сложнее. Поставь на пути монетки из урока 1.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 3 — «Не упади»
    // ════════════════════════════════════════════════════
    'dont-fall': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Дорожка из жёлтых плиток. Как только игрок встаёт на плитку,
                    она через секунду <b>исчезает</b>. Стоять нельзя — нужно
                    всё время бежать вперёд, к зелёному финишу. Зазевался —
                    плитка пропала, и ты падаешь вниз.
                </p>
                <Shot src="lesson3-result.png"
                      caption="Беги — плитки исчезают под ногами" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Таймер <code>game.after</code></b> — как выполнить
                        что-то не сразу, а через несколько секунд;</li>
                    <li><b>Исчезновение объекта</b> — как убрать плитку со сцены
                        с задержкой;</li>
                    <li><b>Флажок-защёлка</b> — как сделать, чтобы плитка
                        запускалась на исчезновение только один раз.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Дорожка из плиток</h3>
                <Step n="1">
                    Поставь твёрдую <b>стартовую площадку</b> из блоков камня.
                </Step>
                <Step n="2">
                    Сделай плитку — примитив-куб размером 2×0.4×2, жёлтого цвета.
                    «Столкновение» и «Закреплён» — включены.
                </Step>
                <Step n="3">
                    Дублируй плитку и выложи дорожку вперёд — 12-14 плиток
                    в ряд, с небольшими промежутками. Можно зигзагом
                    влево-вправо, чтобы было интереснее.
                </Step>
                <Step n="4">
                    В конце поставь зелёную неоновую финишную площадку.
                </Step>
                <Shot src="lesson3-scene.png"
                      caption="Дорожка из исчезающих плиток" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>Следит за падением и победой — как в уроке 2.</p>
                <ScriptKind kind="global" />
                <CodeBoth game="dont-fall" script="g3_main">{`// === ИГРА «НЕ УПАДИ» — главный скрипт ===

let won = false;

game.ui.showText('Беги вперёд! Плитки исчезают!', 3);

game.onTick(() => {
  if (won) return;
  // упал вниз — на старт
  const p = game.player.position;
  if (p && p.y < -3) {
    game.player.respawn();
    game.ui.showText('Упал! Снова.', 1.5);
    game.sound.play('lose');
  }
});

// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('finish', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты добежал!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 3. Скрипт исчезающей плитки</h3>
                <p>
                    Самое главное — повесить на <b>каждую плитку</b> скрипт,
                    который убирает её через секунду после касания.
                </p>
                <ScriptKind kind="object" on="каждую плитку" />
                <CodeBoth game="dont-fall" script="g3_tile_1">{`// === Скрипт исчезающей плитки ===

let triggered = false;   // плитка уже запущена на исчезновение?

game.self.onTouch(() => {
  if (triggered) return;   // если уже запущена — выходим
  triggered = true;
  game.sound.play('click');
  // через 1.2 секунды плитка пропадает
  game.after(1.2, () => {
    game.self.delete();
  });
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>triggered</code> — флажок-защёлка. Игрок может
                        коснуться плитки несколько раз, но запустить таймер
                        нужно только один раз — флажок это гарантирует;</li>
                    <li><code>game.after(1.2, () =&gt; {'{...}'})</code> —
                        «через 1.2 секунды выполни это». Внутри —
                        <code> game.self.delete()</code>, плитка исчезает;</li>
                    <li>можно поменять <code>1.2</code> на другое число —
                        меньше значит сложнее, плитки пропадают быстрее.</li>
                </ul>
                <Note>
                    Звук <code>game.sound.play('click')</code> при касании —
                    подсказка игроку: «плитка пошла исчезать, беги!».
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>встаёшь на плитку — щелчок, через секунду она пропадает;</li>
                    <li>стоишь на месте — проваливаешься, респаун на старте;</li>
                    <li>добежал до зелёного финиша — «Победа».</li>
                </ul>

                <Try>
                    сделай разные плитки с разной задержкой — одни исчезают
                    за 0.6с (быстрые), другие за 2с. Добавь на дорожку монетки
                    из урока 1 — собирай их на бегу.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 4 — «Кнопка-открывашка»
    // ════════════════════════════════════════════════════
    'button-door': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Комната перегорожена стеной с дверью. Игрок подходит
                    к красной кнопке, нажимает клавишу <kbd className="kbd">E</kbd> —
                    и дверь плавно уезжает вверх, открывая проход к финишу.
                </p>
                <Shot src="lesson4-result.png"
                      caption="Нажми E у кнопки — дверь откроется" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Взаимодействие по E (ProximityPrompt)</b> — как
                        сделать объект, с которым игрок «общается» клавишей E
                        (<code>game.self.onInteract</code>);</li>
                    <li><b>Поиск объекта по имени</b> — <code>game.scene.findOne</code>;</li>
                    <li><b>Твин</b> — как плавно подвинуть объект
                        (<code>game.tween</code>).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Комната, стена и дверь</h3>
                <Step n="1">
                    Сделай пол комнаты из блоков камня (примерно 16×24).
                </Step>
                <Step n="2">
                    Поперёк комнаты построй <b>стену</b> из двух кубов-примитивов,
                    оставив между ними проём шириной 3.
                </Step>
                <Step n="3">
                    В проём поставь <b>дверь</b> — куб 3×5×0.8, коричневый.
                    Дай ему имя <b>«Дверь»</b> — по имени его найдёт скрипт.
                </Step>

                <h3 className="lessonH">Шаг 2. Кнопка и финиш</h3>
                <Step n="1">
                    Перед дверью поставь <b>кнопку</b> — примитив-цилиндр,
                    низкий и широкий, красный неоновый. Имя — «Кнопка».
                </Step>
                <Step n="2">
                    За дверью поставь зелёную финишную площадку.
                </Step>
                <Shot src="lesson4-scene.png"
                      caption="Комната: кнопка, дверь в стене, финиш за ней" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="button-door" script="g4_main">{`// === ИГРА «КНОПКА-ОТКРЫВАШКА» — главный скрипт ===

game.ui.showText('Подойди к красной кнопке и нажми E', 4);

// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
  game.ui.showText('Победа! Дверь открыта, ты прошёл!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 4. Скрипт кнопки — главное</h3>
                <p>
                    Скрипт кнопки делает всю работу: ловит нажатие E и
                    открывает дверь.
                </p>
                <ScriptKind kind="object" on="красную кнопку" />
                <CodeBoth game="button-door" script="g4_button">{`// === Скрипт кнопки ===

let opened = false;

game.self.onInteract(() => {
  if (opened) return;        // дверь уже открыта
  opened = true;
  game.sound.play('click');

  // находим дверь по имени
  const door = game.scene.findOne('Дверь');
  // плавно поднимаем её вверх на высоту 8
  game.tween(door, { y: 8 }, { duration: 1.2, easing: 'ease' });

  game.ui.showText('Дверь открывается!', 2);
}, {
  text: 'Открыть дверь',   // подсказка над кнопкой
  distance: 4              // на сколько метров подойти
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.self.onInteract(fn, опции)</code> — это
                        и есть взаимодействие по E. Когда игрок подходит ближе
                        чем на <code>distance</code> метров, над кнопкой
                        появляется подсказка «Открыть дверь». Нажатие
                        <kbd className="kbd">E</kbd> запускает функцию;</li>
                    <li><code>game.scene.findOne('Дверь')</code> — находит дверь
                        по имени, которое мы задали в инспекторе;</li>
                    <li><code>game.tween(door, {'{ y: 8 }'}, ...)</code> —
                        плавно меняет высоту двери с текущей до 8, за 1.2
                        секунды. Дверь уезжает вверх — проход открыт.</li>
                </ul>
                <Note>
                    Почему твин, а не просто переставить дверь? Твин двигает
                    <b> плавно</b> — дверь красиво «уезжает». Если бы мы просто
                    задали новую высоту, дверь дёрнулась бы рывком.
                </Note>

                <h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="button-door" script="g4_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <p>Запусти игру:</p>
                <ul>
                    <li>подойди к кнопке — появится подсказка «Открыть дверь»;</li>
                    <li>нажми <kbd className="kbd">E</kbd> — дверь плавно
                        уедет вверх;</li>
                    <li>пройди в открытый проём, встань на финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай две кнопки и две двери — каждая кнопка открывает
                    свою дверь. Или наоборот: чтобы дверь открылась, нужно
                    нажать <b>две</b> кнопки. Добавь звук закрытия и сделай,
                    чтобы дверь сама закрывалась через 5 секунд.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 5 — «Лабиринт»
    // ════════════════════════════════════════════════════
    'maze': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Лабиринт из стен. Игрок появляется в одном углу и должен
                    найти путь к зелёному выходу в другом конце. Стены
                    высокие — через них не перепрыгнуть, нужно искать проход.
                </p>
                <Shot src="lesson5-result.png"
                      caption="Лабиринт сверху — найди путь к выходу" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Постройка из блоков</b> — как строить стены
                        и коридоры;</li>
                    <li><b>Планирование уровня</b> — как нарисовать лабиринт
                        так, чтобы из него был выход;</li>
                    <li><b>Триггер-финиш</b> — как сделать невидимую зону
                        выхода.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Нарисуй лабиринт на бумаге</h3>
                <p>
                    Прежде чем строить — нарисуй лабиринт на листе в клетку.
                    Клетка = проход или стена. Отметь, где старт, где выход.
                    Обязательно проверь карандашом, что от старта до выхода
                    <b> есть путь</b> — иначе игру не пройти.
                </p>
                <Note>
                    Это самый важный шаг. Строить в редакторе по готовой
                    схеме легко. Строить «на глазок» — запутаешься и забудешь
                    оставить проход.
                </Note>

                <h3 className="lessonH">Шаг 2. Пол</h3>
                <Step n="1">
                    Инструментом <kbd className="kbd">Блок</kbd> выложи
                    квадратный пол из камня — по размеру твоего лабиринта.
                </Step>

                <h3 className="lessonH">Шаг 3. Стены</h3>
                <Step n="1">
                    По своей схеме поставь стены. Стена — это столбик из блоков
                    высотой 3 (чтобы игрок не перепрыгнул).
                </Step>
                <Step n="2">
                    Сначала построй внешнюю границу — рамку по краю пола.
                    Потом — внутренние перегородки-коридоры по схеме.
                </Step>
                <Step n="3">
                    Не забудь оставить проходы! Время от времени запускай игру
                    и проверяй, что лабиринт реально проходится.
                </Step>
                <Shot src="lesson5-scene.png"
                      caption="Стены лабиринта в редакторе" />

                <h3 className="lessonH">Шаг 4. Старт и финиш</h3>
                <Step n="1">
                    Поставь точку спавна (вкладка <i>Игра → Ставить спавн</i>)
                    в начале лабиринта.
                </Step>
                <Step n="2">
                    В конце лабиринта положи на пол зелёную неоновую плитку —
                    финиш. Размер чуть меньше клетки, «Столкновение» выключи,
                    имя — «Финиш».
                </Step>

                <h3 className="lessonH">Шаг 5. Скрипты</h3>
                <p>Скрипты совсем простые — лабиринт держится на постройке.</p>
                <ScriptKind kind="global" />
                <CodeBoth game="maze" script="g5_main">{`// === ИГРА «ЛАБИРИНТ» — главный скрипт ===

game.ui.showText('Найди выход из лабиринта!', 3);

// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
  game.ui.showText('Победа! Ты нашёл выход!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="maze" script="g5_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 6. Проверка</h3>
                <ul>
                    <li>пройди лабиринт от старта до зелёного выхода;</li>
                    <li>дошёл до выхода — «Победа» и конфетти;</li>
                    <li>убедись, что нигде нет «дыр» в стенах и тупиков
                        без смысла.</li>
                </ul>

                <Try>
                    спрячь в тупиках лабиринта монетки из урока 1 — пусть
                    игрок собирает их по пути. Сделай лабиринт побольше.
                    Добавь второй этаж — лестницу из блоков наверх.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 6 — «Цветные плитки»
    // ════════════════════════════════════════════════════
    'color-tiles': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На полу — сетка серых плиток. Когда игрок наступает
                    на плитку, она становится ярко-зелёной. Цель — обойти
                    и раскрасить все плитки.
                </p>
                <Shot src="lesson6-result.png"
                      caption="Раскрашенные и серые плитки" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Смена цвета объекта</b> — <code>game.scene.setColor</code>;</li>
                    <li><b>game.self.ref</b> — как скрипт указывает «адрес»
                        своего объекта;</li>
                    <li><b>Счётчик с целью</b> — раскрашено столько-то из всех.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Сетка плиток</h3>
                <Step n="1">
                    Сделай пол из блоков травы.
                </Step>
                <Step n="2">
                    Плитка — примитив-куб 1.8×0.3×1.8, серого цвета.
                    «Столкновение» включено (на плитку наступают).
                </Step>
                <Step n="3">
                    Дублируй плитку и разложи сеткой 6×6 — ровными рядами
                    с небольшими промежутками. Дай каждой понятное имя.
                </Step>
                <Shot src="lesson6-scene.png"
                      caption="Сетка серых плиток на полу" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="color-tiles" script="g6_main">{`// === ИГРА «ЦВЕТНЫЕ ПЛИТКИ» — главный скрипт ===

let painted = 0;     // сколько плиток раскрашено
const TOTAL = 36;    // всего плиток (6×6)

game.ui.score = 0;
game.ui.showText('Наступи на все плитки!', 3);

// Плитки сообщают о покраске через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('paint', () => {
  painted = painted + 1;
  game.ui.score = painted;
  game.sound.play('pickup');
  if (painted >= TOTAL) {
    game.ui.showText('Победа! Все плитки раскрашены!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</CodeBoth>
                <Note>
                    Замени число <code>36</code> на столько плиток, сколько
                    реально поставил. Если сетка 5×5 — будет 25.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт плитки</h3>
                <ScriptKind kind="object" on="каждую плитку" />
                <CodeBoth game="color-tiles" script="g6_tile_1">{`// === Скрипт цветной плитки ===

let painted = false;   // плитка уже раскрашена?

game.self.onTouch(() => {
  if (painted) return;
  painted = true;
  // меняем цвет плитки на ярко-зелёный
  game.scene.setColor(game.self.ref, '#33dd55');
  game.broadcast('paint');   // сообщаем главному скрипту о покраске
});`}</CodeBoth>
                <p>Главное здесь:</p>
                <ul>
                    <li><code>game.scene.setColor(ref, цвет)</code> — меняет
                        цвет объекта прямо во время игры;</li>
                    <li><code>game.self.ref</code> — «адрес» этой плитки.
                        Любая команда <code>game.scene.*</code> работает
                        по ref, и через <code>game.self.ref</code> скрипт
                        передаёт адрес своего объекта;</li>
                    <li><code>game.broadcast('paint')</code> — шлёт сообщение
                        «paint». Главный скрипт ловит его через
                        <code> game.onMessage('paint', ...)</code> и прибавляет
                        счёт. Скрипты живут в разных «песочницах» — переменные
                        одного не видны другому, общаются только сообщениями;</li>
                    <li>флажок <code>painted</code> — чтобы плитка
                        засчиталась только один раз.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>наступаешь на серую плитку — она зеленеет, счёт растёт;</li>
                    <li>раскрасил все — «Победа».</li>
                </ul>

                <Try>
                    сделай так, чтобы плитки красились в <b>случайный</b> цвет —
                    через <code>game.random</code>. Или сделай «вредные»
                    красные плитки: наступил — счёт обнулился, начинай заново.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 7 — «Поймай падающее»
    // ════════════════════════════════════════════════════
    'catch-falling': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    С неба каждые полторы секунды падает жёлтый куб в случайном
                    месте. Игрок бегает по площадке и ловит кубы — касаешься
                    куба, он засчитывается и исчезает. Поймай 15 кубов.
                </p>
                <Shot src="lesson7-result.png"
                      caption="Кубы падают с неба — лови их" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Спавн объектов из скрипта</b> — создавать кубы
                        прямо во время игры (<code>game.scene.spawn</code>);</li>
                    <li><b>Таймер <code>game.every</code></b> — делать что-то
                        снова и снова с интервалом;</li>
                    <li><b>Случайные числа</b> — <code>game.random</code>;</li>
                    <li><b>Отслеживание объектов</b> — как помнить, какие
                        объекты «наши».</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Площадка</h3>
                <Step n="1">
                    Сделай площадку 16×16 из травы с каменным бортиком —
                    чтобы было где ловить и некуда падать.
                </Step>
                <p>
                    Кубы создаются скриптом, заранее их ставить не нужно —
                    вся игра в одном скрипте.
                </p>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="catch-falling" script="g7_main">{`// === ИГРА «ПОЙМАЙ ПАДАЮЩЕЕ» — главный скрипт ===

let score = 0;
const GOAL = 15;          // сколько кубов нужно поймать
let won = false;

// Пойманные кубы — чтобы не засчитать один куб дважды.
const caught = {};

game.ui.score = 0;
game.ui.showText('Лови падающие кубы! Нужно 15', 3);

// Каждые 1.5 секунды роняем с неба новый куб.
game.every(1.5, () => {
  if (won) return;
  const x = game.random(-6, 6);   // случайная точка над площадкой
  const z = game.random(-6, 6);
  const cube = game.scene.spawn('primitive:cube', {
    x: x, y: 14, z: z,
    sx: 0.8, sy: 0.8, sz: 0.8,
    color: '#ffcc33',
    anchored: false,              // anchored:false — куб ПАДАЕТ (физика)
  });
  // если за 6 секунд не поймали — куб исчезнет сам
  game.scene.deleteAfter(cube, 6);
});

// Игрок коснулся падающего куба. onPlayerTouch шлёт e.target —
// ref ('primitive:N') только для заспавненных объектов (кубов),
// пол и стены сюда НЕ приходят.
game.onPlayerTouch((e) => {
  if (won) return;
  const ref = e && e.target;
  if (!ref || caught[ref]) return;   // нет ref или куб уже пойман
  caught[ref] = true;
  score = score + 1;
  game.ui.score = score;
  game.sound.play('coin');
  game.scene.delete(ref);            // пойманный куб исчезает

  if (score >= GOAL) {
    won = true;
    game.ui.showText('Победа! Ты поймал 15 кубов!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</CodeBoth>
                <p>Разберём по частям:</p>
                <ul>
                    <li><code>game.every(1.5, fn)</code> — каждые 1.5 секунды
                        выполняет функцию. В ней мы создаём куб;</li>
                    <li><code>game.scene.spawn('primitive:cube', опции)</code> —
                        создаёт куб прямо во время игры. Высота
                        <code> y: 14</code> — куб появляется высоко, а
                        <code> anchored: false</code> — он падает по физике;</li>
                    <li><code>game.random(-6, 6)</code> — случайное число
                        от -6 до 6, так куб падает в случайном месте;</li>
                    <li><code>caught[ref] = true</code> — помечаем уже пойманный
                        куб, чтобы одно касание не засчиталось дважды;</li>
                    <li><code>deleteAfter(cube, 6)</code> — если куб не поймали
                        за 6 секунд, он сам исчезнет (уже упал на землю).</li>
                </ul>
                <Note>
                    Зачем нужен <code>caught</code>? Касание куба может прийти
                    несколько раз подряд. Пометка <code>caught[ref]</code>
                    гарантирует: каждый куб засчитываем ровно один раз.
                </Note>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>кубы сыплются с неба каждые 1.5 секунды;</li>
                    <li>добежал и коснулся куба — звон, +1 очко;</li>
                    <li>не успел — куб упал и пропал;</li>
                    <li>поймал 15 — «Победа».</li>
                </ul>

                <Try>
                    ускорь падение кубов — спавни их чаще (поменяй 1.5
                    на 0.8). Сделай редкие красные кубы, которые дают +5
                    очков. Добавь таймер: успей поймать 15 кубов за минуту.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 8 — «Беги к финишу»
    // ════════════════════════════════════════════════════
    'run-to-finish': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Длинная беговая трасса. Как только начинается игра —
                    включается секундомер. Игрок бежит к зелёному финишу.
                    Добежал — секундомер останавливается и показывает
                    результат. Можно соревноваться: кто быстрее.
                </p>
                <Shot src="lesson8-result.png"
                      caption="Беговая трасса с секундомером" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Секундомер</b> — <code>game.ui.timer</code>;</li>
                    <li><b>dt в onTick</b> — как измерять прошедшее время;</li>
                    <li><b>Округление</b> — показать время красиво.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Трасса</h3>
                <Step n="1">
                    Выложи из блоков длинную дорожку — например, 6 блоков
                    в ширину и 60 в длину.
                </Step>
                <Step n="2">
                    По бокам поставь невысокие бортики, чтобы игрок
                    не сбежал с трассы.
                </Step>
                <Step n="3">
                    В конце трассы положи зелёную неоновую финишную
                    плитку, имя «Финиш».
                </Step>
                <Step n="4">
                    Точку спавна поставь в самом начале трассы.
                </Step>
                <Shot src="lesson8-scene.png"
                      caption="Трасса с финишем в конце" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="run-to-finish" script="g8_main">{`// === ИГРА «БЕГИ К ФИНИШУ» — главный скрипт ===

let finished = false;
let time = 0;             // прошло секунд

game.ui.timer = 0;
game.ui.showText('Беги к зелёному финишу — на время!', 3);

// Каждый кадр прибавляем к таймеру прошедшее время.
game.onTick((dt) => {
  if (finished) return;
  time = time + dt;             // dt — секунды с прошлого кадра
  game.ui.timer = time;         // показываем секундомер
});

// Финиш сообщает о завершении забега через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('finish', () => {
  if (finished) return;
  finished = true;
  // округляем время до десятых
  const t = Math.round(time * 10) / 10;
  game.ui.showText('Финиш! Твоё время: ' + t + ' сек', 6);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Главное здесь — измерение времени:</p>
                <ul>
                    <li><code>game.onTick((dt) =&gt; {'{...}'})</code> —
                        функция получает <b>dt</b>: сколько секунд прошло
                        с прошлого кадра (обычно ~0.016 — это 1/60 секунды);</li>
                    <li><code>time = time + dt</code> — складывая все dt,
                        мы получаем, сколько всего прошло времени;</li>
                    <li><code>game.ui.timer = time</code> — выводит секундомер
                        на экран в формате ММ:СС;</li>
                    <li><code>Math.round(time * 10) / 10</code> — округляет
                        до одной цифры после запятой (12.3 вместо 12.34567).</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="run-to-finish" script="g8_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('finish');   // сообщаем главному скрипту о финише
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>с началом игры в углу побежал секундомер;</li>
                    <li>беги к финишу <kbd className="kbd">Shift</kbd> для
                        ускорения;</li>
                    <li>встал на зелёную плитку — секундомер замер,
                        показалось твоё время.</li>
                </ul>

                <Try>
                    поставь на трассе препятствия — кубы-стены, через которые
                    надо перепрыгивать. Сделай несколько «дорожек» и засекай
                    время на каждой. Покажи лучшее время через
                    <code> game.save</code> — рекорд.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 9 — «Светофор»
    // ════════════════════════════════════════════════════
    'traffic-light': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    В конце дорожки — большой шар-светофор. Он по очереди
                    становится зелёным и красным. На <b>зелёный</b> можно
                    бежать к финишу, на <b>красный</b> нужно замереть.
                    Двинулся на красный — игра возвращает на старт.
                </p>
                <Shot src="lesson9-result.png"
                      caption="Светофор: зелёный — беги, красный — замри" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Фазы по таймеру</b> — переключать состояние игры
                        через <code>game.after</code>;</li>
                    <li><b>Проверка движения</b> — как узнать, что игрок
                        сдвинулся (сравнить позиции);</li>
                    <li><b>game.scene.setColor</b> — менять цвет светофора.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Дорожка и светофор</h3>
                <Step n="1">
                    Сделай длинную дорожку из блоков (8 в ширину, ~50 в длину).
                </Step>
                <Step n="2">
                    В конце дорожки, повыше, поставь большую <b>сферу</b> —
                    это светофор. Материал «Неон», цвет красный. Имя —
                    «Светофор».
                </Step>
                <Step n="3">
                    Перед светофором — зелёная финишная плитка. Точка спавна —
                    в начале дорожки.
                </Step>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>Это самый сложный скрипт пока — разберём внимательно.</p>
                <ScriptKind kind="global" />
                <CodeBoth game="traffic-light" script="g9_main">{`// === ИГРА «СВЕТОФОР» — главный скрипт ===

let phase = 'green';      // 'green' (беги) или 'red' (замри)
let won = false;
let prev = null;          // прошлая позиция игрока

game.ui.showText('Зелёный — беги! Красный — замри!', 3);

// findOne нельзя сразу в начале скрипта — снимок сцены приходит
// чуть позже. Светофор находим и запускаем цикл через game.after.
let light = null;

// Переключаем свет: зелёный 3с, красный 2.5с, по кругу.
function green() {
  if (won || !light) return;
  phase = 'green';
  game.scene.setColor(light, '#22dd55');
  game.ui.showText('ЗЕЛЁНЫЙ — беги!', 1.2);
  game.after(3, red);
}
function red() {
  if (won || !light) return;
  phase = 'red';
  game.scene.setColor(light, '#e23b3b');
  game.ui.showText('КРАСНЫЙ — замри!', 1.2);
  game.after(2.5, green);
}
game.after(0.2, () => {
  light = game.scene.findOne('Светофор');
  green();   // начинаем с зелёного
});

// Каждый кадр: если красный и игрок шевелится — на старт.
game.onTick((dt) => {
  if (won) return;
  const p = game.player.position;
  if (!p) return;
  if (prev && phase === 'red') {
    // на сколько игрок сдвинулся с прошлого кадра
    const moved = Math.hypot(p.x - prev.x, p.z - prev.z);
    if (moved / dt > 0.8) {        // двигался — пойман
      game.player.respawn();
      game.ui.showText('Двинулся на красный! На старт.', 2);
      game.sound.play('lose');
    }
  }
  prev = { x: p.x, z: p.z };       // запомнить позицию
});

// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты дошёл до финиша!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Как работают фазы:</p>
                <ul>
                    <li><code>green()</code> ставит зелёный цвет и через
                        <code> game.after(3, red)</code> запланирует
                        переключение на красный через 3 секунды;</li>
                    <li><code>red()</code> — наоборот: красный, и через 2.5с
                        обратно <code>green</code>. Так свет мигает по кругу;</li>
                    <li>две функции вызывают друг друга через таймер — это
                        и есть «вечный» светофор.</li>
                </ul>
                <p>Как ловится движение:</p>
                <ul>
                    <li>каждый кадр запоминаем позицию игрока в <code>prev</code>;</li>
                    <li><code>Math.hypot(dx, dz)</code> — на сколько игрок
                        сдвинулся за кадр;</li>
                    <li><code>moved / dt</code> — это его <b>скорость</b>.
                        Если на красный скорость больше 0.8 — игрок шевелится,
                        возвращаем на старт.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="traffic-light" script="g9_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <ul>
                    <li>светофор мигает зелёный/красный;</li>
                    <li>на зелёный беги, на красный замри;</li>
                    <li>двинулся на красный — вернёт на старт;</li>
                    <li>добежал до финиша на зелёный — «Победа».</li>
                </ul>

                <Try>
                    делай красную фазу всё короче с каждым кругом — игра
                    будет напряжённее. Добавь второй светофор посередине.
                    Сделай несколько игроков-соперников.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 10 — «Прыжок-пружина»
    // ════════════════════════════════════════════════════
    'spring-jump': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Башня из площадок-этажей на разной высоте. Между ними —
                    оранжевые батуты. Игрок встаёт на батут, тот подбрасывает
                    его высоко вверх — на следующий этаж. Допрыгай до верха.
                </p>
                <Shot src="lesson10-result.png"
                      caption="Башня из батутов — прыгай всё выше" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.player.boostJump</b> — мощный подброс игрока
                        вверх;</li>
                    <li><b>Многоуровневая сцена</b> — строить вверх, не только
                        вширь;</li>
                    <li><b>Один скрипт на много объектов</b> — все батуты
                        работают одинаково.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Башня</h3>
                <Step n="1">
                    Поставь стартовую площадку из блоков на земле.
                </Step>
                <Step n="2">
                    Сделай 2-3 площадки-этажа из кубов-примитивов на высотах
                    7, 14, 21 — друг над другом со сдвигом. «Закреплён»
                    и «Столкновение» включены.
                </Step>
                <Step n="3">
                    На вершине — зелёная финишная плитка.
                </Step>

                <h3 className="lessonH">Шаг 2. Батуты</h3>
                <Step n="1">
                    Батут — примитив-цилиндр, низкий и широкий (2×0.4×2),
                    оранжевый неоновый.
                </Step>
                <Step n="2">
                    Поставь по батуту на старте и на каждом этаже (кроме
                    верхнего) — там, откуда нужно прыгать выше.
                </Step>
                <Shot src="lesson10-scene.png"
                      caption="Этажи с батутами между ними" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="spring-jump" script="g10_main">{`// === ИГРА «ПРЫЖОК-ПРУЖИНА» — главный скрипт ===

let won = false;

game.ui.showText('Прыгай по батутам всё выше!', 3);

// упал вниз — на старт
game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -3) {
    game.player.respawn();
    game.sound.play('lose');
  }
});

// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты допрыгал до верха!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 4. Скрипт батута</h3>
                <p>Повесь этот скрипт на <b>каждый</b> батут — он одинаковый.</p>
                <ScriptKind kind="object" on="каждый батут" />
                <CodeBoth game="spring-jump" script="g10_tramp_4">{`// === Скрипт батута ===
// Игрок встал на батут — мощный подброс вверх.

game.self.onTouch(() => {
  game.player.boostJump(3.2);   // 3.2 = в 3 раза выше обычного прыжка
  game.sound.play('jump');
});`}</CodeBoth>
                <p>
                    <code>game.player.boostJump(сила)</code> — мгновенно
                    подбрасывает игрока. <code>1</code> — как обычный прыжок,
                    <code> 3.2</code> — в три с лишним раза выше. Подбери число
                    так, чтобы игрок допрыгивал от батута до следующего этажа.
                </p>
                <Note>
                    Если игрок не долетает до этажа — увеличь силу
                    (<code>boostJump(4)</code>) или поставь этажи ближе.
                    Если перелетает — наоборот, уменьши.
                </Note>

                <h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="spring-jump" script="g10_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <ul>
                    <li>встал на батут — подлетел вверх;</li>
                    <li>попал на этаж — иди к следующему батуту;</li>
                    <li>добрался до зелёного верха — «Победа».</li>
                </ul>

                <Try>
                    сделай батуты с разной силой — где-то слабый, где-то очень
                    мощный. Добавь движущиеся этажи (твин из урока 2). Расставь
                    в воздухе монетки, которые ловишь во время полёта.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 11 — «Эхо-комната»
    // ════════════════════════════════════════════════════
    'echo-room': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Комната с шестью разноцветными плитками. Когда игрок
                    наступает на плитку — она звучит и вспыхивает искрами.
                    Каждая плитка — свой звук. Пройди все шесть, потом
                    встань на зелёный финиш в центре.
                </p>
                <Shot src="lesson11-result.png"
                      caption="Звуковые плитки эхо-комнаты" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Звук на событие</b> — <code>game.sound.play</code>;</li>
                    <li><b>game.self.position</b> — узнать, где находится
                        объект скрипта;</li>
                    <li><b>Частицы-вспышка</b> — эффект при касании.</li>
                </ul>
                <Note>
                    Звук в играх обязателен — игра без звука кажется «мёртвой».
                    Этот урок как раз про то, как оживить игру звуком.
                </Note>

                <h3 className="lessonH">Шаг 1. Комната</h3>
                <Step n="1">
                    Сделай пол комнаты (14×14) и стены из блоков высотой 3 —
                    замкнутое пространство.
                </Step>

                <h3 className="lessonH">Шаг 2. Звуковые плитки</h3>
                <Step n="1">
                    Плитка — примитив-цилиндр, низкий и широкий, неоновый.
                    Сделай 6 штук <b>разных цветов</b> и расставь по комнате.
                </Step>
                <Step n="2">
                    В центре комнаты — зелёная финишная плитка.
                </Step>
                <Shot src="lesson11-scene.png"
                      caption="Шесть цветных плиток и финиш" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="echo-room" script="g11_main">{`// === ИГРА «ЭХО-КОМНАТА» — главный скрипт ===

let stepped = 0;          // на сколько плиток наступили
const TOTAL = 6;

game.ui.score = 0;
game.ui.showText('Наступи на все цветные плитки!', 3);

// Плитки и финиш сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.

// игрок впервые наступил на звуковую плитку
game.onMessage('step', () => {
  stepped = stepped + 1;
  game.ui.score = stepped;
  if (stepped >= TOTAL) {
    game.ui.showText('Все плитки звучали! Иди на финиш.', 3);
  }
});

// игрок встал на финиш
game.onMessage('finish', () => {
  if (stepped < TOTAL) {
    game.ui.showText('Сначала пройди все ' + TOTAL + ' плиток!', 2);
    return;
  }
  game.ui.showText('Победа! Эхо-комната пройдена!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 4. Скрипт звуковой плитки</h3>
                <p>
                    Повесь на каждую плитку. Поменяй звук — у каждой плитки
                    свой: <code>'coin'</code>, <code>'jump'</code>,
                    <code> 'pickup'</code>, <code>'click'</code>,
                    <code> 'hit'</code>.
                </p>
                <ScriptKind kind="object" on="каждую цветную плитку" />
                <CodeBoth game="echo-room" script="g11_tile_1">{`// === Скрипт звуковой плитки ===

let used = false;   // на эту плитку уже наступали?

game.self.onTouch(() => {
  // звук играет КАЖДЫЙ раз — это эхо-комната
  game.sound.play('coin');   // у каждой плитки свой звук!
  // вспышка частиц над плиткой
  game.scene.spawnParticles('sparks', game.self.position,
    { duration: 0.6, color: '#e23b3b' });
  // засчитываем плитку только в первый раз
  if (!used) {
    used = true;
    game.broadcast('step');   // сообщаем главному скрипту о новой плитке
  }
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.sound.play('coin')</code> — проигрывает
                        звук. Доступные звуки: <code>coin</code>, <code>jump</code>,
                        <code> pickup</code>, <code>click</code>, <code>hit</code>,
                        <code> win</code>, <code>lose</code>;</li>
                    <li><code>game.self.position</code> — координаты самой
                        плитки, над ней появятся искры;</li>
                    <li><code>game.broadcast('step')</code> — шлёт сообщение
                        главному скрипту, тот ловит его через
                        <code> game.onMessage('step', ...)</code>. Скрипты
                        в разных «песочницах» общаются только сообщениями;</li>
                    <li>звук играет при <b>каждом</b> касании (эхо!), а в счёт
                        плитка идёт только первый раз — за это отвечает
                        флажок <code>used</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="echo-room" script="g11_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('finish');   // сообщаем главному скрипту о финише
});`}</CodeBoth>
                <ul>
                    <li>наступаешь на плитку — звук и искры;</li>
                    <li>прошёл все 6 — появится подсказка идти на финиш;</li>
                    <li>встал на финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай мелодию: пусть игрок наступает на плитки в нужном
                    порядке (это будет похоже на музыкальную игру). Добавь
                    больше плиток. Меняй цвет плитки после нажатия.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 12 — «Дверь по коду»
    // ════════════════════════════════════════════════════
    'code-door': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Перед запертой дверью — четыре кнопки с цифрами. Чтобы
                    дверь открылась, нужно нажать кнопки (клавишей
                    <kbd className="kbd">E</kbd>) в <b>правильном порядке</b> —
                    знать секретный код. Ошибся — код сбрасывается, начинай
                    заново.
                </p>
                <Shot src="lesson12-result.png"
                      caption="Кнопки-цифры — набери код, чтобы открыть дверь" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Хранить последовательность</b> — массив нажатых
                        кнопок;</li>
                    <li><b>Проверять порядок</b> — совпадает ли ввод с кодом;</li>
                    <li><b>Сбрасывать прогресс</b> — при ошибке начать заново.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Комната, дверь, кнопки</h3>
                <Step n="1">
                    Сделай комнату со стеной и дверью — как в уроке 4. Дверь
                    назови «Дверь».
                </Step>
                <Step n="2">
                    Перед дверью поставь <b>4 кнопки-цилиндра</b> в ряд.
                    Назови их «Кнопка_1», «Кнопка_2», «Кнопка_3», «Кнопка_4».
                </Step>
                <Step n="3">
                    За дверью — зелёный финиш.
                </Step>
                <Shot src="lesson12-scene.png"
                      caption="Четыре кнопки-цифры перед дверью" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Здесь самое интересное — проверка кода. Разберём подробно.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="code-door" script="g12_main">{`// === ИГРА «ДВЕРЬ ПО КОДУ» — главный скрипт ===

// СЕКРЕТНЫЙ КОД — порядок кнопок. Поменяй на свой!
const CODE = [3, 1, 4, 2];

let entered = [];         // что игрок уже нажал
let opened = false;

game.ui.showText('Нажми кнопки в правильном порядке (E)', 4);

// Кнопки и финиш сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
// Номер нажатой кнопки приходит в data: { num }.

// игрок нажал кнопку с номером d.num
game.onMessage('press', (d) => {
  if (opened) return;
  game.sound.play('click');
  entered.push(d.num);

  // проверяем — совпадает ли начало с кодом
  const i = entered.length - 1;
  if (entered[i] !== CODE[i]) {
    // ошибка — сброс
    entered = [];
    game.ui.showText('Неверно! Код сброшен.', 1.5);
    game.sound.play('lose');
    return;
  }

  // весь код введён верно
  if (entered.length === CODE.length) {
    opened = true;
    game.ui.showText('Код верный! Дверь открывается.', 3);
    game.sound.play('win');
    const door = game.scene.findOne('Дверь');
    game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
  } else {
    game.ui.showText('Верно! Дальше...', 1);
  }
});

// игрок встал на финиш
game.onMessage('win', () => {
  game.ui.showText('Победа! Ты разгадал код!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Как работает проверка кода:</p>
                <ul>
                    <li><code>CODE = [3, 1, 4, 2]</code> — секретный код,
                        массив номеров кнопок по порядку;</li>
                    <li><code>entered</code> — список того, что игрок уже нажал;</li>
                    <li><code>game.onMessage('press', (d) =&gt; ...)</code> —
                        ловит сообщение от кнопки. Номер кнопки приходит
                        в <code>d.num</code>. Кнопки шлют его через
                        <code> game.broadcast('press', {'{ num: ... }'})</code> —
                        скрипты в разных «песочницах» общаются только
                        сообщениями;</li>
                    <li><code>entered.push(d.num)</code> — добавляем новое
                        нажатие в конец списка;</li>
                    <li><code>entered[i] !== CODE[i]</code> — проверяем: совпало
                        ли последнее нажатие с нужной цифрой кода. Не совпало —
                        <code> entered = []</code> очищает список, начинай
                        заново;</li>
                    <li>когда длина <code>entered</code> равна длине
                        <code> CODE</code> и всё совпало — код разгадан,
                        дверь уезжает твином.</li>
                </ul>
                <Note>
                    Хитрость: мы проверяем каждое нажатие <b>сразу</b>. Если
                    игрок ошибся на втором шаге — не нужно ждать четвёртого,
                    код сбросится тут же.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипты кнопок</h3>
                <p>
                    На каждую кнопку — свой скрипт. Отличается только
                    цифра в <code>press(...)</code> и в подсказке.
                </p>
                <ScriptKind kind="object" on="Кнопку_1 (на остальных — поменяй цифру)" />
                <CodeBoth game="code-door" script="g12_btn_1">{`// === Скрипт кнопки-цифры 1 ===

game.self.onInteract(() => {
  // сообщаем главному скрипту номер нажатой кнопки
  game.broadcast('press', { num: 1 });   // ← номер кнопки
}, {
  text: 'Нажать кнопку 1',
  distance: 3
});`}</CodeBoth>
                <p>
                    Для «Кнопки_2» поставь <code>{'{ num: 2 }'}</code> и текст
                    «Нажать кнопку 2», и так далее.
                </p>

                <h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="code-door" script="g12_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <ul>
                    <li>подходи к кнопкам, жми <kbd className="kbd">E</kbd>;</li>
                    <li>нажал по коду 3-1-4-2 — дверь открылась;</li>
                    <li>ошибся — «Код сброшен», начинай сначала;</li>
                    <li>прошёл в дверь, встал на финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай код длиннее — 6 кнопок. Подскажи код игроку
                    через надписи-billboard над кнопками. Сделай так, чтобы
                    после трёх ошибок включалась «сигнализация» — звук.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 13 — «Торговец»
    // ════════════════════════════════════════════════════
    'trader': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    В лавке за прилавком стоит NPC-торговец. Игрок подходит,
                    нажимает <kbd className="kbd">E</kbd> — торговец отвечает
                    репликой и дарит ключ. С ключом можно открыть дверь
                    и дойти до финиша.
                </p>
                <Shot src="lesson13-result.png"
                      caption="Торговец за прилавком — поговори и получи ключ" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>NPC</b> — создавать персонажей-жителей
                        (<code>game.scene.spawnNpc</code>);</li>
                    <li><b>Реплики NPC</b> — <code>npc.say</code>;</li>
                    <li><b>Инвентарь</b> — выдавать и проверять предметы
                        (<code>game.inventory</code>).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Лавка</h3>
                <Step n="1">
                    Сделай пол лавки из деревянных блоков.
                </Step>
                <Step n="2">
                    Поставь <b>прилавок</b> — длинный куб-примитив. Имя
                    «Прилавок».
                </Step>
                <Step n="3">
                    Сделай стену с дверью (имя «Дверь») и зелёный финиш
                    за ней — как в уроке 4.
                </Step>
                <Shot src="lesson13-scene.png"
                      caption="Лавка: прилавок, дверь, финиш" />
                <Note>
                    Самого торговца ставить не нужно — он появится из скрипта
                    командой <code>spawnNpc</code>.
                </Note>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="trader" script="g13_main">{`// === ИГРА «ТОРГОВЕЦ» — главный скрипт ===

game.ui.showText('Поговори с торговцем — нажми E у прилавка', 4);

// создаём NPC-торговца за прилавком
const trader = game.scene.spawnNpc('character-a', {
  x: 0, y: 1, z: 5,    // y=1 — на верху блоков пола (иначе утоплен)
  name: 'Торговец Боб',
  hp: 100,
  speed: 0,           // торговец стоит на месте
});

let hasKey = false;

// Прилавок, дверь и финиш сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.

// игрок заговорил с торговцем
game.onMessage('talk', () => {
  if (hasKey) {
    trader.say('Иди к двери, ключ у тебя!', 3);
    return;
  }
  hasKey = true;
  trader.say('Привет! Вот тебе ключ от двери. Удачи!', 4);
  game.inventory.add({ name: 'Ключ' });
  game.ui.showText('Ты получил Ключ!', 2);
  game.sound.play('pickup');
});

// игрок пытается открыть дверь
game.onMessage('openDoor', () => {
  if (!game.inventory.has('Ключ')) {
    game.ui.showText('Дверь заперта. Нужен ключ от торговца.', 2);
    return;
  }
  game.ui.showText('Ключ подошёл! Дверь открыта.', 3);
  game.sound.play('win');
  const door = game.scene.findOne('Дверь');
  game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
});

// игрок встал на финиш
game.onMessage('win', () => {
  game.ui.showText('Победа! Ты прошёл лавку торговца!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.scene.spawnNpc('character-a', опции)</code> —
                        создаёт NPC. <code>speed: 0</code> — торговец не ходит,
                        стоит за прилавком;</li>
                    <li><code>trader.say('текст', 4)</code> — над головой NPC
                        на 4 секунды появляется реплика;</li>
                    <li><code>game.inventory.add({'{ name: '}'Ключ'{' }'})</code> —
                        кладёт предмет в инвентарь игрока;</li>
                    <li><code>game.inventory.has('Ключ')</code> — проверяет,
                        есть ли предмет. Дверь открывается только с ключом.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт прилавка</h3>
                <ScriptKind kind="object" on="прилавок" />
                <CodeBoth game="trader" script="g13_counter">{`// === Скрипт прилавка ===
game.self.onInteract(() => {
  game.broadcast('talk');   // сообщаем главному скрипту: говорим с торговцем
}, {
  text: 'Поговорить с торговцем',
  distance: 4
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 4. Скрипт двери</h3>
                <ScriptKind kind="object" on="дверь" />
                <CodeBoth game="trader" script="g13_door">{`// === Скрипт двери ===
game.self.onInteract(() => {
  game.broadcast('openDoor');   // сообщаем главному скрипту: открыть дверь
}, {
  text: 'Открыть дверь',
  distance: 4
});`}</CodeBoth>

                <h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="trader" script="g13_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <ul>
                    <li>подойди к прилавку, нажми <kbd className="kbd">E</kbd> —
                        торговец заговорит и даст ключ;</li>
                    <li>подойди к двери, нажми <kbd className="kbd">E</kbd> —
                        с ключом она откроется;</li>
                    <li>встань на финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай, чтобы торговец продавал ключ за монетки (собери
                    монетки из урока 1, потом купи). Добавь второго NPC
                    с другим предметом. Пусть торговец ходит за прилавком
                    через <code>npc.moveTo</code>.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 14 — «Собери по тегам»
    // ════════════════════════════════════════════════════
    'collect-by-tag': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На площадке вперемешку стоят жёлтые звёзды и синие кубы.
                    Собирать нужно только звёзды — кубы это обманки. Игра
                    помечает все звёзды специальной меткой-<b>тегом</b>,
                    а потом по тегу легко считает, сколько звёзд ещё осталось.
                </p>
                <Shot src="lesson14-result.png"
                      caption="Жёлтые звёзды собираем, синие кубы — обманки" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Теги</b> — это «ярлычки», которые можно навесить
                        на объекты, чтобы потом находить именно их
                        (<code>game.scene.tag</code>);</li>
                    <li><b>Снять тег</b> — <code>game.scene.untag</code>;</li>
                    <li><b>Найти всё по тегу</b> — <code>game.scene.getTagged</code>
                        возвращает список всех объектов с этим тегом;</li>
                    <li><b>Считать остаток</b> — узнавать, сколько целей ещё
                        не собрано.</li>
                </ul>
                <Note>
                    Тег — это как наклейка. Можешь наклеить на всё, что нужно
                    собрать, ярлычок «звезда», а потом одной командой спросить:
                    «дай мне все объекты с ярлычком звезда». Очень удобно, когда
                    объектов много.
                </Note>

                <h3 className="lessonH">Шаг 1. Площадка</h3>
                <Step n="1">
                    Инструментом <kbd className="kbd">Блок</kbd> выложи
                    квадратную травяную площадку примерно 16×16.
                </Step>

                <h3 className="lessonH">Шаг 2. Звёзды и кубы-обманки</h3>
                <Step n="1">
                    Звезда — примитив-<b>конус</b> жёлтого цвета
                    <code> #ffd700</code>, материал «Неон». Поставь 7 штук
                    и дай им имена «Звезда_1», «Звезда_2» ... «Звезда_7».
                </Step>
                <Step n="2">
                    «Столкновение» у звёзд <b>выключи</b> — игрок проходит
                    сквозь звезду, а касание поймает скрипт.
                </Step>
                <Step n="3">
                    Куб-обманка — примитив-<b>куб</b> синего цвета
                    <code> #3b82f6</code>. Поставь 5 штук вперемешку
                    со звёздами. Имена для них не важны — собирать их не надо.
                </Step>
                <Shot src="lesson14-scene.png"
                      caption="Звёзды-конусы и синие кубы-обманки на площадке" />
                <Note>
                    Очень важно, чтобы звёзды назывались именно
                    «Звезда_1» ... «Звезда_7» — главный скрипт ищет их
                    по этим именам, чтобы навесить тег.
                </Note>

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <p>
                    Главный скрипт сначала «наклеивает» тег на все звёзды,
                    а потом считает, сколько звёзд осталось.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="collect-by-tag" script="g14_main">{`// === ИГРА «СОБЕРИ ПО ТЕГАМ» — главный скрипт ===

game.ui.showText('Собери все ЖЁЛТЫЕ звёзды!', 3);

game.ui.score = 0;

// помечаем все звёзды тегом 'звезда'.
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже,
// поэтому простановку тегов делаем через game.after.
game.after(0.2, () => {
  for (let i = 1; i <= 7; i++) {
    const star = game.scene.findOne('Звезда_' + i);
    if (star) game.scene.tag(star, 'звезда');
  }
});

// Звёзды сообщают о сборе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('collected', () => {
  // getTagged вернёт все ещё не собранные звёзды
  const left = game.scene.getTagged('звезда').length;
  game.ui.score = 7 - left;
  game.sound.play('coin');
  if (left === 0) {
    game.ui.showText('Победа! Все звёзды собраны!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</CodeBoth>
                <p>Разберём построчно:</p>
                <ul>
                    <li>цикл <code>for</code> внутри <code>game.after(0.2, ...)</code>
                        проходит по звёздам с 1 по 7, находит каждую по имени
                        через <code>findOne</code> (небольшая задержка нужна,
                        чтобы снимок сцены успел появиться);</li>
                    <li><code>game.scene.tag(star, 'звезда')</code> — навешивает
                        на звезду тег-ярлычок «звезда»;</li>
                    <li><code>game.scene.getTagged('звезда')</code> — возвращает
                        <b> список</b> всех объектов с тегом «звезда». А
                        <code> .length</code> — это сколько их в списке;</li>
                    <li><code>7 - left</code> — если осталось 5 звёзд, значит
                        собрано 2 (всего 7 минус 5). Это и показываем в счёте;</li>
                    <li><code>if (left === 0)</code> — звёзд с тегом не осталось,
                        все собраны — победа.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Скрипт звезды</h3>
                <p>Этот скрипт повесь на <b>каждую</b> из 7 звёзд.</p>
                <ScriptKind kind="object" on="каждую звезду" />
                <CodeBoth game="collect-by-tag" script="g14_star_1">{`// === Скрипт звезды ===
game.self.onTouch(() => {
  // снимаем тег и удаляем звезду
  game.scene.untag(game.self.ref, 'звезда');
  game.self.delete();
  game.broadcast('collected');   // сообщаем главному скрипту о сборе
});`}</CodeBoth>
                <p>Что происходит при касании:</p>
                <ul>
                    <li><code>game.scene.untag(game.self.ref, 'звезда')</code> —
                        снимает тег «звезда» с этой звезды. Теперь
                        <code> getTagged</code> её больше не вернёт;</li>
                    <li><code>game.self.delete()</code> — звезда исчезает
                        со сцены;</li>
                    <li><code>game.broadcast('collected')</code> — шлёт
                        сообщение главному скрипту. Тот ловит его через
                        <code> game.onMessage('collected', ...)</code> и
                        пересчитывает остаток. Скрипты в разных «песочницах»
                        общаются только сообщениями.</li>
                </ul>
                <Note>
                    У синих кубов скрипта нет — поэтому коснуться их можно,
                    но ничего не произойдёт. Тег есть только у звёзд, кубы —
                    просто украшение-обманка.
                </Note>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <ul>
                    <li>собирай жёлтые звёзды — счёт растёт, звенит монетка;</li>
                    <li>синие кубы можно трогать — ничего не происходит;</li>
                    <li>собрал все 7 звёзд — «Победа» и конфетти.</li>
                </ul>

                <Try>
                    добавь второй тег — например, «бонус» на пару особых
                    красных звёзд, которые дают больше очков. Сделай больше
                    кубов-обманок. Покрась звёзды в разные цвета, но всё равно
                    помечай тегом «звезда».
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 15 — «Тир»
    // ════════════════════════════════════════════════════
    'shooting-range': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Настоящий тир: на постаментах стоят красные мишени-шары.
                    Нужно кликать по ним мышкой — за каждое попадание мишень
                    взрывается искрами и даёт очко. Выбей все 8 мишеней.
                </p>
                <Shot src="lesson15-result.png"
                      caption="Красные мишени на постаментах — кликай по ним" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Клик по 3D-объекту</b> — как сделать так, чтобы
                        объект реагировал на щелчок мышью
                        (<code>game.self.onClick</code>);</li>
                    <li><b>Счётчик попаданий</b> — считать, сколько мишеней
                        выбито;</li>
                    <li><b>Эффект-взрыв</b> — частицы при попадании.</li>
                </ul>
                <Note>
                    <code>onTouch</code> из прошлых уроков срабатывал, когда
                    игрок <b>касается</b> объекта телом. А <code>onClick</code> —
                    когда игрок <b>щёлкает</b> по объекту мышкой, даже издалека.
                    Для тира это как раз то, что нужно.
                </Note>

                <h3 className="lessonH">Шаг 1. Площадка тира</h3>
                <Step n="1">
                    Выложи прямоугольную площадку из каменных блоков — это
                    зал тира.
                </Step>

                <h3 className="lessonH">Шаг 2. Постаменты и мишени</h3>
                <Step n="1">
                    Постамент — примитив-<b>куб</b>, узкий и высокий
                    (размер 1×2×1), серого цвета. Поставь 8 постаментов
                    дальше от точки старта, чтобы было куда целиться.
                </Step>
                <Step n="2">
                    Мишень — примитив-<b>сфера</b> ярко-красного цвета
                    <code> #ff3030</code>, материал «Неон». Поставь по сфере
                    на каждый постамент, повыше. У мишеней «Столкновение»
                    выключи. Дай им имена «Мишень_2», «Мишень_4» и т.д.
                </Step>
                <Shot src="lesson15-scene.png"
                      caption="Восемь мишеней-сфер на постаментах" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="shooting-range" script="g15_main">{`// === ИГРА «ТИР» — главный скрипт ===

let score = 0;
const TOTAL = 8;

game.ui.score = 0;
game.ui.showText('Кликай по красным мишеням!', 3);

// Мишени сообщают о попадании через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('hit', () => {
  score = score + 1;
  game.ui.score = score;
  game.sound.play('hit');
  if (score >= TOTAL) {
    game.ui.showText('Победа! Все мишени выбиты!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</CodeBoth>
                <p>Здесь всё знакомо:</p>
                <ul>
                    <li><code>score</code> — счётчик попаданий;</li>
                    <li><code>TOTAL = 8</code> — всего мишеней;</li>
                    <li><code>game.onMessage('hit', ...)</code> — ловит
                        сообщение «hit» от мишеней. Каждая мишень шлёт его
                        через <code>game.broadcast('hit')</code>, когда по ней
                        попали. Внутри +1 к счёту и проверка: выбиты ли все 8.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Скрипт мишени</h3>
                <p>
                    Главное в этом уроке — повесь скрипт на <b>каждую</b>
                    мишень-сферу.
                </p>
                <ScriptKind kind="object" on="каждую мишень" />
                <CodeBoth game="shooting-range" script="g15_target_2">{`// === Скрипт мишени ===
// Клик по 3D-объекту = выстрел в него.

game.self.onClick(() => {
  // взрыв искр на месте мишени
  game.scene.spawnParticles('explosion', game.self.position,
    { count: 1, color: '#ff6633' });
  game.self.delete();   // мишень сбита
  game.broadcast('hit');   // сообщаем главному скрипту о попадании
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.self.onClick(() =&gt; {'{...}'})</code> —
                        срабатывает, когда игрок щёлкнул мышкой по этой мишени;</li>
                    <li><code>game.scene.spawnParticles('explosion', ...)</code> —
                        на месте мишени вспыхивает взрыв оранжевых искр;</li>
                    <li><code>game.self.position</code> — координаты самой
                        мишени, там и будет взрыв;</li>
                    <li><code>game.self.delete()</code> — мишень исчезает,
                        а <code>game.broadcast('hit')</code> шлёт сообщение
                        главному скрипту — тот добавляет очко. Скрипты в разных
                        «песочницах» общаются только сообщениями.</li>
                </ul>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <ul>
                    <li>наведи прицел на красную мишень и щёлкни мышкой;</li>
                    <li>мишень взрывается искрами, счёт растёт;</li>
                    <li>выбил все 8 мишеней — «Победа» и конфетти.</li>
                </ul>

                <Try>
                    сделай несколько мишеней маленькими — по ним сложнее
                    попасть. Добавь «вредную» чёрную мишень: попал по ней —
                    минус очко. Сделай, чтобы мишени двигались твином
                    из урока 4 — попасть будет труднее.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 16 — «Лава-пол»
    // ════════════════════════════════════════════════════
    'lava-floor': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Перед игроком — большое озеро раскалённой лавы. Идти
                    по лаве нельзя — она жжёт и отнимает здоровье. Нужно
                    перепрыгивать с одного каменного островка на другой,
                    пока не доберёшься до зелёного финиша на той стороне.
                </p>
                <Shot src="lesson16-result.png"
                      caption="Прыгай по островкам — в лаву падать нельзя" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Урон игроку</b> — как отнять здоровье
                        (<code>game.player.damage</code>);</li>
                    <li><b>Здоровье (HP)</b> — у игрока есть полоска жизни,
                        она кончилась — игрок воскресает на старте;</li>
                    <li><b>Невидимая зона-триггер</b> — объект, который не виден,
                        но ловит касание игрока.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Озеро лавы и старт</h3>
                <Step n="1">
                    Инструментом <kbd className="kbd">Блок</kbd> выбери блок
                    <b> лавы</b> и выложи большое прямоугольное озеро.
                </Step>
                <Step n="2">
                    В начале озера поставь поверх лавы небольшую
                    <b> стартовую площадку</b> из каменных блоков — отсюда
                    игрок начинает, тут безопасно.
                </Step>

                <h3 className="lessonH">Шаг 2. Островки и финиш</h3>
                <Step n="1">
                    Островок — примитив-<b>куб</b>, плоский и широкий
                    (размер примерно 2.4×0.6×2.4), серого цвета. Подними
                    его чуть над лавой. «Столкновение» и «Закреплён» включены.
                </Step>
                <Step n="2">
                    Дублируй островок и расставь дорожкой через всё озеро —
                    так, чтобы с каждого можно было допрыгнуть до следующего.
                </Step>
                <Step n="3">
                    В конце поставь зелёную неоновую финишную площадку,
                    имя «Финиш».
                </Step>
                <Shot src="lesson16-scene.png"
                      caption="Каменные островки дорожкой через озеро лавы" />

                <h3 className="lessonH">Шаг 3. Зона лавы</h3>
                <p>
                    Чтобы лава наносила урон, над озером нужен один большой
                    <b> невидимый</b> куб-зона. Он ловит касание игрока.
                </p>
                <Step n="1">
                    Поставь куб-примитив и растяни его так, чтобы он накрыл
                    всё озеро лавы. Тонкий по высоте.
                </Step>
                <Step n="2">
                    В инспекторе у этого куба <b>выключи «Видимый»</b> и
                    <b> выключи «Столкновение»</b>. Дай имя «ЛаваЗона».
                </Step>
                <Note>
                    Почему отдельная невидимая зона, а не сами блоки лавы?
                    На блоки скрипт повесить нельзя, а на примитив — можно.
                    Поэтому делаем один большой невидимый примитив-ловушку
                    поверх лавы.
                </Note>

                <h3 className="lessonH">Шаг 4. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="lava-floor" script="g16_main">{`// === ИГРА «ЛАВА-ПОЛ» — главный скрипт ===

let won = false;

game.ui.showText('Прыгай по островкам! Лава жжёт!', 3);

// если HP кончилось — игрок воскреснет на старте сам.
// следим за падением в лаву ниже уровня.
game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -2) {
    game.player.respawn();
  }
});

// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты перебрался через лаву!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <ul>
                    <li><code>game.onTick</code> следит, не провалился ли игрок
                        совсем низко — тогда возвращает его на старт;</li>
                    <li><code>game.onMessage('win', ...)</code> — победа.
                        Финиш шлёт сюда сообщение «win» через
                        <code> game.broadcast('win')</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 5. Скрипт лавы — главное</h3>
                <ScriptKind kind="object" on="невидимую зону «ЛаваЗона»" />
                <CodeBoth game="lava-floor" script="g16_lava">{`// === Скрипт лавы ===
// Игрок коснулся лавы — урон. У damage есть защита (i-frames),
// так что урон не каждый кадр, а раз в ~0.5 секунды.

game.self.onTouch(() => {
  game.player.damage(20);
  game.sound.play('hit');
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.player.damage(20)</code> — отнимает у игрока
                        20 единиц здоровья. Когда HP дойдёт до нуля, игрок
                        сам воскреснет на старте;</li>
                    <li>урон срабатывает не каждый кадр, а примерно раз
                        в полсекунды — это встроенная «защита от частого урона»
                        (по-английски i-frames). Иначе на лаве здоровье
                        улетало бы мгновенно;</li>
                    <li>звук <code>'hit'</code> — сигнал «ой, жжётся!».</li>
                </ul>

                <h3 className="lessonH">Шаг 6. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="lava-floor" script="g16_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');   // сообщаем главному скрипту о победе
});`}</CodeBoth>
                <ul>
                    <li>прыгай с островка на островок;</li>
                    <li>попал в лаву — здоровье тает, слышен звук урона;</li>
                    <li>здоровье кончилось — воскресаешь на старте;</li>
                    <li>добрался до зелёного финиша — «Победа».</li>
                </ul>

                <Try>
                    сделай урон лавы посильнее (<code>damage(35)</code>) —
                    будет сложнее. Добавь по пути «аптечку» — зелёную сферу,
                    касание которой лечит через <code>game.player.heal</code>.
                    Сделай острова поменьше — прыгать точнее.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 17 — «Ключ и сундук»
    // ════════════════════════════════════════════════════
    'key-chest': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На поляне среди зелёных кустов спрятан золотой ключ.
                    Игрок ищет ключ, подбирает его, а потом подходит
                    к сундуку и открывает его клавишей <kbd className="kbd">E</kbd>.
                    Без ключа сундук не откроется.
                </p>
                <Shot src="lesson17-result.png"
                      caption="Найди ключ среди кустов и открой сундук" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Инвентарь</b> — у игрока есть «рюкзак», куда можно
                        класть предметы (<code>game.inventory.add</code>);</li>
                    <li><b>Проверка предмета</b> — есть ли нужная вещь
                        в инвентаре (<code>game.inventory.has</code>);</li>
                    <li><b>Взаимодействие по E</b> — открыть сундук
                        (<code>game.self.onInteract</code>).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поляна и кусты</h3>
                <Step n="1">
                    Выложи большую квадратную травяную площадку — это поляна.
                </Step>
                <Step n="2">
                    Куст — примитив-<b>куб</b> зелёного цвета
                    <code> #2f7d32</code>. Поставь 5-6 кустов вразброс
                    по поляне — среди них и спрячется ключ.
                </Step>

                <h3 className="lessonH">Шаг 2. Ключ и сундук</h3>
                <Step n="1">
                    Ключ — примитив-<b>тор</b> (колечко) жёлтого цвета
                    <code> #ffd700</code>, материал «Неон». Положи его
                    где-нибудь в уголке, рядом с кустом. «Столкновение»
                    у ключа выключи. Имя — «Ключ».
                </Step>
                <Step n="2">
                    Сундук — примитив-<b>куб</b> коричневого цвета. Поставь
                    его в центре поляны. Имя — «Сундук».
                </Step>
                <Shot src="lesson17-scene.png"
                      caption="Кусты, спрятанный ключ-колечко и сундук" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="key-chest" script="g17_main">{`// === ИГРА «КЛЮЧ И СУНДУК» — главный скрипт ===

game.ui.showText('Найди ключ и открой сундук!', 3);

// Ключ и сундук сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.

// игрок подобрал ключ
game.onMessage('takeKey', () => {
  game.inventory.add({ name: 'Ключ' });
  game.ui.showText('Ты нашёл Ключ!', 2);
  game.sound.play('pickup');
});

// игрок пытается открыть сундук
game.onMessage('openChest', () => {
  if (!game.inventory.has('Ключ')) {
    game.ui.showText('Сундук заперт. Сначала найди ключ.', 2);
    return;
  }
  game.ui.showText('Победа! Сундук открыт — там сокровище!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Здесь два обработчика сообщений:</p>
                <ul>
                    <li><code>game.onMessage('takeKey', ...)</code> — игрок
                        нашёл ключ. <code>game.inventory.add({'{ name: '}'Ключ'{' }'})</code>
                        кладёт предмет «Ключ» в рюкзак игрока;</li>
                    <li><code>game.onMessage('openChest', ...)</code> — попытка
                        открыть сундук. Сначала <code>game.inventory.has('Ключ')</code>
                        проверяет, есть ли ключ в рюкзаке. Если нет — сообщение
                        «Сундук заперт» и <code>return</code> прерывает функцию;</li>
                    <li>если ключ есть — победа, летит конфетти;</li>
                    <li>сообщения «takeKey» и «openChest» шлют скрипты ключа
                        и сундука через <code>game.broadcast(...)</code> —
                        скрипты в разных «песочницах» общаются только
                        сообщениями.</li>
                </ul>
                <Note>
                    <code>return</code> — это «выйти из функции прямо сейчас».
                    Если ключа нет, дальше код просто не выполняется — сундук
                    остаётся закрытым.
                </Note>

                <h3 className="lessonH">Шаг 4. Скрипт ключа</h3>
                <ScriptKind kind="object" on="ключ" />
                <CodeBoth game="key-chest" script="g17_key">{`// === Скрипт ключа ===
game.self.onTouch(() => {
  game.broadcast('takeKey');   // сообщаем главному скрипту: ключ найден
  game.self.delete();   // ключ подобран
});`}</CodeBoth>
                <p>
                    Игрок коснулся ключа — скрипт шлёт сообщение
                    <code> game.broadcast('takeKey')</code> (главный скрипт
                    положит ключ в инвентарь), а <code>game.self.delete()</code>
                    убирает ключ с поляны: он же теперь в рюкзаке.
                </p>

                <h3 className="lessonH">Шаг 5. Скрипт сундука</h3>
                <ScriptKind kind="object" on="сундук" />
                <CodeBoth game="key-chest" script="g17_chest">{`// === Скрипт сундука ===
game.self.onInteract(() => {
  game.broadcast('openChest');   // сообщаем главному скрипту: открыть сундук
}, { text: 'Открыть сундук', distance: 4 });`}</CodeBoth>
                <p>
                    Когда игрок подходит ближе чем на 4 метра, над сундуком
                    появляется подсказка «Открыть сундук». Нажатие
                    <kbd className="kbd">E</kbd> шлёт сообщение «openChest» —
                    а главный скрипт уже проверит, есть ли ключ.
                </p>

                <h3 className="lessonH">Шаг 6. Проверка</h3>
                <ul>
                    <li>подойди к сундуку без ключа, нажми
                        <kbd className="kbd">E</kbd> — «Сундук заперт»;</li>
                    <li>найди ключ-колечко, коснись его — «Ты нашёл Ключ»;</li>
                    <li>вернись к сундуку, нажми <kbd className="kbd">E</kbd> —
                        «Победа», конфетти.</li>
                </ul>

                <Try>
                    спрячь <b>два</b> ключа, и пусть сундук открывается только
                    когда оба в инвентаре. Сделай ключ совсем маленьким — искать
                    труднее. Добавь второй сундук с другой наградой.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 18 — «Качели»
    // ════════════════════════════════════════════════════
    'swing': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Между двумя площадками висят большие качели. Они сами
                    раскачиваются туда-сюда. Игрок запрыгивает на качели
                    с возвышенности и, прокатившись на них, перебирается
                    на финишную площадку.
                </p>
                <Shot src="lesson18-result.png"
                      caption="Качели раскачиваются — прокатись на них" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Констрейнт-петля (шарнир)</b> — как закрепить объект
                        на оси, чтобы он мог поворачиваться вокруг неё
                        (<code>game.constraints.hinge</code>);</li>
                    <li><b>Управлять углом петли</b> — <code>hinge.setAngle</code>;</li>
                    <li><b>Повторяющийся таймер</b> — <code>game.every</code>
                        для раскачивания.</li>
                </ul>
                <Note>
                    Констрейнт — это «связь» или «крепление». Петля-шарнир
                    (по-английски hinge) — это как петля у двери: объект не падает
                    и не уезжает, а только поворачивается вокруг оси. Качели
                    как раз качаются вокруг своей оси.
                </Note>

                <h3 className="lessonH">Шаг 1. Площадки</h3>
                <Step n="1">
                    Сделай <b>стартовую возвышенность</b> — небольшую башенку
                    из каменных блоков высотой 2-3 блока. С неё игрок запрыгнет
                    на качели.
                </Step>

                <h3 className="lessonH">Шаг 2. Качели и финиш</h3>
                <Step n="1">
                    Качели — примитив-<b>куб</b>, широкий и плоский
                    (размер примерно 4×0.5×3), коричневого цвета. Подними его
                    в воздух на уровень возвышенности. Имя — «Качели».
                </Step>
                <Step n="2">
                    По другую сторону от качелей поставь зелёную неоновую
                    финишную площадку. Имя — «Финиш».
                </Step>
                <Shot src="lesson18-scene.png"
                      caption="Возвышенность, качели и финиш по другую сторону" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <p>
                    Главный скрипт делает качели «настоящими»: вешает их
                    на петлю и раскачивает.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="swing" script="g18_main">{`// === ИГРА «КАЧЕЛИ» — главный скрипт ===

let won = false;

game.ui.showText('Запрыгни на качели и прокатись!', 3);

// делаем качели на петле и раскачиваем их.
// ВАЖНО: game.scene.findOne нельзя звать сразу в начале скрипта —
// снимок сцены приходит чуть позже. Ждём 0.2с через game.after.
let hinge = null;
let dir = -35;
game.after(0.2, () => {
  const swing = game.scene.findOne('Качели');
  hinge = game.constraints.hinge(swing, {
    pivotX: 0, pivotZ: 0,   // ось — посередине качелей
    angle: 35
  });
  // раскачиваем туда-сюда каждые 1.4 секунды
  game.every(1.4, () => {
    if (won || !hinge) return;
    hinge.setAngle(dir);
    dir = -dir;
  });
});

game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -3) game.player.respawn();
});

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты перебрался на качелях!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Разберём по частям:</p>
                <ul>
                    <li><code>game.after(0.2, ...)</code> — ждём 0.2 секунды
                        перед поиском качелей. Снимок сцены приходит чуть позже
                        старта, поэтому <code>findOne</code> сразу позвать
                        нельзя — качелей ещё «не видно»;</li>
                    <li><code>game.scene.findOne('Качели')</code> — находим
                        качели по имени;</li>
                    <li><code>game.constraints.hinge(swing, опции)</code> —
                        вешаем качели на петлю-шарнир. <code>pivotX</code> и
                        <code> pivotZ</code> — где ось вращения (0, 0 — точно
                        посередине качелей);</li>
                    <li><code>hinge.setAngle(dir)</code> — поворачивает качели
                        на заданный угол. -35 — наклон в одну сторону,
                        35 — в другую;</li>
                    <li><code>dir = -dir</code> — меняет знак: было -35, стало 35,
                        потом снова -35. Так качели качаются туда-сюда;</li>
                    <li><code>game.every(1.4, ...)</code> — каждые 1.4 секунды
                        меняем угол — вот и раскачивание.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
                <p>
                    Финиш ловит касание игрока и шлёт сообщение
                    <code> 'win'</code> — главный скрипт его поймает и покажет
                    победу.
                </p>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="swing" script="g18_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <ul>
                    <li>качели сами качаются туда-сюда;</li>
                    <li>запрыгни на них с возвышенности, поймай момент;</li>
                    <li>перебрался на зелёную площадку — «Победа».</li>
                </ul>

                <Try>
                    сделай качели быстрее — поменяй 1.4 на 0.9 секунды.
                    Увеличь угол наклона до 50 — размах станет больше.
                    Поставь подряд <b>двое</b> качелей — перепрыгивай с одних
                    на другие.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 19 — «Лифт»
    // ════════════════════════════════════════════════════
    'elevator': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    В комнате два этажа — нижний и верхний. Между ними ездит
                    синяя платформа-лифт: сама поднимается наверх и
                    опускается вниз, без остановки. Игрок встаёт на лифт,
                    дожидается верха и сходит на финиш.
                </p>
                <Shot src="lesson19-result.png"
                      caption="Синий лифт сам ездит вверх-вниз" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Твин с повтором</b> — как заставить движение
                        повторяться много раз (<code>repeat</code>);</li>
                    <li><b>Yoyo</b> — как сделать, чтобы объект ехал туда,
                        а потом сам возвращался обратно;</li>
                    <li><b>Постоянное движение</b> — лифт ездит «вечно».</li>
                </ul>
                <Note>
                    Слово «yoyo» — от игрушки йо-йо, которая то опускается,
                    то поднимается. В твине <code>yoyo: true</code> означает:
                    доехал до конца — поезжай обратно. А <code>repeat</code> —
                    сколько раз так повторить.
                </Note>

                <h3 className="lessonH">Шаг 1. Два этажа</h3>
                <Step n="1">
                    Выложи из каменных блоков <b>нижний этаж</b> — пол комнаты.
                </Step>
                <Step n="2">
                    Сбоку, высоко над полом (примерно на высоте 12 блоков),
                    выложи <b>верхний этаж</b> — площадку, до которой нужно
                    добраться.
                </Step>

                <h3 className="lessonH">Шаг 2. Лифт и финиш</h3>
                <Step n="1">
                    Лифт — примитив-<b>куб</b>, плоский и широкий
                    (размер примерно 3.5×0.5×3.5), синего цвета
                    <code> #3357ff</code>, материал «Неон». Поставь его
                    на нижнем этаже. «Столкновение» и «Закреплён» включены.
                    Имя — «Лифт».
                </Step>
                <Step n="2">
                    На верхнем этаже поставь зелёную неоновую финишную
                    площадку. Имя — «Финиш».
                </Step>
                <Shot src="lesson19-scene.png"
                      caption="Нижний этаж, лифт и верхний этаж с финишем" />

                <h3 className="lessonH">Шаг 3. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="elevator" script="g19_main">{`// === ИГРА «ЛИФТ» — главный скрипт ===

let won = false;

game.ui.showText('Встань на синий лифт — он повезёт наверх', 3);

// лифт вечно ездит: вниз (y=1) ↔ верх (y=12.3).
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже.
game.after(0.2, () => {
  const lift = game.scene.findOne('Лифт');
  game.tween(lift, { y: 12.3 }, {
    duration: 3.5,
    yoyo: true,       // обратно вниз
    repeat: 999,      // почти бесконечно
    easing: 'ease'
  });
});

game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -3) game.player.respawn();
});

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты поднялся на лифте!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Разберём твин лифта:</p>
                <ul>
                    <li><code>game.after(0.2, ...)</code> — ждём 0.2 секунды:
                        снимок сцены приходит чуть позже старта, поэтому
                        <code> findOne</code> сразу позвать нельзя;</li>
                    <li><code>game.scene.findOne('Лифт')</code> — находим лифт
                        по имени;</li>
                    <li><code>game.tween(lift, {'{ y: 12.3 }'}, опции)</code> —
                        плавно поднимает лифт на высоту 12.3;</li>
                    <li><code>duration: 3.5</code> — подъём занимает 3.5 секунды;</li>
                    <li><code>yoyo: true</code> — доехав до верха, лифт сам
                        поедет обратно вниз;</li>
                    <li><code>repeat: 999</code> — повторить этот путь 999 раз,
                        то есть практически бесконечно — лифт ездит всю игру;</li>
                    <li><code>easing: 'ease'</code> — лифт мягко разгоняется
                        и тормозит, а не дёргается.</li>
                </ul>

                <h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
                <p>
                    Финиш ловит касание игрока и шлёт сообщение
                    <code> 'win'</code>. Главный скрипт ловит его через
                    <code> game.onMessage('win', ...)</code> — так два скрипта
                    из разных «песочниц» общаются.
                </p>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="elevator" script="g19_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <ul>
                    <li>лифт ездит вверх-вниз сам по себе;</li>
                    <li>встань на него внизу и дождись верха;</li>
                    <li>сойди на верхний этаж, встань на финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай лифт медленнее (<code>duration: 5</code>) или
                    быстрее. Добавь второй лифт, который ездит в другую
                    сторону. Поставь на верхнем этаже монетки из урока 1.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 20 — «Имена над врагами»
    // ════════════════════════════════════════════════════
    'enemy-names': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На арене стоят три врага: Гоблин, Скелет и Орк. Над
                    головой каждого висит метка с его именем и здоровьем.
                    Игрок подходит к врагу, кликает по нему — наносит урон,
                    и метка показывает, сколько HP осталось. Победи всех троих.
                </p>
                <Shot src="lesson20-result.png"
                      caption="Враги с именами и HP над головой" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Метки-billboard</b> — надписи, которые висят
                        над объектом и всегда повёрнуты к игроку
                        (<code>game.scene.setLabel</code>);</li>
                    <li><b>Убрать метку</b> — <code>game.scene.clearLabel</code>;</li>
                    <li><b>NPC-враги</b> — создавать врагов и наносить им урон;</li>
                    <li><b>Обновление метки</b> — менять текст метки, когда
                        здоровье врага меняется.</li>
                </ul>
                <Note>
                    Billboard-метка — это надпись в 3D-мире, которая всегда
                    «смотрит лицом» на игрока, как бы он ни ходил вокруг.
                    Так в играх показывают имена и полоски HP над персонажами.
                </Note>

                <h3 className="lessonH">Шаг 1. Арена</h3>
                <Step n="1">
                    Выложи большую квадратную площадку из каменных блоков —
                    это арена.
                </Step>
                <p>
                    Врагов ставить руками не нужно — они появятся из скрипта
                    командой <code>spawnNpc</code>, как торговец в уроке 13.
                </p>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Вся игра в одном скрипте. Он создаёт врагов, вешает
                    над ними метки и обрабатывает удары.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="enemy-names" script="g20_main">{`// === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт ===

game.ui.showText('Победи всех врагов! Кликай по ним', 3);

// данные врагов: имя и позиция
const enemyData = [
  { name: 'Гоблин',  x: -5, z: 3,  hp: 60 },
  { name: 'Скелет',  x: 4,  z: 5,  hp: 80 },
  { name: 'Орк',     x: 0,  z: 8,  hp: 100 },
];

let alive = enemyData.length;

enemyData.forEach((d) => {
  // создаём NPC-врага
  const npc = game.scene.spawnNpc('character-b', {
    x: d.x, y: 1, z: d.z,    // y=1 — на верху блоков пола
    name: d.name, hp: d.hp, speed: 0,
  });

  // вешаем над врагом метку-billboard с именем и HP
  let hp = d.hp;
  function updateLabel() {
    game.scene.setLabel(npc.ref, d.name + '  HP: ' + hp, {
      color: '#ff5555', height: 3
    });
  }
  updateLabel();

  // когда враг гибнет — убираем метку
  npc.onDeath(() => {
    game.scene.clearLabel(npc.ref);
    alive = alive - 1;
    game.sound.play('hit');
    if (alive <= 0) {
      game.ui.showText('Победа! Все враги повержены!', 5);
      game.sound.play('win');
      const p = game.player.position;
      game.scene.spawnParticles('confetti',
        { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
    }
  });

  // урон врагу по клику игрока рядом — упрощённо бьём раз в клик
  // (в этой игре кликаем по сцене возле врага)
  game.onClick(() => {
    if (hp <= 0) return;
    // бьём только если игрок близко к этому врагу
    const p = game.player.position;
    if (!p || !npc.position) return;
    const dist = Math.hypot(p.x - npc.position.x, p.z - npc.position.z);
    if (dist < 4) {
      hp = hp - 30;
      if (hp < 0) hp = 0;
      npc.damage(30);
      updateLabel();
      game.scene.spawnParticles('sparks', npc.position, { duration: 0.4 });
    }
  });
});`}</CodeBoth>
                <p>Разберём по частям. Сначала про создание врагов:</p>
                <ul>
                    <li><code>enemyData</code> — список врагов: у каждого имя,
                        координаты и запас здоровья;</li>
                    <li><code>enemyData.forEach((d) =&gt; {'{...}'})</code> —
                        перебираем список и для каждого делаем одно и то же;</li>
                    <li><code>game.scene.spawnNpc('character-b', ...)</code> —
                        создаёт NPC-врага. <code>speed: 0</code> — враг стоит
                        на месте.</li>
                </ul>
                <p>Теперь про метку над врагом:</p>
                <ul>
                    <li><code>game.scene.setLabel(npc.ref, текст, опции)</code> —
                        вешает над врагом надпись. Текст склеен из имени и HP:
                        получится «Гоблин  HP: 60»;</li>
                    <li><code>color</code> — цвет надписи, <code>height</code> —
                        на какой высоте над врагом она висит;</li>
                    <li><code>updateLabel()</code> — функция, которая заново
                        пишет метку. Зовём её каждый раз, когда HP изменилось.</li>
                </ul>
                <p>Про удар и гибель:</p>
                <ul>
                    <li><code>game.onClick</code> — игрок щёлкнул мышью.
                        <code> Math.hypot(...)</code> считает расстояние
                        до врага: бьём, только если ближе 4 метров;</li>
                    <li><code>npc.damage(30)</code> — наносим врагу 30 урона,
                        <code> updateLabel()</code> обновляет цифру HP в метке;</li>
                    <li><code>npc.onDeath(...)</code> — когда враг погиб,
                        <code> game.scene.clearLabel(npc.ref)</code> убирает
                        его метку, а счётчик <code>alive</code> уменьшается;</li>
                    <li>когда <code>alive</code> дошёл до нуля — все враги
                        повержены, победа.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>над каждым врагом видна метка с именем и HP;</li>
                    <li>подойди к врагу ближе и кликай — HP в метке падает;</li>
                    <li>HP дошло до нуля — враг гибнет, метка пропадает;</li>
                    <li>победил всех троих — «Победа» и конфетти.</li>
                </ul>

                <Try>
                    добавь четвёртого врага в список <code>enemyData</code> —
                    просто допиши строчку. Сделай врагов посильнее (больше HP).
                    Поменяй цвет метки в зависимости от здоровья: зелёная,
                    пока HP высокое, красная — когда мало.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 21 — «Преследователь»
    // ════════════════════════════════════════════════════
    'chaser': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    По полю за тобой гонится злой NPC-охотник. Он бегает
                    чуть быстрее обычного и не отстаёт. Нужно петлять между
                    каменными кубами-укрытиями и добежать до зелёного
                    укрытия-финиша. Поймал — игра возвращает на старт.
                </p>
                <Shot src="lesson21-result.png"
                      caption="Охотник гонится — добеги до зелёного укрытия" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>NPC-персонаж</b> — как создать живого героя
                        скриптом (<code>game.scene.spawnNpc</code>);</li>
                    <li><b>Преследование</b> — как заставить NPC гнаться
                        за игроком (<code>enemy.follow('player')</code>);</li>
                    <li><b>Расстояние между точками</b> — как узнать, далеко
                        ли враг (<code>Math.hypot</code>).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле и укрытия</h3>
                <Step n="1">
                    Инструментом <kbd className="kbd">Блок</kbd> выложи
                    большое травяное поле — широкое и длинное, чтобы было
                    где убегать.
                </Step>
                <Step n="2">
                    Поставь несколько больших серых кубов-примитивов
                    (3×3×3) — это укрытия, за которыми ты будешь петлять,
                    пока враг их обегает. «Столкновение» и «Закреплён»
                    включены.
                </Step>
                <Step n="3">
                    В дальнем конце поля положи зелёную неоновую площадку —
                    финиш. Имя — «Финиш». Точку спавна поставь в начале.
                </Step>
                <Shot src="lesson21-scene.png"
                      caption="Поле с серыми укрытиями и зелёным финишем" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт создаёт врага, запускает погоню и следит,
                    не догнал ли он игрока.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="chaser" script="g21_main">{`// === ИГРА «ПРЕСЛЕДОВАТЕЛЬ» — главный скрипт ===

let won = false;

game.ui.showText('Убегай от врага! Добеги до укрытия!', 3);

// создаём NPC-преследователя
const enemy = game.scene.spawnNpc('character-b', {
  x: 0, y: 1, z: -3,    // y=1 — на верху блоков пола (иначе утоплен)
  name: 'Охотник', hp: 100, speed: 4,
});
// враг постоянно гонится за игроком
enemy.follow('player');

// каждый кадр проверяем — не догнал ли враг.
// enemy.position наполняется не сразу после spawnNpc (NPC появляется
// через кадр) — пока позиции нет, пропускаем кадр.
game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  const e = enemy.position;
  if (!p || !e) return;
  const dist = Math.hypot(p.x - e.x, p.z - e.z);
  if (dist < 1.6) {
    // враг поймал — на старт
    game.player.respawn();
    game.ui.showText('Пойман! Беги снова!', 2);
    game.sound.play('lose');
  }
});

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  enemy.stop();
  game.ui.showText('Победа! Ты убежал от врага!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Разберём построчно:</p>
                <ul>
                    <li><code>game.scene.spawnNpc('character-b', опции)</code> —
                        создаёт NPC прямо во время игры. В опциях — где
                        появится, имя, здоровье и <code>speed</code> —
                        скорость бега;</li>
                    <li><code>enemy.follow('player')</code> — командует NPC
                        «гонись за игроком». Дальше враг сам бежит за тобой
                        каждый кадр;</li>
                    <li><code>Math.hypot(p.x - e.x, p.z - e.z)</code> —
                        расстояние между игроком и врагом. Если оно меньше
                        1.6 — враг вплотную, значит поймал;</li>
                    <li><code>enemy.stop()</code> в победе — враг замирает,
                        погоня закончена.</li>
                </ul>
                <Note>
                    Скорость врага <code>speed: 4</code> чуть больше скорости
                    обычной ходьбы. Если убегать слишком легко — увеличь её,
                    слишком сложно — уменьши.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="chaser" script="g21_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <p>
                    Когда игрок касается финиша, скрипт шлёт сообщение
                    <code> game.broadcast('win')</code>. Главный скрипт ловит
                    его через <code>game.onMessage('win', ...)</code> —
                    останавливает врага и показывает «Победа». Так два скрипта
                    из разных «песочниц» общаются сообщениями.
                </p>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>с началом игры за тобой бежит охотник;</li>
                    <li>петляй между серыми кубами, чтобы враг отстал;</li>
                    <li>поймал — респаун на старте;</li>
                    <li>добежал до зелёного укрытия — «Победа».</li>
                </ul>

                <Try>
                    добавь второго врага — просто создай ещё один NPC
                    и тоже дай ему <code>follow('player')</code>. Сделай
                    укрытий побольше. Спрячь по полю монетки из урока 1 —
                    собирай их на бегу.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 22 — «Зона опасности»
    // ════════════════════════════════════════════════════
    'danger-zone': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Длинный коридор, а посередине — большая красная зона.
                    Пока игрок внутри неё, у него тает здоровье. Перед зоной
                    лежит зелёная аптечка — подбери её, чтобы пополнить HP,
                    и пробеги опасный участок до финиша.
                </p>
                <Shot src="lesson22-result.png"
                      caption="Красная зона снимает HP — пробеги её быстро" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Вход и выход из зоны</b> — события
                        <code> onTouch</code> и <code>onUntouch</code>;</li>
                    <li><b>Урон игроку</b> — <code>game.player.damage</code>;</li>
                    <li><b>Лечение</b> — <code>game.player.heal</code>;</li>
                    <li><b>Повторяющийся таймер</b> — <code>game.every</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Коридор, зона и аптечка</h3>
                <Step n="1">
                    Выложи длинный пол-коридор из камня.
                </Step>
                <Step n="2">
                    Посередине поставь большой красный куб-примитив, материал
                    «Неон». «Столкновение» <b>выключи</b> — игрок проходит
                    сквозь него, а вход поймает скрипт. Имя — «ЗонаОпасности».
                </Step>
                <Step n="3">
                    Перед зоной сбоку положи маленькую зелёную сферу —
                    аптечку. «Столкновение» выключено, имя — «Аптечка».
                </Step>
                <Step n="4">
                    В конце коридора — зелёная неоновая площадка-финиш,
                    имя «Финиш».
                </Step>
                <Shot src="lesson22-scene.png"
                      caption="Коридор с красной зоной, аптечкой и финишем" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="danger-zone" script="g22_main">{`// === ИГРА «ЗОНА ОПАСНОСТИ» — главный скрипт ===

let inZone = false;       // игрок сейчас в красной зоне?
let won = false;

game.ui.showText('Пробеги через красную зону к финишу!', 3);

// пока игрок в зоне — каждые 0.6с снимаем HP
game.every(0.6, () => {
  if (won) return;
  if (inZone) {
    game.player.damage(12);
    game.sound.play('hit');
  }
});

// зона и финиш шлют сюда сообщения через broadcast — скрипты
// в разных песочницах, общие переменные между ними не видны.
game.onMessage('zone-enter', () => {
  inZone = true;
  game.ui.showText('Опасно! Беги быстрее!', 1.5);
});
game.onMessage('zone-leave', () => { inZone = false; });
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты прошёл зону опасности!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>inZone</code> — флажок «игрок внутри красной
                        зоны». Скрипт зоны включает и выключает его
                        сообщениями;</li>
                    <li><code>game.every(0.6, fn)</code> — каждые 0.6 секунды
                        выполняет функцию. Внутри: если игрок в зоне —
                        <code> game.player.damage(12)</code> снимает 12 HP;</li>
                    <li><code>game.onMessage('zone-enter', ...)</code> и
                        <code> game.onMessage('zone-leave', ...)</code> — ловят
                        сообщения от зоны: «игрок вошёл» и «вышел». Зона шлёт
                        их через <code>game.broadcast</code>, потому что её
                        скрипт работает в отдельной «песочнице» и не видит
                        переменную <code>inZone</code> напрямую;</li>
                    <li><code>game.onMessage('win', ...)</code> — ловит
                        сообщение от финиша.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт зоны опасности</h3>
                <ScriptKind kind="object" on="красную зону" />
                <CodeBoth game="danger-zone" script="g22_zone">{`// === Скрипт зоны опасности ===
// onTouch — игрок вошёл, onUntouch — вышел.

game.self.onTouch(() => {
  game.broadcast('zone-enter');
});
game.self.onUntouch(() => {
  game.broadcast('zone-leave');
});`}</CodeBoth>
                <p>
                    Здесь важна <b>пара событий</b>: <code>onTouch</code>
                    срабатывает, когда игрок входит в зону, а
                    <code> onUntouch</code> — когда выходит. Так зона точно
                    знает, внутри игрок или нет.
                </p>

                <h3 className="lessonH">Шаг 4. Скрипт аптечки</h3>
                <ScriptKind kind="object" on="зелёную аптечку" />
                <CodeBoth game="danger-zone" script="g22_heal">{`// === Скрипт аптечки ===
game.self.onTouch(() => {
  game.player.heal(60);
  game.ui.showText('+60 HP', 1.5);
  game.sound.play('pickup');
  game.self.delete();
});`}</CodeBoth>
                <p>
                    <code>game.player.heal(60)</code> — добавляет игроку
                    60 единиц здоровья. Аптечку взяли один раз —
                    <code> game.self.delete()</code> убирает её.
                </p>

                <h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="danger-zone" script="g22_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <ul>
                    <li>подбери зелёную аптечку — +60 HP;</li>
                    <li>в красной зоне здоровье тает каждые 0.6 секунды;</li>
                    <li>выбежал из зоны — урон прекратился;</li>
                    <li>добежал до финиша живым — «Победа».</li>
                </ul>

                <Try>
                    сделай зону опаснее — снимай больше HP или чаще. Поставь
                    несколько аптечек по пути. Добавь вторую зону другого
                    цвета, которая, наоборот, лечит, пока ты в ней стоишь.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 23 — «Переключатели»
    // ════════════════════════════════════════════════════
    'switches': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Перед стеной с дверью стоят три красных рычага. Дёргать
                    их (клавишей <kbd className="kbd">E</kbd>) нужно в
                    правильном порядке. Угадал последовательность — дверь
                    уезжает вверх. Ошибся — все рычаги сбрасываются, начинай
                    заново.
                </p>
                <Shot src="lesson23-result.png"
                      caption="Три рычага — дёрни их в нужном порядке" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Проверка последовательности</b> — как сравнить,
                        что нажал игрок, с правильным порядком;</li>
                    <li><b>Взаимодействие по E</b> — <code>game.self.onInteract</code>;</li>
                    <li><b>Массив-список</b> — как копить нажатия и сбрасывать
                        их при ошибке.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Стена, дверь и рычаги</h3>
                <Step n="1">
                    Сделай пол комнаты из камня.
                </Step>
                <Step n="2">
                    Поперёк комнаты построй стену из двух кубов с проёмом
                    в центре. В проём поставь куб-дверь, имя — «Дверь».
                </Step>
                <Step n="3">
                    Перед дверью поставь три рычага — высокие красные
                    неоновые цилиндры. Имена — «Рычаг_1», «Рычаг_2»,
                    «Рычаг_3».
                </Step>
                <Step n="4">
                    За дверью — зелёная неоновая площадка-финиш.
                </Step>
                <Shot src="lesson23-scene.png"
                      caption="Три рычага перед стеной с дверью" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт хранит правильный порядок и проверяет
                    каждое нажатие.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="switches" script="g23_main">{`// === ИГРА «ПЕРЕКЛЮЧАТЕЛИ» — главный скрипт ===

// правильный порядок рычагов
const ORDER = [2, 3, 1];
let pressed = [];
let opened = false;

game.ui.showText('Дёрни рычаги в нужном порядке (E)', 4);

// рычаги дёргаются через broadcast('lever', { num }) — скрипты
// в разных песочницах, состояние pressed[] живёт только здесь.
game.onMessage('lever', (d) => {
  const n = d.num;
  if (opened) return;
  game.sound.play('click');
  pressed.push(n);
  const i = pressed.length - 1;
  if (pressed[i] !== ORDER[i]) {
    pressed = [];
    game.ui.showText('Неверно! Рычаги сброшены.', 1.5);
    game.sound.play('lose');
    return;
  }
  if (pressed.length === ORDER.length) {
    opened = true;
    game.ui.showText('Верно! Дверь открыта.', 3);
    game.sound.play('win');
    const door = game.scene.findOne('Дверь');
    game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
  } else {
    game.ui.showText('Так держать!', 1);
  }
});

game.onMessage('win', () => {
  game.ui.showText('Победа! Ты разгадал порядок!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Самое интересное — проверка порядка:</p>
                <ul>
                    <li><code>ORDER = [2, 3, 1]</code> — секретный порядок:
                        сначала второй рычаг, потом третий, потом первый.
                        Поменяй числа на свой порядок;</li>
                    <li><code>pressed.push(n)</code> — добавляет нажатый рычаг
                        в список того, что игрок уже дёрнул;</li>
                    <li><code>if (pressed[i] !== ORDER[i])</code> — сравниваем
                        последнее нажатие с тем, что должно быть. Не совпало —
                        <code> pressed = []</code> очищает список, начинай
                        заново;</li>
                    <li><code>if (pressed.length === ORDER.length)</code> —
                        все три рычага дёрнуты правильно — дверь уезжает
                        вверх через <code>game.tween</code>.</li>
                </ul>
                <Note>
                    Идея «копим список и сверяем по шагам» — та же, что
                    в игре «Дверь по коду» (урок 12). Так проверяют любые
                    комбинации: коды, пароли, последовательности.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт рычага</h3>
                <p>
                    На каждый рычаг свой скрипт — отличается только номер.
                    Вот скрипт первого рычага:
                </p>
                <ScriptKind kind="object" on="Рычаг_1" />
                <CodeBoth game="switches" script="g23_lever_1">{`// === Скрипт рычага 1 ===
game.self.onInteract(() => {
  game.broadcast('lever', { num: 1 });
}, { text: 'Дёрнуть рычаг 1', distance: 3 });`}</CodeBoth>
                <p>
                    Рычаг шлёт сообщение <code>game.broadcast('lever', {'{ num: 1 }'})</code>:
                    имя сообщения — <code>'lever'</code>, а второй кусок —
                    «посылка» с номером рычага. Главный скрипт ловит её
                    через <code>game.onMessage('lever', ...)</code> и узнаёт
                    номер из <code>d.num</code>.
                </p>
                <p>
                    На «Рычаг_2» — такой же скрипт, но с
                    <code> num: 2</code> и текстом «Дёрнуть рычаг 2»,
                    на «Рычаг_3» — с <code>num: 3</code>. Номер сообщает
                    главному скрипту, какой именно рычаг дёрнули.
                </p>

                <h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="switches" script="g23_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <ul>
                    <li>подойди к рычагу — появится подсказка «Дёрнуть рычаг»;</li>
                    <li>дёргай <kbd className="kbd">E</kbd> в верном
                        порядке — дверь откроется;</li>
                    <li>ошибся — «Неверно», начинай заново;</li>
                    <li>прошёл в проём, встал на финиш — «Победа».</li>
                </ul>

                <Try>
                    добавь четвёртый рычаг и удлини <code>ORDER</code>.
                    Сделай рычаги одного цвета, чтобы порядок было сложнее
                    угадать. Покрась рычаг в зелёный, когда его дёрнули
                    правильно — через <code>game.scene.setColor</code>.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 24 — «Падающий мост»
    // ════════════════════════════════════════════════════
    'falling-bridge': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Над пропастью протянут мост из деревянных досок. Стоит
                    игроку наступить на доску — через секунду она рушится.
                    Значит, бежать нужно без остановки. Замешкался —
                    проваливаешься в пропасть.
                </p>
                <Shot src="lesson24-result.png"
                      caption="Беги по мосту — доски рушатся под ногами" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Таймер <code>game.after</code></b> — сделать
                        что-то через секунду, а не сразу;</li>
                    <li><b>Удаление объекта</b> — как убрать доску со сцены;</li>
                    <li><b>Флажок-защёлка</b> — чтобы доска запускалась
                        на падение только один раз.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Старт, пропасть и мост</h3>
                <Step n="1">
                    Поставь твёрдую стартовую площадку из камня.
                </Step>
                <Step n="2">
                    Дальше — пустота: пропасть, через которую нет пола.
                </Step>
                <Step n="3">
                    Над пропастью выложи мост из досок — кубов-примитивов
                    2.4×0.35×2 коричневого цвета, в один ряд. «Столкновение»
                    и «Закреплён» включены. Имена — «Доска_1», «Доска_2»
                    и так далее.
                </Step>
                <Step n="4">
                    На другом краю пропасти — твёрдая площадка с зелёным
                    неоновым финишем, имя «Финиш».
                </Step>
                <Shot src="lesson24-scene.png"
                      caption="Мост из досок над пропастью" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <CodeBoth game="falling-bridge" script="g24_main">{`// === ИГРА «ПАДАЮЩИЙ МОСТ» — главный скрипт ===

let won = false;

game.ui.showText('Беги по мосту — доски рушатся!', 3);

game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -3) {
    game.player.respawn();
    game.ui.showText('Упал в пропасть! Снова.', 1.5);
    game.sound.play('lose');
  }
});

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Ты перебежал мост!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>
                    Главный скрипт <code>game.onTick</code> каждый кадр
                    следит за высотой игрока: упал ниже -3 — провалился
                    в пропасть, возвращаем на старт. А
                    <code> game.onMessage('win', ...)</code> ждёт сообщение
                    от финиша — финиш работает в своей «песочнице», поэтому
                    о победе он сообщает через <code>game.broadcast</code>.
                </p>

                <h3 className="lessonH">Шаг 3. Скрипт доски</h3>
                <p>Этот скрипт вешается на <b>каждую</b> доску моста.</p>
                <ScriptKind kind="object" on="каждую доску" />
                <CodeBoth game="falling-bridge" script="g24_plank_1">{`// === Скрипт доски моста ===
let cracking = false;
game.self.onTouch(() => {
  if (cracking) return;
  cracking = true;
  game.sound.play('click');
  game.after(1, () => { game.self.delete(); });
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>cracking</code> — флажок-защёлка. Игрок может
                        наступить на доску несколько раз, но запустить таймер
                        падения нужно только один раз;</li>
                    <li><code>game.after(1, fn)</code> — «через 1 секунду
                        выполни это». Внутри —
                        <code> game.self.delete()</code>, доска исчезает;</li>
                    <li>звук <code>'click'</code> при касании — сигнал
                        игроку: «доска затрещала, беги!».</li>
                </ul>
                <Note>
                    Скрипт одинаковый для всех досок. Создай его на первой
                    доске, а когда дублируешь доску
                    (<kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>) —
                    скрипт скопируется вместе с ней.
                </Note>

                <h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="falling-bridge" script="g24_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <ul>
                    <li>встал на доску — щелчок, через секунду она рушится;</li>
                    <li>стоишь на месте — проваливаешься, респаун на старте;</li>
                    <li>перебежал мост, встал на финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай доски «быстрее» — пусть рушатся не через 1, а через
                    0.6 секунды. Сделай мост подлиннее или с поворотом. Поставь
                    на нём монетки из урока 1 — собирай на бегу.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 25 — «Камера-облёт»
    // ════════════════════════════════════════════════════
    'flyby-camera': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Когда игра начинается, камера сама красиво облетает
                    уровень по нескольким точкам — показывает игроку, что
                    его ждёт. Как только облёт закончился, управление
                    возвращается, и игрок идёт к финишу.
                </p>
                <Shot src="lesson25-result.png"
                      caption="Камера облетает уровень в начале игры" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Кат-сцена камеры</b> — как сделать кино-облёт
                        уровня (<code>game.camera.cutscene</code>);</li>
                    <li><b>Событие конца облёта</b> —
                        <code> game.onCutsceneDone</code>;</li>
                    <li><b>Массив точек</b> — как задать путь, по которому
                        полетит камера.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Уровень с декорациями</h3>
                <Step n="1">
                    Выложи широкое травяное поле — дорогу от старта
                    к финишу.
                </Step>
                <Step n="2">
                    Вдоль дороги расставь несколько фиолетовых неоновых
                    столбов-цилиндров — чтобы во время облёта было на что
                    посмотреть.
                </Step>
                <Step n="3">
                    В конце дороги — зелёная неоновая площадка-финиш,
                    имя «Финиш». Точку спавна поставь в начале.
                </Step>
                <Shot src="lesson25-scene.png"
                      caption="Дорога с фиолетовыми столбами и финишем" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт запускает облёт камеры и ждёт, когда он
                    закончится.
                </p>
                <ScriptKind kind="global" />
                <CodeBoth game="flyby-camera" script="g25_main">{`// === ИГРА «КАМЕРА-ОБЛЁТ» — главный скрипт ===

let won = false;

// при старте — облёт уровня камерой по точкам
game.camera.cutscene([
  { x: 0,  y: 18, z: -10 },
  { x: 12, y: 12, z: 8 },
  { x: -12, y: 12, z: 18 },
  { x: 0,  y: 10, z: 28 },
], { segDuration: 1.8 });

// когда облёт закончился — отдаём камеру игроку
game.onCutsceneDone(() => {
  game.ui.showText('Вперёд, к зелёному финишу!', 3);
});

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Уровень пройден!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</CodeBoth>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.camera.cutscene([точки], опции)</code> —
                        запускает кино-облёт. Камера летит по списку точек
                        одна за другой;</li>
                    <li>каждая точка — это <code>{'{ x, y, z }'}</code>,
                        место, откуда камера смотрит на уровень;</li>
                    <li><code>segDuration: 1.8</code> — сколько секунд камера
                        летит от одной точки до следующей;</li>
                    <li><code>game.onCutsceneDone(fn)</code> — функция внутри
                        выполнится, когда облёт закончится. Тут мы показываем
                        подсказку «Вперёд, к финишу!».</li>
                </ul>
                <Note>
                    Облёт нужен, чтобы игрок сразу увидел весь уровень —
                    где он, куда бежать, что его ждёт. Так делают во многих
                    больших играх перед началом миссии.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт финиша и проверка</h3>
                <p>
                    Финиш ловит касание игрока и шлёт сообщение
                    <code> 'win'</code>. Главный скрипт ловит его через
                    <code> game.onMessage('win', ...)</code> — так скрипты
                    из разных «песочниц» общаются между собой.
                </p>
                <ScriptKind kind="object" on="зелёный финиш" />
                <CodeBoth game="flyby-camera" script="g25_finish">{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</CodeBoth>
                <ul>
                    <li>с началом игры камера облетает уровень;</li>
                    <li>облёт закончился — управление вернулось, появилась
                        подсказка;</li>
                    <li>дойди до зелёного финиша — «Победа».</li>
                </ul>

                <Try>
                    добавь камере больше точек — облёт станет длиннее и
                    интереснее. Замедли облёт, увеличив <code>segDuration</code>.
                    Поставь по пути монетки или препятствия, которые камера
                    покажет во время облёта.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 26 — «Магнит монет»
    // ════════════════════════════════════════════════════
    'coin-magnet': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    По полю разбросаны золотые монетки. Стоит игроку
                    подойти к монетке поближе — она сама подлетает к нему,
                    будто притянутая магнитом. Собери все 8 монеток.
                </p>
                <Shot src="lesson26-result.png"
                      caption="Монетки сами летят к игроку — как магнит" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Проверка расстояния каждый кадр</b> — как монетка
                        «видит», что игрок рядом (<code>game.onTick</code> +
                        <code> Math.hypot</code>);</li>
                    <li><b>Твин к позиции игрока</b> — как заставить объект
                        лететь к игроку (<code>game.tween</code>);</li>
                    <li><b>Флажки состояния</b> — чтобы монетка летела один
                        раз и засчиталась один раз.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле и монетки</h3>
                <Step n="1">
                    Выложи большое травяное поле.
                </Step>
                <Step n="2">
                    Поставь 8 монеток — жёлтые сферы-примитивы 0.6×0.6×0.6,
                    материал «Неон». «Столкновение» <b>выключи</b> — касание
                    поймает скрипт. Имена — «Монетка_1» … «Монетка_8».
                </Step>
                <Step n="3">
                    Расставь монетки по углам и краям поля — чтобы за каждой
                    нужно было подойти.
                </Step>
                <Shot src="lesson26-scene.png"
                      caption="Восемь монеток разбросаны по полю" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «МАГНИТ МОНЕТ» — главный скрипт ===

let score = 0;
const TOTAL = 8;
game.ui.score = 0;
game.ui.showText('Подходи к монеткам — они притянутся!', 3);

// монетки сообщают о сборе через broadcast('coin') — скрипты
// в разных песочницах, счётчик score живёт только здесь.
game.onMessage('coin', () => {
  score = score + 1;
  game.ui.score = score;
  game.sound.play('coin');
  if (score >= TOTAL) {
    game.ui.showText('Победа! Все монетки собраны!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</Code>
                <p>
                    Главный скрипт простой — он только считает собранные
                    монетки и проверяет победу. Каждая монетка работает
                    в своей «песочнице», поэтому о сборе она сообщает через
                    <code> game.broadcast('coin')</code>, а главный скрипт
                    ловит сообщение через <code>game.onMessage('coin', ...)</code>.
                    Вся «магнитная магия» — в скрипте самой монетки.
                </p>

                <h3 className="lessonH">Шаг 3. Скрипт магнитной монетки</h3>
                <p>Этот скрипт вешается на <b>каждую</b> монетку.</p>
                <ScriptKind kind="object" on="каждую монетку" />
                <Code>{`// === Скрипт магнитной монетки ===

let flying = false;   // монетка уже летит к игроку?
let taken = false;

game.onTick(() => {
  if (taken) return;
  const c = game.self.position;
  const p = game.player.position;
  // позиции могут быть не готовы первые кадры — ждём
  if (!c || !p) return;
  const dist = Math.hypot(p.x - c.x, p.z - c.z);

  // игрок коснулся монетки — собрана
  if (dist < 1.2) {
    taken = true;
    game.self.delete();
    game.broadcast('coin');   // сообщить главному скрипту
    return;
  }
  // подошёл близко — монетка летит к игроку
  if (!flying && dist < 6) {
    flying = true;
    game.tween(game.self.ref,
      { x: p.x, y: p.y + 1, z: p.z },
      { duration: 0.5, easing: 'ease' });
  }
});`}</Code>
                <p>Разберём по частям:</p>
                <ul>
                    <li>первые кадры <code>game.self.position</code> и
                        <code> game.player.position</code> могут быть ещё
                        не готовы — проверка <code>if (!c || !p) return</code>
                        просто пропускает такой кадр;</li>
                    <li>каждый кадр считаем <code>dist</code> — расстояние
                        от монетки до игрока через <code>Math.hypot</code>;</li>
                    <li>если <code>dist {'<'} 1.2</code> — игрок дотянулся
                        до монетки. Ставим <code>taken</code>, удаляем
                        монетку и засчитываем её;</li>
                    <li>если <code>dist {'<'} 6</code> и монетка ещё
                        не летит — включаем <code>flying</code> и запускаем
                        <code> game.tween</code> к позиции игрока. Монетка
                        плавно подлетает за 0.5 секунды;</li>
                    <li>флажки <code>flying</code> и <code>taken</code>
                        нужны, чтобы твин запустился один раз и монетка
                        засчиталась один раз.</li>
                </ul>
                <Note>
                    Твин летит к той точке, где игрок был в момент запуска.
                    Если игрок отбежит — монетка прилетит «в пустоту»,
                    но <code>dist {'<'} 6</code> запустит её снова.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>подходишь к монетке ближе 6 метров — она летит к тебе;</li>
                    <li>монетка коснулась — звон, +1 очко;</li>
                    <li>собрал все 8 — «Победа».</li>
                </ul>

                <Try>
                    увеличь «радиус магнита» — поменяй <code>6</code>
                    на большее число, монетки будут лететь издалека. Сделай
                    редкую красную монетку, которая <b>убегает</b> от игрока,
                    а не притягивается.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 27 — «Двойной прыжок»
    // ════════════════════════════════════════════════════
    'double-jump': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Паркур, где платформы стоят так далеко друг от друга,
                    что обычным прыжком до них не добраться. Скрипт включает
                    игроку <b>двойной прыжок</b> — можно прыгнуть ещё раз
                    прямо в воздухе и допрыгнуть до следующей платформы.
                </p>
                <Shot src="lesson27-result.png"
                      caption="Платформы далеко — нужен двойной прыжок" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Двойной прыжок</b> — особая способность игрока
                        (<code>game.player.setDoubleJump</code>);</li>
                    <li><b>Дизайн паркура</b> — как ставить платформы под
                        двойной прыжок;</li>
                    <li><b>Падение и респаун</b> — возврат на старт при
                        падении.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Старт и платформы</h3>
                <Step n="1">
                    Поставь небольшую стартовую площадку из травы.
                </Step>
                <Step n="2">
                    Расставь 5 платформ — кубов-примитивов 2.5×0.5×2.5
                    коричневого цвета. Делай между ними <b>большие
                    разрывы</b> — такие, чтобы обычным прыжком не долететь.
                    «Столкновение» и «Закреплён» включены.
                </Step>
                <Step n="3">
                    Подними каждую следующую платформу чуть выше — дорожка
                    идёт вверх.
                </Step>
                <Step n="4">
                    В конце — зелёная неоновая площадка-финиш, имя «Финиш».
                </Step>
                <Shot src="lesson27-scene.png"
                      caption="Платформы с большими разрывами" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ДВОЙНОЙ ПРЫЖОК» — главный скрипт ===

let won = false;

// включаем игроку двойной прыжок — теперь можно прыгнуть
// ещё раз прямо в воздухе
game.player.setDoubleJump(true);
game.ui.showText('Жми Space ДВАЖДЫ — двойной прыжок!', 4);

game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -3) {
    game.player.respawn();
    game.sound.play('lose');
  }
});

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Двойной прыжок освоен!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>Главное здесь:</p>
                <ul>
                    <li><code>game.player.setDoubleJump(true)</code> — даёт
                        игроку способность прыгать ещё раз в воздухе.
                        Достаточно одной этой строчки;</li>
                    <li>теперь, нажав <kbd className="kbd">Space</kbd> второй
                        раз во время полёта, игрок подпрыгивает ещё выше;</li>
                    <li><code>game.onTick</code> следит за падением — упал
                        ниже -3, возвращаем на старт.</li>
                </ul>
                <Note>
                    Без двойного прыжка этот паркур пройти нельзя — платформы
                    специально стоят слишком далеко. Способность меняет
                    правила игры.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт финиша и проверка</h3>
                <p>
                    Финиш ловит касание игрока и шлёт сообщение
                    <code> 'win'</code>. Главный скрипт ловит его через
                    <code> game.onMessage('win', ...)</code> — так два скрипта
                    из разных «песочниц» общаются.
                </p>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</Code>
                <ul>
                    <li>прыгай <kbd className="kbd">Space</kbd>, в воздухе
                        жми ещё раз — второй прыжок;</li>
                    <li>так допрыгивай с платформы на платформу;</li>
                    <li>упал вниз — респаун на старте;</li>
                    <li>встал на зелёный финиш — «Победа».</li>
                </ul>

                <Try>
                    раздвинь платформы ещё дальше — паркур станет сложнее.
                    Добавь движущуюся платформу через <code>game.tween</code>
                    с <code>yoyo: true</code>. Поставь монетки между
                    платформами — собирай их в прыжке.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 28 — «Призрачные стены»
    // ════════════════════════════════════════════════════
    'ghost-walls': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Коридор перегорожен четырьмя фиолетовыми стенами. Каждую
                    можно сделать <b>призрачной</b>: кликнул по стене — она
                    становится полупрозрачной и проходимой. Пройди сквозь
                    все стены к финишу.
                </p>
                <Shot src="lesson28-result.png"
                      caption="Кликни по стене — она станет призрачной" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Клик по объекту</b> — событие
                        <code> game.self.onClick</code>;</li>
                    <li><b>Проходимость</b> — как убрать у объекта
                        столкновение во время игры
                        (<code>game.physics.passThrough</code>);</li>
                    <li><b>Прозрачность</b> — <code>game.scene.setOpacity</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Коридор и стены</h3>
                <Step n="1">
                    Выложи длинный пол-коридор из камня.
                </Step>
                <Step n="2">
                    Поперёк коридора поставь 4 широкие фиолетовые стены —
                    кубы-примитивы, материал «Неон». «Столкновение»
                    включено — пока в них упираются. Имена — «Стена_1» …
                    «Стена_4».
                </Step>
                <Step n="3">
                    В конце коридора — зелёная неоновая площадка-финиш,
                    имя «Финиш».
                </Step>
                <Shot src="lesson28-scene.png"
                      caption="Коридор с четырьмя фиолетовыми стенами" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ПРИЗРАЧНЫЕ СТЕНЫ» — главный скрипт ===

game.ui.showText('Кликай по фиолетовым стенам — пройди сквозь!', 4);

// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
  game.ui.showText('Победа! Ты прошёл сквозь все стены!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>
                    Главный скрипт только обрабатывает победу — он ловит
                    сообщение <code>'win'</code> от финиша через
                    <code> game.onMessage</code>. Вся «магия» — в скриптах
                    стен.
                </p>

                <h3 className="lessonH">Шаг 3. Скрипт призрачной стены</h3>
                <p>Этот скрипт вешается на <b>каждую</b> стену.</p>
                <ScriptKind kind="object" on="каждую стену" />
                <Code>{`// === Скрипт призрачной стены ===

let ghost = false;
game.self.onClick(() => {
  if (ghost) return;
  ghost = true;
  // стена становится проходимой и полупрозрачной
  game.physics.passThrough(game.self.ref, true);
  game.scene.setOpacity(game.self.ref, 0.25);
  game.sound.play('click');
  game.ui.showText('Стена стала призрачной!', 1.5);
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.self.onClick(fn)</code> — функция внутри
                        срабатывает, когда игрок кликнул по этой стене мышкой;</li>
                    <li><code>game.physics.passThrough(ref, true)</code> —
                        убирает у стены столкновение: теперь сквозь неё можно
                        пройти;</li>
                    <li><code>game.scene.setOpacity(ref, 0.25)</code> —
                        делает стену почти прозрачной (0.25 — четверть
                        непрозрачности), чтобы было видно: она призрачная;</li>
                    <li><code>ghost</code> — флажок, чтобы клики после
                        первого ничего не делали.</li>
                </ul>
                <Note>
                    Сначала меняем физику (<code>passThrough</code>), потом
                    внешний вид (<code>setOpacity</code>) — игрок и видит,
                    и чувствует, что стена «расколдована».
                </Note>

                <h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</Code>
                <ul>
                    <li>кликни по фиолетовой стене — она побледнеет;</li>
                    <li>пройди сквозь призрачную стену;</li>
                    <li>так пройди все 4 стены;</li>
                    <li>встань на зелёный финиш — «Победа».</li>
                </ul>

                <Try>
                    сделай среди стен «обманки» — стены, которые при клике
                    не исчезают, а наоборот мигают красным. Добавь стенам
                    задержку: после клика стена становится призрачной только
                    через секунду (<code>game.after</code>).
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 29 — «Магазин»
    // ════════════════════════════════════════════════════
    'shop': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Игрок собирает монетки, разбросанные по комнате. Потом
                    подходит к прилавку и покупает ключ за 5 монет. С ключом
                    в инвентаре он открывает запертую дверь и доходит
                    до финиша.
                </p>
                <Shot src="lesson29-result.png"
                      caption="Собери монеты, купи ключ, открой дверь" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Валюта-экономика</b> — копить монеты и тратить их;</li>
                    <li><b>Инвентарь</b> — класть предмет игроку
                        (<code>game.inventory.add</code>) и проверять, есть ли
                        он (<code>game.inventory.has</code>);</li>
                    <li><b>Покупка</b> — проверить, хватает ли денег.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Комната, прилавок, дверь</h3>
                <Step n="1">
                    Сделай пол комнаты из досок дерева.
                </Step>
                <Step n="2">
                    Поставь 7 монеток — жёлтые сферы-примитивы, «Столкновение»
                    выключено. Имена — «Монетка_1» … «Монетка_7».
                </Step>
                <Step n="3">
                    Поставь прилавок — длинный куб, имя «Прилавок».
                </Step>
                <Step n="4">
                    Дальше — стена из двух кубов с проёмом, в проёме
                    куб-дверь (имя «Дверь»). За дверью — зелёный неоновый
                    финиш.
                </Step>
                <Shot src="lesson29-scene.png"
                      caption="Магазин: монетки, прилавок, дверь, финиш" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт — «мозг» магазина: считает монеты,
                    продаёт ключ, открывает дверь.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «МАГАЗИН» — главный скрипт ===

let coins = 0;
const PRICE = 5;          // ключ стоит 5 монет
let bought = false;

game.ui.score = 0;
game.ui.showText('Собери монетки и купи ключ у продавца!', 4);

// монетки/прилавок/дверь/финиш шлют сюда сообщения через broadcast —
// скрипты в разных песочницах, состояние coins/bought живёт только здесь.
game.onMessage('coin', () => {
  coins = coins + 1;
  game.ui.score = coins;
  game.sound.play('coin');
});

game.onMessage('buy', () => {
  if (bought) {
    game.ui.showText('Ключ уже куплен, иди к двери!', 2);
    return;
  }
  if (coins < PRICE) {
    game.ui.showText('Мало монет! Нужно ' + PRICE + ', есть ' + coins, 2);
    game.sound.play('lose');
    return;
  }
  bought = true;
  coins = coins - PRICE;
  game.ui.score = coins;
  game.inventory.add({ name: 'Ключ' });
  game.ui.showText('Куплен Ключ! Открой дверь.', 3);
  game.sound.play('win');
});

game.onMessage('open-door', () => {
  if (!game.inventory.has('Ключ')) {
    game.ui.showText('Дверь заперта. Купи ключ в магазине.', 2);
    return;
  }
  const door = game.scene.findOne('Дверь');
  game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
  game.ui.showText('Дверь открыта!', 2);
});

game.onMessage('win', () => {
  game.ui.showText('Победа! Ты прошёл магазин!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>
                    Каждый объект магазина — монетка, прилавок, дверь, финиш —
                    работает в своей «песочнице» и не видит переменные
                    главного скрипта. Поэтому объекты шлют сообщения через
                    <code> game.broadcast('имя')</code>, а главный скрипт ловит
                    их через <code>game.onMessage('имя', ...)</code>. Разберём
                    четыре обработчика:
                </p>
                <ul>
                    <li><code>onMessage('coin', ...)</code> — монетка собрана:
                        <code> coins</code> растёт, число на экране тоже;</li>
                    <li><code>onMessage('buy', ...)</code> — покупка ключа.
                        Сначала проверяем <code>coins {'<'} PRICE</code> —
                        хватает ли денег. Хватает — отнимаем 5 монет и
                        <code> game.inventory.add</code> кладёт ключ
                        в инвентарь;</li>
                    <li><code>onMessage('open-door', ...)</code> —
                        <code> game.inventory.has('Ключ')</code> проверяет,
                        есть ли ключ. Есть — дверь уезжает вверх;</li>
                    <li><code>onMessage('win', ...)</code> — победа на финише.</li>
                </ul>
                <Note>
                    Инвентарь — это «карман» игрока. Положил предмет через
                    <code> add</code>, проверил через <code>has</code>. Так
                    делают ключи, оружие, зелья — что угодно.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт монетки</h3>
                <ScriptKind kind="object" on="каждую монетку" />
                <Code>{`// === Скрипт монетки ===
game.self.onTouch(() => {
  game.broadcast('coin');
  game.self.delete();
});`}</Code>

                <h3 className="lessonH">Шаг 4. Скрипт прилавка и двери</h3>
                <ScriptKind kind="object" on="прилавок" />
                <Code>{`// === Скрипт прилавка ===
game.self.onInteract(() => {
  game.broadcast('buy');
}, { text: 'Купить ключ (5 монет)', distance: 4 });`}</Code>
                <ScriptKind kind="object" on="дверь" />
                <Code>{`// === Скрипт двери ===
game.self.onInteract(() => {
  game.broadcast('open-door');
}, { text: 'Открыть дверь', distance: 4 });`}</Code>
                <p>
                    И прилавок, и дверь — это объекты с взаимодействием по
                    <kbd className="kbd">E</kbd>. Подошёл к прилавку и нажал
                    E — объект шлёт сообщение <code>'buy'</code>, подошёл
                    к двери и нажал E — летит сообщение <code>'open-door'</code>.
                    Главный скрипт ловит эти сообщения и решает, что делать.
                </p>

                <h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('win');
});`}</Code>
                <ul>
                    <li>собери монетки — счётчик растёт;</li>
                    <li>у прилавка нажми <kbd className="kbd">E</kbd> — если
                        монет 5 и больше, купишь ключ;</li>
                    <li>у двери нажми <kbd className="kbd">E</kbd> — с ключом
                        она откроется;</li>
                    <li>дойди до финиша — «Победа».</li>
                </ul>

                <Try>
                    добавь второй товар — например, «Зелье скорости» за 3
                    монеты. Сделай монеток побольше или подними цену ключа.
                    Поставь два прилавка с разными ценами.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 30 — «Квест с заданиями»
    // ════════════════════════════════════════════════════
    'quest-tasks': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На поле стоит NPC-старейшина. Он даёт цепочку из трёх
                    заданий: собери монетку, дойди до синего флага, вернись
                    к нему. Каждое задание открывает следующее. Выполнил всё —
                    победа.
                </p>
                <Shot src="lesson30-result.png"
                      caption="Старейшина даёт цепочку заданий" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>NPC-диалоги</b> — как NPC «говорит»
                        (<code>npc.say</code>);</li>
                    <li><b>Этапы квеста</b> — как хранить, на каком шаге
                        квеста сейчас игрок (переменная-состояние);</li>
                    <li><b>Цепочка заданий</b> — как одно задание открывает
                        следующее.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле, тумба, монетка, флаг</h3>
                <Step n="1">
                    Выложи большое травяное поле.
                </Step>
                <Step n="2">
                    Поставь тумбу-квестодателя — куб, имя «Квестодатель».
                    Рядом с ней скрипт создаст NPC.
                </Step>
                <Step n="3">
                    В одном углу поля поставь жёлтую сферу-монетку
                    (имя «КвестМонетка»), в другом — синий конус-флаг
                    (имя «КвестФлаг»). У обоих «Столкновение» выключено.
                </Step>
                <Shot src="lesson30-scene.png"
                      caption="Поле с квестодателем, монеткой и флагом" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт хранит этап квеста и ведёт игрока
                    по заданиям.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===

// этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага,
// 3=вернуться к NPC, 4=готово
let stage = 0;

game.ui.showText('Подойди к квестодателю (E)', 3);

// создаём NPC рядом с тумбой
const npc = game.scene.spawnNpc('character-a', {
  x: 1.5, y: 1, z: 2, name: 'Старейшина', hp: 100, speed: 0,   // y=1 — на полу
});

// квестодатель/монетка/флаг шлют сюда сообщения через broadcast —
// скрипты в разных песочницах, этап квеста stage живёт только здесь.
game.onMessage('talk', () => {
  if (stage === 0) {
    stage = 1;
    npc.say('Задание 1: найди жёлтую монетку!', 4);
    game.ui.showText('Квест: собери монетку', 3);
  } else if (stage === 3) {
    stage = 4;
    npc.say('Молодец! Квест выполнен!', 4);
    game.ui.showText('Победа! Квест пройден!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  } else if (stage === 4) {
    npc.say('Спасибо, герой!', 3);
  } else {
    npc.say('Сначала выполни задание!', 3);
  }
});

game.onMessage('coin-done', () => {
  if (stage !== 1) return;
  stage = 2;
  game.sound.play('coin');
  npc.say('Отлично! Теперь дойди до синего флага.', 4);
  game.ui.showText('Квест: дойди до флага', 3);
});

game.onMessage('flag-done', () => {
  if (stage !== 2) return;
  stage = 3;
  game.sound.play('pickup');
  game.ui.showText('Квест: вернись к квестодателю', 3);
});`}</Code>
                <p>Главное здесь — переменная <code>stage</code>:</p>
                <ul>
                    <li><code>stage</code> хранит, на каком шаге квеста
                        игрок: 0 — не начат, 1 — собирает монетку, 2 — идёт
                        к флагу, 3 — возвращается, 4 — готово;</li>
                    <li>квестодатель, монетка и флаг — отдельные скрипты,
                        каждый в своей «песочнице». Они не видят переменную
                        <code> stage</code>, поэтому шлют сообщения через
                        <code> game.broadcast('имя')</code>, а главный скрипт
                        ловит их через <code>game.onMessage('имя', ...)</code>;</li>
                    <li><code>onMessage('talk', ...)</code> — разговор с NPC.
                        Что он скажет, зависит от <code>stage</code>: на старте
                        даёт первое задание, в конце поздравляет;</li>
                    <li><code>onMessage('coin-done', ...)</code> и
                        <code> onMessage('flag-done', ...)</code> — ловят
                        сообщения от монетки и флага. Они проверяют
                        <code> stage</code> и переводят квест на следующий
                        этап;</li>
                    <li><code>npc.say('текст', 4)</code> — NPC показывает
                        над собой облачко с репликой на 4 секунды.</li>
                </ul>
                <Note>
                    Проверки вроде <code>if (stage !== 1) return</code> важны:
                    они не дают засчитать задание раньше времени. Монетку
                    нельзя «сдать», пока NPC не дал задание её собрать.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт квестодателя</h3>
                <ScriptKind kind="object" on="тумбу квестодателя" />
                <Code>{`// === Скрипт квестодателя ===
game.self.onInteract(() => {
  game.broadcast('talk');
}, { text: 'Поговорить', distance: 4 });`}</Code>

                <h3 className="lessonH">Шаг 4. Скрипты монетки и флага</h3>
                <ScriptKind kind="object" on="квест-монетку" />
                <Code>{`// === Скрипт квест-монетки ===
game.self.onTouch(() => {
  game.broadcast('coin-done');
  game.self.delete();
});`}</Code>
                <ScriptKind kind="object" on="квест-флаг" />
                <Code>{`// === Скрипт квест-флага ===
game.self.onTouch(() => {
  game.broadcast('flag-done');
});`}</Code>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <ul>
                    <li>подойди к квестодателю, нажми
                        <kbd className="kbd">E</kbd> — он даст первое
                        задание;</li>
                    <li>собери монетку — NPC даст второе задание;</li>
                    <li>дойди до флага — NPC попросит вернуться;</li>
                    <li>вернись к квестодателю и поговори — «Победа».</li>
                </ul>

                <Try>
                    добавь четвёртый этап квеста — например, «принеси два
                    предмета». Сделай NPC награду: после квеста дай игроку
                    предмет через <code>game.inventory.add</code>. Поставь
                    второго NPC со своим квестом.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 31 — «Защита базы»
    // ════════════════════════════════════════════════════
    'base-defense': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Из дальнего конца поля к твоей базе идут волны
                    NPC-врагов. Кликай по ним, чтобы уничтожать. Уничтожь 12
                    врагов — победа. Пропустишь 5 врагов до базы — поражение.
                </p>
                <Shot src="lesson31-result.png"
                      caption="Враги идут к базе — кликай по ним" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Волны врагов</b> — спавнить NPC снова и снова
                        (<code>game.every</code> + <code>spawnNpc</code>);</li>
                    <li><b>Движение NPC к точке</b> — <code>enemy.moveTo</code>;</li>
                    <li><b>Клик по врагу</b> — уничтожение
                        (<code>enemy.remove</code>);</li>
                    <li><b>Два счётчика</b> — победа и поражение
                        одновременно.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле и база</h3>
                <Step n="1">
                    Выложи длинное поле из камня — дорогу, по которой пойдут
                    враги.
                </Step>
                <Step n="2">
                    У ближнего края поставь большой синий неоновый куб —
                    базу, имя «База». Точку спавна поставь рядом с базой.
                </Step>
                <p>
                    Враги создаются скриптом — заранее их ставить не нужно.
                </p>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>Это большой скрипт — разберём его по частям.</p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ЗАЩИТА БАЗЫ» — главный скрипт ===

let killed = 0;           // сколько врагов уничтожено
let leaked = 0;           // сколько врагов дошло до базы
const GOAL = 12;          // победа — уничтожить 12 врагов
const MAX_LEAK = 5;       // проигрыш — 5 врагов прорвались
let over = false;

game.ui.score = 0;
game.ui.showText('Защити базу! Кликай по врагам', 3);

// каждые 2 секунды появляется новый враг далеко от базы
let total = 0;
game.every(2, () => {
  if (over || total >= GOAL + MAX_LEAK) return;
  total = total + 1;
  const x = game.random(-8, 8);
  const enemy = game.scene.spawnNpc('character-b', {
    x: x, y: 1, z: 38, name: 'Враг', hp: 30, speed: 2.5,   // y=1 — на полу
  });
  // враг идёт к базе (точка 0,0,0)
  enemy.moveTo(0, 2);

  let dead = false;
  // клик по врагу — уничтожить
  game.onClick(() => {
    if (dead || over) return;
    const p = game.player.position;
    const e = enemy.position;
    if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 5) {
      dead = true;
      enemy.remove();
      game.scene.spawnParticles('explosion', e, { count: 1 });
      game.sound.play('hit');
      killed = killed + 1;
      game.ui.score = killed;
      if (killed >= GOAL && !over) {
        over = true;
        game.ui.showText('Победа! База защищена!', 5);
        game.sound.play('win');
      }
    }
  });

  // если враг дошёл до базы — пропуск
  const watch = game.every(0.4, () => {
    if (dead || over) { game.cancel(watch); return; }
    // enemy.position готов не сразу после spawnNpc — ждём
    const ep = enemy.position;
    if (ep && ep.z < 4) {
      dead = true;
      game.cancel(watch);
      enemy.remove();
      leaked = leaked + 1;
      game.sound.play('lose');
      game.ui.showText('Враг прорвался! (' + leaked + '/' + MAX_LEAK + ')', 2);
      if (leaked >= MAX_LEAK && !over) {
        over = true;
        game.ui.showText('База разрушена! Поражение.', 5);
      }
    }
  });
});`}</Code>
                <p>Как появляются волны врагов:</p>
                <ul>
                    <li><code>game.every(2, ...)</code> — каждые 2 секунды
                        создаётся новый враг в случайной точке у дальнего
                        края (<code>z: 38</code>);</li>
                    <li><code>enemy.moveTo(0, 2)</code> — командует врагу
                        идти к точке базы (координата 0) со скоростью 2.</li>
                </ul>
                <p>Что происходит с каждым врагом:</p>
                <ul>
                    <li><code>game.onClick(...)</code> — клик мышкой. Если
                        игрок близко к врагу (меньше 5 метров) —
                        <code> enemy.remove()</code> убирает врага, летит
                        взрыв, <code>killed</code> растёт;</li>
                    <li><code>game.every(0.4, ...)</code> в переменной
                        <code> watch</code> — следит за врагом. Если он дошёл
                        до базы (<code>z {'<'} 4</code>) — счётчик
                        <code> leaked</code> растёт;</li>
                    <li><code>game.cancel(watch)</code> — останавливает этот
                        таймер-следилку, когда враг уже мёртв или ушёл;</li>
                    <li>две проверки: <code>killed {'>'}= GOAL</code> —
                        победа, <code>leaked {'>'}= MAX_LEAK</code> —
                        поражение.</li>
                </ul>
                <Note>
                    У каждого врага свой <code>game.onClick</code> и свой
                    таймер <code>watch</code>. Флажок <code>dead</code> —
                    личный для каждого врага: он не даёт убить или засчитать
                    одного врага дважды.
                </Note>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>каждые 2 секунды появляется новый враг;</li>
                    <li>подбеги и кликни по врагу — взрыв, +1 к счёту;</li>
                    <li>враг дошёл до базы — счётчик прорывов растёт;</li>
                    <li>12 уничтожено — «Победа», 5 прорвались — «Поражение».</li>
                </ul>

                <Try>
                    ускорь волны — спавни врагов чаще (поменяй 2 на 1).
                    Сделай врагов быстрее или живучее. Добавь «жирного»
                    врага раз в 5 волн — большого и медленного.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 32 — «Гонка с кругами»
    // ════════════════════════════════════════════════════
    'lap-race': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Кольцевая трасса с четырьмя чекпоинтами по углам. Нужно
                    проехать 2 круга, пересекая чекпоинты строго по порядку.
                    Сверху идёт секундомер — старайся уложиться побыстрее.
                </p>
                <Shot src="lesson32-result.png"
                      caption="Кольцевая трасса с чекпоинтами — 2 круга" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Чекпоинты по порядку</b> — как проверить, что
                        игрок проезжает точки в нужной последовательности;</li>
                    <li><b>Счётчик кругов</b> — как считать пройденные круги;</li>
                    <li><b>Секундомер</b> — <code>game.ui.timer</code> +
                        <code> dt</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Кольцевая трасса</h3>
                <Step n="1">
                    Выложи квадратное кольцо из камня — большой квадрат
                    с дыркой в центре. По нему игрок будет наматывать круги.
                </Step>
                <Step n="2">
                    По четырём сторонам кольца поставь 4 чекпоинта — высокие
                    тонкие кубы-примитивы, материал «Неон». Первый сделай
                    зелёным (старт/финиш), остальные жёлтыми. «Столкновение»
                    выключено. Имена — «Чекпоинт_1» … «Чекпоинт_4».
                </Step>
                <Step n="3">
                    Точку спавна поставь у первого (зелёного) чекпоинта.
                </Step>
                <Shot src="lesson32-scene.png"
                      caption="Кольцевая трасса с четырьмя чекпоинтами" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ГОНКА С КРУГАМИ» — главный скрипт ===

const LAPS = 2;           // сколько кругов проехать
const CP_COUNT = 4;       // чекпоинтов на круге
let nextCp = 0;           // какой чекпоинт ждём (0..3)
let lap = 0;              // текущий круг
let time = 0;
let won = false;

game.ui.timer = 0;
game.ui.showText('Проедь 2 круга через чекпоинты!', 3);

game.onTick((dt) => {
  if (won) return;
  time = time + dt;
  game.ui.timer = time;
});

// чекпоинты шлют сюда broadcast('checkpoint', { num }) — скрипты
// в разных песочницах, прогресс гонки nextCp/lap живёт только здесь.
game.onMessage('checkpoint', (d) => {
  // игрок пересёк чекпоинт с номером n (1..4)
  const n = d.num;
  if (won) return;
  // ждём именно следующий по порядку чекпоинт
  if (n - 1 !== nextCp) return;
  game.sound.play('click');
  nextCp = nextCp + 1;
  if (nextCp >= CP_COUNT) {
    nextCp = 0;
    lap = lap + 1;
    if (lap >= LAPS) {
      won = true;
      const t = Math.round(time * 10) / 10;
      game.ui.showText('Финиш! Круги пройдены за ' + t + ' сек', 6);
      game.sound.play('win');
      const p = game.player.position;
      game.scene.spawnParticles('confetti',
        { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
    } else {
      game.ui.showText('Круг ' + lap + ' из ' + LAPS + '!', 2);
    }
  }
});`}</Code>
                <p>Главное здесь — порядок чекпоинтов:</p>
                <ul>
                    <li><code>game.onMessage('checkpoint', (d) =&gt; {'{...}'})</code> —
                        ловит сообщение от чекпоинта. Каждый чекпоинт — отдельный
                        скрипт в своей «песочнице», поэтому номер приходит
                        в «посылке» <code>d.num</code>, а не через общую
                        переменную;</li>
                    <li><code>nextCp</code> — номер чекпоинта, который мы
                        ждём следующим (начинаем с 0);</li>
                    <li><code>if (n - 1 !== nextCp) return</code> — если
                        игрок пересёк <b>не тот</b> чекпоинт (например,
                        проехал второй раньше первого) — пропускаем, он
                        не засчитан;</li>
                    <li>пересёк правильный — <code>nextCp</code> сдвигается
                        дальше. Прошёл все 4 — <code>lap</code> (круг)
                        растёт, а <code>nextCp</code> сбрасывается в 0;</li>
                    <li><code>if (lap {'>'}= LAPS)</code> — проехал 2 круга,
                        секундомер останавливается, показывается время.</li>
                </ul>
                <Note>
                    Проверка порядка не даёт «срезать» — нельзя проехать
                    половину трассы и засчитать круг. Только все чекпоинты
                    по очереди дадут круг.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипт чекпоинта</h3>
                <p>
                    На каждый чекпоинт — свой скрипт, отличается только
                    номер. Вот скрипт первого чекпоинта:
                </p>
                <ScriptKind kind="object" on="Чекпоинт_1" />
                <Code>{`// === Скрипт чекпоинта 1 ===
game.self.onTouch(() => {
  game.broadcast('checkpoint', { num: 1 });
});`}</Code>
                <p>
                    Чекпоинт шлёт сообщение
                    <code> game.broadcast('checkpoint', {'{ num: 1 }'})</code>:
                    имя сообщения — <code>'checkpoint'</code>, а «посылка»
                    с номером говорит главному скрипту, какую точку пересёк
                    игрок. На «Чекпоинт_2» — такой же скрипт с
                    <code> num: 2</code>, на «Чекпоинт_3» — с <code>num: 3</code>,
                    на «Чекпоинт_4» — с <code>num: 4</code>.
                </p>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>с началом игры пошёл секундомер;</li>
                    <li>проезжай чекпоинты по кругу по порядку;</li>
                    <li>после 4 чекпоинтов засчитывается круг;</li>
                    <li>проехал 2 круга — «Финиш» и твоё время.</li>
                </ul>

                <Try>
                    увеличь число кругов — поменяй <code>LAPS</code> на 3.
                    Поставь на трассе препятствия. Покрась пройденный
                    чекпоинт в зелёный через <code>game.scene.setColor</code>,
                    чтобы было видно, где ты уже был.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 33 — «Платформер с боссом»
    // ════════════════════════════════════════════════════
    'boss-platformer': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Сначала паркур — допрыгай по платформам до высокой
                    арены. Там тебя ждёт NPC-босс с большим запасом
                    здоровья. Кликай по нему, пока полоска HP не обнулится.
                    Над боссом висит метка с его здоровьем.
                </p>
                <Shot src="lesson33-result.png"
                      caption="Паркур до арены, потом бой с боссом" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Паркур + бой</b> — как соединить две механики
                        в одной игре;</li>
                    <li><b>Спавн босса один раз</b> — создать NPC, когда
                        игрок дошёл до арены;</li>
                    <li><b>Метка над NPC</b> — показать HP босса
                        (<code>game.scene.setLabel</code>);</li>
                    <li><b>Событие смерти NPC</b> — <code>boss.onDeath</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Старт, паркур, арена</h3>
                <Step n="1">
                    Поставь стартовую площадку из травы внизу.
                </Step>
                <Step n="2">
                    Расставь 4 платформы — кубы-примитивы, ступеньками вверх
                    к арене. «Столкновение» и «Закреплён» включены.
                </Step>
                <Step n="3">
                    Наверху выложи большую арену из камня — площадку, где
                    будет бой с боссом.
                </Step>
                <Shot src="lesson33-scene.png"
                      caption="Платформы паркура ведут к арене наверху" />
                <p>
                    Босса заранее ставить не нужно — его создаёт скрипт,
                    когда игрок добирается до арены.
                </p>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ПЛАТФОРМЕР С БОССОМ» — главный скрипт ===

let won = false;
let bossSpawned = false;
let bossHp = 120;

game.ui.showText('Пройди паркур до арены босса!', 3);

// упал — на старт
game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (!p) return;   // позиция игрока может быть не готова первые кадры
  if (p.y < -3) {
    game.player.respawn();
    game.sound.play('lose');
  }
  // дошёл до арены — спавним босса один раз
  if (!bossSpawned && p.z > 24 && p.y > 5) {
    bossSpawned = true;
    const boss = game.scene.spawnNpc('character-b', {
      x: 0, y: 7, z: 32, name: 'БОСС', hp: bossHp, speed: 2,   // y=7 — на полу арены (блоки y=6)
    });
    boss.follow('player');
    game.scene.setLabel(boss.ref, 'БОСС  HP: ' + bossHp, {
      color: '#ff3333', height: 3.5
    });
    game.ui.showText('БОСС! Кликай по нему!', 3);

    boss.onDeath(() => {
      won = true;
      game.scene.clearLabel(boss.ref);
      game.ui.showText('Победа! Босс повержен!', 5);
      game.sound.play('win');
      const p2 = game.player.position;
      game.scene.spawnParticles('confetti',
        { x: p2.x, y: p2.y + 3, z: p2.z }, { duration: 3, count: 3 });
    });

    // удар по боссу кликом, если игрок близко
    game.onClick(() => {
      if (won) return;
      const pp = game.player.position;
      const bp = boss.position;
      if (pp && bp && Math.hypot(pp.x - bp.x, pp.z - bp.z) < 5) {
        bossHp = bossHp - 20;
        if (bossHp < 0) bossHp = 0;
        boss.damage(20);
        game.scene.setLabel(boss.ref, 'БОСС  HP: ' + bossHp, {
          color: '#ff3333', height: 3.5
        });
        game.scene.spawnParticles('sparks', bp, { duration: 0.4 });
        game.sound.play('hit');
      }
    });
  }
});`}</Code>
                <p>Разберём по частям. Сначала — паркур:</p>
                <ul>
                    <li><code>game.onTick</code> следит за падением: упал
                        ниже -3 — респаун;</li>
                    <li><code>if (!bossSpawned && p.z {'>'} 24 && p.y {'>'} 5)</code> —
                        как только игрок зашёл на арену (далеко по
                        <code> z</code> и высоко по <code>y</code>) —
                        создаём босса. Флажок <code>bossSpawned</code>
                        не даёт создать его второй раз.</li>
                </ul>
                <p>Потом — бой:</p>
                <ul>
                    <li><code>boss.follow('player')</code> — босс гонится
                        за игроком;</li>
                    <li><code>game.scene.setLabel(boss.ref, текст, опции)</code> —
                        вешает над боссом метку-табличку с его HP. Это
                        billboard — она всегда повёрнута к камере;</li>
                    <li><code>game.onClick</code> — клик по боссу, если игрок
                        близко: <code>bossHp</code> уменьшается на 20,
                        <code> boss.damage(20)</code> ранит NPC, метка
                        обновляется;</li>
                    <li><code>boss.onDeath(fn)</code> — когда у босса HP
                        дошло до нуля, эта функция выполнится: победа,
                        метку убираем через <code>clearLabel</code>.</li>
                </ul>
                <Note>
                    Босс <code>hp: 120</code>, удар снимает 20 — значит
                    нужно 6 точных кликов. Меняй эти числа, чтобы сделать
                    бой легче или сложнее.
                </Note>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>пройди паркур по платформам до арены;</li>
                    <li>на арене появится босс с меткой HP;</li>
                    <li>подбегай и кликай — HP босса падает;</li>
                    <li>HP дошло до нуля — «Победа».</li>
                </ul>

                <Try>
                    сделай босса сильнее — больше HP или быстрее. Добавь
                    второго мини-босса. Сделай, чтобы босс тоже наносил
                    игроку урон при касании — паркур обратно за аптечкой.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 34 — «Сбор урожая»
    // ════════════════════════════════════════════════════
    'harvest': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На грядках растут шесть растений. Каждое медленно растёт
                    (анимация-твин) и за 5 секунд становится спелым — жёлтым.
                    Только спелое растение можно собрать клавишей
                    <kbd className="kbd">E</kbd>. Собрал слишком рано —
                    не считается. Собери весь урожай.
                </p>
                <Shot src="lesson34-result.png"
                      caption="Дождись, пока растение поспеет, и собирай" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Твин роста</b> — как плавно увеличить объект
                        (<code>game.tween</code> по размеру);</li>
                    <li><b>Событие <code>onDone</code></b> — сделать что-то,
                        когда твин закончился;</li>
                    <li><b>Состояние «спелое»</b> — флажок, который меняет
                        правила взаимодействия.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Грядки и растения</h3>
                <Step n="1">
                    Выложи поле из блоков земли — это грядки.
                </Step>
                <Step n="2">
                    Поставь 6 растений — конусы-примитивы, изначально
                    <b> маленькие</b> (например, размер 0.4×0.5×0.4),
                    зелёного цвета. «Столкновение» включено. Имена —
                    «Растение_1» … «Растение_6».
                </Step>
                <Shot src="lesson34-scene.png"
                      caption="Шесть маленьких растений на грядках" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «СБОР УРОЖАЯ» — главный скрипт ===

let harvested = 0;
const GOAL = 6;
game.ui.score = 0;
game.ui.showText('Дождись, пока растения вырастут, и собери!', 4);

// растения сообщают о сборе через broadcast('harvested') — скрипты
// в разных песочницах, счётчик harvested живёт только здесь.
game.onMessage('harvested', () => {
  harvested = harvested + 1;
  game.ui.score = harvested;
  game.sound.play('coin');
  if (harvested >= GOAL) {
    game.ui.showText('Победа! Весь урожай собран!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</Code>
                <p>
                    Главный скрипт считает собранные растения и проверяет
                    победу. Каждое растение работает в своей «песочнице»,
                    поэтому о сборе оно сообщает через
                    <code> game.broadcast('harvested')</code>, а главный
                    скрипт ловит сообщение через
                    <code> game.onMessage('harvested', ...)</code>. Вся
                    «грядочная магия» — в скрипте растения.
                </p>

                <h3 className="lessonH">Шаг 3. Скрипт растения</h3>
                <p>Этот скрипт вешается на <b>каждое</b> растение.</p>
                <ScriptKind kind="object" on="каждое растение" />
                <Code>{`// === Скрипт растения ===

let ripe = false;       // растение выросло (спелое)?
let picked = false;

// растение медленно растёт за 5 секунд. Вместе с sy растёт и y —
// чтобы низ конуса оставался на земле (растение «растёт из земли»).
game.tween(game.self.ref,
  { sx: 1.3, sy: 2.6, sz: 1.3, y: 2.3 },
  { duration: 5, onDone: () => {
      ripe = true;
      game.scene.setColor(game.self.ref, '#ffcc33');  // спелое — жёлтое
    }
  });

// собрать растение (E)
game.self.onInteract(() => {
  if (picked) return;
  if (!ripe) {
    game.ui.showText('Ещё не выросло! Подожди.', 1.5);
    return;
  }
  picked = true;
  game.self.delete();
  game.broadcast('harvested');
}, { text: 'Собрать', distance: 3 });`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.tween(ref, {'{ sx, sy, sz, y }'}, опции)</code> —
                        твин по размеру. Растение плавно увеличивается
                        с маленького до большого за 5 секунд. Заодно растёт
                        и <code>y</code> — чтобы низ конуса оставался на земле,
                        а не уходил под неё;</li>
                    <li><code>onDone: () =&gt; {'{...}'}</code> — функция,
                        которая выполнится, <b>когда твин закончится</b>.
                        Внутри: ставим <code>ripe = true</code> (растение
                        спелое) и красим его в жёлтый;</li>
                    <li><code>game.self.onInteract</code> — сбор по
                        <kbd className="kbd">E</kbd>. Если
                        <code> !ripe</code> — растение ещё не поспело,
                        показываем подсказку и выходим;</li>
                    <li>спелое — <code>picked = true</code>, растение
                        исчезает и засчитывается.</li>
                </ul>
                <Note>
                    Флажок <code>ripe</code> меняет правила: до того как он
                    стал <code>true</code>, собрать растение нельзя. Это
                    и есть «состояние» объекта.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>с началом игры растения начинают расти;</li>
                    <li>попробуй собрать раннее — «Ещё не выросло!»;</li>
                    <li>через 5 секунд растение пожелтело — спелое;</li>
                    <li>собери все 6 спелых — «Победа».</li>
                </ul>

                <Try>
                    сделай растения с разным временем роста — одни за 3
                    секунды, другие за 8. Добавь «перезрелое» состояние:
                    если не собрать вовремя, растение чернеет и пропадает.
                    Поставь грядок побольше.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 35 — «Прятки от NPC»
    // ════════════════════════════════════════════════════
    'hide-from-npc': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    По полю ходит NPC-искатель и постоянно идёт к игроку.
                    Среди поля расставлены стены-укрытия. Нужно петлять
                    за ними и не дать себя поймать целых 40 секунд. Поймал —
                    игрок возвращается на старт, но таймер не сбрасывается.
                </p>
                <Shot src="lesson35-result.png"
                      caption="Искатель ищет тебя — прячься 40 секунд" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>NPC-преследователь</b> —
                        <code> spawnNpc</code> + <code>follow('player')</code>;</li>
                    <li><b>Таймер выживания</b> — продержаться нужное
                        количество секунд (<code>game.ui.timer</code> +
                        <code> dt</code>);</li>
                    <li><b>Проверка поимки</b> — <code>Math.hypot</code>
                        между игроком и NPC.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле и укрытия</h3>
                <Step n="1">
                    Выложи большое травяное поле — простор, чтобы было
                    где убегать.
                </Step>
                <Step n="2">
                    Расставь по полю 7 стен-укрытий — серые кубы-примитивы
                    4×4×1.5. «Столкновение» и «Закреплён» включены.
                    Расположи их так, чтобы вокруг каждого можно было
                    обежать.
                </Step>
                <Step n="3">
                    Точку спавна поставь в углу поля.
                </Step>
                <p>
                    NPC-искатель создаётся скриптом — ставить его заранее
                    не нужно.
                </p>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>Вся игра — в одном скрипте. Разберём его.</p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ПРЯТКИ ОТ NPC» — главный скрипт ===

let time = 0;
const SURVIVE = 40;       // продержись 40 секунд
let won = false;

game.ui.timer = 0;
game.ui.showText('Прячься за стенами 40 секунд!', 4);

// NPC-искатель ходит за игроком
const seeker = game.scene.spawnNpc('character-b', {
  x: 0, y: 1, z: 10, name: 'Искатель', hp: 100, speed: 3,   // y=1 — на полу
});
seeker.follow('player');

game.onTick((dt) => {
  if (won) return;
  time = time + dt;
  game.ui.timer = time;

  // искатель поймал — на старт, время продолжается.
  // seeker.position появляется через кадр после spawnNpc — ждём.
  const p = game.player.position;
  const e = seeker.position;
  if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 1.7) {
    game.player.respawn();
    game.ui.showText('Найден! Прячься снова!', 1.5);
    game.sound.play('lose');
  }

  // продержался — победа
  if (time >= SURVIVE) {
    won = true;
    seeker.stop();
    game.ui.showText('Победа! Ты прятался 40 секунд!', 5);
    game.sound.play('win');
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.scene.spawnNpc(...)</code> создаёт
                        искателя, <code>seeker.follow('player')</code> —
                        он гонится за игроком;</li>
                    <li><code>time = time + dt</code> каждый кадр копит
                        прошедшее время, <code>game.ui.timer</code> выводит
                        секундомер;</li>
                    <li><code>Math.hypot(...) {'<'} 1.7</code> — искатель
                        вплотную. Игрок возвращается на старт, но
                        <code> time</code> не обнуляется — таймер идёт
                        дальше;</li>
                    <li><code>if (time {'>'}= SURVIVE)</code> — продержался
                        40 секунд: <code>seeker.stop()</code> останавливает
                        искателя, показывается «Победа».</li>
                </ul>
                <Note>
                    Хитрость игры: при поимке таймер <b>не сбрасывается</b>.
                    Игрока просто отбрасывает на старт. Поэтому даже если
                    раз поймали — до победы всё равно недалеко.
                </Note>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>с началом игры за тобой ходит искатель;</li>
                    <li>петляй вокруг серых стен, чтобы он не догнал;</li>
                    <li>поймал — респаун на старте, таймер идёт дальше;</li>
                    <li>продержался 40 секунд — «Победа».</li>
                </ul>

                <Try>
                    добавь второго искателя — игра станет напряжённее.
                    Сделай искателя быстрее или удлини время выживания.
                    Поставь больше укрытий — или, наоборот, убери часть,
                    чтобы прятаться было сложнее.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 36 — «Головоломка с ящиками»
    // ════════════════════════════════════════════════════
    'box-puzzle': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На поле стоят 3 коричневых ящика и 3 зелёные плиты-цели.
                    Игрок подходит к ящику и нажимает <kbd className="kbd">E</kbd>
                    — ящик плавно перепрыгивает на следующую клетку. Задача —
                    расставить все ящики по их плитам. Получилось — победа.
                </p>
                <Shot src="lesson36-result.png"
                      caption="Головоломка: ящики нужно поставить на зелёные плиты" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Твин-перемещение</b> — как плавно сдвигать объект
                        из точки в точку (<code>game.tween</code>);</li>
                    <li><b>Список позиций</b> — как хранить ряд клеток в массиве
                        и ходить по нему по кругу;</li>
                    <li><b>Проверка состояния</b> — как понять, что все три
                        ящика стоят на своих местах;</li>
                    <li><b>Несколько объектов с одной логикой</b> — каждый ящик
                        знает свой номер.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле, плиты и ящики</h3>
                <Step n="1">
                    Построй ровную площадку из камня примерно 18×18 блоков.
                </Step>
                <Step n="2">
                    Поставь 3 примитива-<b>цилиндра</b> в ряд впереди: размер
                    2.4×0.2×2.4, цвет зелёный <code>#22dd55</code>, материал
                    «Неон». Это плиты-цели. «Столкновение» — выключи, чтобы
                    ящик мог встать прямо на плиту.
                </Step>
                <Step n="3">
                    Поставь 3 примитива-<b>куба</b> размером 1.8 — это ящики.
                    Цвет коричневый <code>#b5651d</code>. Каждый ящик — напротив
                    своей плиты, но в дальнем ряду. Дай имена «Ящик_1»,
                    «Ящик_2», «Ящик_3».
                </Step>
                <Shot src="lesson36-scene.png"
                      caption="Три ящика в дальнем ряду, три зелёные плиты у игрока" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт хранит, какие ящики уже на плитах, и ловит
                    момент победы.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ГОЛОВОЛОМКА С ЯЩИКАМИ» — главный скрипт ===

// для каждого ящика — на какой плите он сейчас (true/false)
const onPlate = [false, false, false];
let won = false;

game.ui.showText('Поставь все 3 ящика на зелёные плиты!', 4);

// Ящики сообщают сюда через game.broadcast('box', { i, on }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('box', (d) => {
  // ящик с номером d.i (0..2) встал/сошёл с плиты
  onPlate[d.i] = d.on;
  if (d.on) game.sound.play('click');
  // все три ящика на плитах?
  if (!won && onPlate[0] && onPlate[1] && onPlate[2]) {
    won = true;
    game.ui.showText('Победа! Все ящики на местах!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>onPlate</code> — массив из трёх «галочек»: стоит
                        ли ящик №0, №1, №2 на плите;</li>
                    <li><code>game.onMessage('box', ...)</code> — главный скрипт
                        ждёт сообщение «box» от ящиков. В <code>d.i</code> —
                        номер ящика, в <code>d.on</code> — стоит ли он на плите;</li>
                    <li><code>if (onPlate[0] {'&&'} onPlate[1] {'&&'} onPlate[2])</code>
                        — победа, только когда все три «галочки» стоят разом.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт ящика</h3>
                <p>
                    Скрипт висит на ящике. Каждый ящик ходит по своему ряду
                    из 5 клеток. Тут показан ящик №1 (для второго и третьего
                    поменяй число <code>i</code> в сообщении на 1 и 2).
                </p>
                <ScriptKind kind="object" on="каждый ящик" />
                <Code>{`// === Скрипт ящика 1 ===

// ряд позиций по Z, по которым прыгает ящик
const ROW = [-6, -3, 0, 3, 6];
let cell = 0;                  // на какой клетке ряда стоит ящик
const PLATE_Z = 6;             // плита в конце ряда

game.self.onInteract(() => {
  // перепрыгиваем на следующую клетку (по кругу)
  cell = (cell + 1) % ROW.length;
  const z = ROW[cell];
  game.tween(game.self.ref, { z: z }, { duration: 0.4, easing: 'ease' });
  // сообщаем главному скрипту — стоит ли ящик на плите
  game.broadcast('box', { i: 0, on: z === PLATE_Z });
}, { text: 'Двинуть ящик', distance: 3 });`}</Code>
                <p>Что происходит:</p>
                <ul>
                    <li><code>ROW</code> — пять клеток ряда (значения Z);</li>
                    <li><code>cell = (cell + 1) % ROW.length</code> — переходим
                        к следующей клетке; <code>%</code> заворачивает с конца
                        ряда обратно к началу;</li>
                    <li><code>game.tween(...)</code> — плавно сдвигает ящик
                        на новую клетку за 0.4 секунды;</li>
                    <li><code>z === PLATE_Z</code> — проверка «ящик на плите?»:
                        результат (<code>true</code>/<code>false</code>) уходит
                        в поле <code>on</code> сообщения;</li>
                    <li><code>game.broadcast('box', ...)</code> — шлёт сообщение
                        «box» главному скрипту, а тот ловит его через
                        <code> game.onMessage('box', ...)</code>.</li>
                </ul>
                <Note>
                    У каждого ящика свой номер <code>i</code>: первый шлёт
                    <code> {'{'} i: 0 {'}'}</code>, второй — <code>{'{'} i: 1 {'}'}</code>,
                    третий — <code>{'{'} i: 2 {'}'}</code>. Не перепутай числа!
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>подойди к ящику, нажми <kbd className="kbd">E</kbd> —
                        ящик плавно прыгает вперёд;</li>
                    <li>доведи каждый ящик до его зелёной плиты;</li>
                    <li>все три на местах — «Победа» и конфетти.</li>
                </ul>

                <Try>
                    добавь четвёртый ящик и плиту. Сделай ряды длиннее —
                    головоломка станет дольше. Поставь ящикам разные цвета и
                    плиты в тон — пусть каждый ящик ищет «свой» цвет.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 37 — «Полоса препятствий»
    // ════════════════════════════════════════════════════
    'obstacle-course': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Длинная трасса с препятствиями: красные шипы наносят урон,
                    в полу — ямы, над одной ямой ездит синяя платформа. Посередине
                    стоит жёлтый чекпоинт — пройдёшь его, и после падения игра
                    вернёт тебя сюда, а не на старт. В конце — зелёный финиш.
                </p>
                <Shot src="lesson37-result.png"
                      caption="Полоса препятствий: шипы, ямы, движущаяся платформа" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Урон от препятствия</b> — как отнять у игрока
                        здоровье (<code>game.player.damage</code>);</li>
                    <li><b>Движущаяся платформа</b> — твин «туда-обратно»
                        с <code>yoyo: true</code>;</li>
                    <li><b>Чекпоинт</b> — как перенести точку возрождения
                        (<code>game.player.setSpawn</code>);</li>
                    <li><b>Падение в яму</b> — возврат игрока через
                        <code> onTick</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Трасса с ямами</h3>
                <Step n="1">
                    Построй длинную дорожку из камня — 8 блоков в ширину,
                    около 50 в длину.
                </Step>
                <Step n="2">
                    Вырежи в полу две <b>ямы</b> — просто не клади блоки в этих
                    местах. Игрок, который провалится, упадёт вниз.
                </Step>
                <Step n="3">
                    Расставь по трассе примитивы-<b>конусы</b> красного цвета —
                    это шипы. Размер 1.2×1.6×1.2, материал «Неон», «Столкновение»
                    выключи (касание ловит скрипт).
                </Step>
                <Step n="4">
                    Над первой ямой поставь синий куб «ДвижПлатформа» (3×0.5×3),
                    над второй — узкий коричневый «Мостик». Посередине трассы —
                    жёлтый конус «Чекпоинт». В конце — зелёный куб «Финиш».
                </Step>
                <Shot src="lesson37-scene.png"
                      caption="Трасса с шипами, ямами, платформой и чекпоинтом" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ПОЛОСА ПРЕПЯТСТВИЙ» — главный скрипт ===

let won = false;

game.ui.showText('Пройди полосу: шипы, ямы, платформа!', 4);

// движущаяся платформа ездит над ямой туда-сюда.
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже.
game.after(0.2, () => {
  const mover = game.scene.findOne('ДвижПлатформа');
  game.tween(mover, { x: 3 }, {
    duration: 2, yoyo: true, repeat: 999, easing: 'ease'
  });
});

game.onTick(() => {
  if (won) return;
  const p = game.player.position;
  if (p && p.y < -3) {
    game.player.respawn();   // упал в яму — на чекпоинт/старт
    game.sound.play('lose');
  }
});

// Чекпоинт и финиш сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('checkpoint', () => {
  // ставим точку возрождения на чекпоинт
  game.player.setSpawn({ x: -0.5, y: 1, z: 24 });
  game.ui.showText('Чекпоинт сохранён!', 2);
  game.sound.play('pickup');
});

game.onMessage('finish', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Полоса пройдена!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.after(0.2, ...)</code> — ждём чуть-чуть,
                        потому что <code>findOne</code> в самом начале ещё не
                        видит объекты сцены;</li>
                    <li><code>game.tween(mover, ...)</code> с <code>yoyo: true</code>
                        и <code>repeat: 999</code> — платформа бесконечно
                        ездит туда и обратно;</li>
                    <li><code>onTick</code> ловит падение в яму
                        (<code>p.y {'<'} -3</code>) и возвращает игрока;</li>
                    <li><code>game.onMessage('checkpoint', ...)</code> ловит
                        сообщение от чекпоинта и зовёт
                        <code> game.player.setSpawn(...)</code> — теперь
                        респаун будет в середине трассы;</li>
                    <li><code>game.onMessage('finish', ...)</code> — победа
                        с конфетти, флажок <code>won</code> защищает от повтора.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипты шипа, чекпоинта и финиша</h3>
                <ScriptKind kind="object" on="каждый красный шип" />
                <Code>{`// === Скрипт шипа ===
game.self.onTouch(() => {
  game.player.damage(25);
  game.sound.play('hit');
});`}</Code>
                <ScriptKind kind="object" on="жёлтый чекпоинт" />
                <Code>{`// === Скрипт чекпоинта ===
game.self.onTouch(() => {
  game.broadcast('checkpoint');
});`}</Code>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('finish');
});`}</Code>
                <p>
                    Шип при касании отнимает 25 здоровья. Чекпоинт сохраняет
                    точку возрождения. Финиш зовёт победу.
                </p>
                <Note>
                    <code>setSpawn</code> меняет место, куда вернёт
                    <code> respawn</code>. До чекпоинта это старт, после —
                    середина трассы. Так игроку не приходится бежать заново.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>наступил на шип — здоровье уменьшилось;</li>
                    <li>упал в яму — вернуло на старт (или на чекпоинт);</li>
                    <li>прошёл жёлтый конус — респаун теперь посередине;</li>
                    <li>дошёл до зелёного финиша — «Победа».</li>
                </ul>

                <Try>
                    добавь второй чекпоинт ближе к финишу. Сделай шипы,
                    которые двигаются (твин). Добавь монетки на сложных
                    участках — награда за риск.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 38 — «Музыкальная игра»
    // ════════════════════════════════════════════════════
    'music-game': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    На поле 4 цветные плитки-ноты. Игра сначала проигрывает
                    мелодию из 5 нот — слушай внимательно. Потом нужно повторить
                    её, нажимая плитки <kbd className="kbd">E</kbd> в том же
                    порядке. Ошибся — мелодия начинается сначала.
                </p>
                <Shot src="lesson38-result.png"
                      caption="Четыре ноты-плитки: запомни мелодию и повтори" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Последовательность</b> — как хранить мелодию
                        в массиве чисел;</li>
                    <li><b>Отложенные события</b> — как проиграть ноты одну
                        за другой через <code>game.after</code>;</li>
                    <li><b>Сравнение с эталоном</b> — как проверить, что игрок
                        нажимает в правильном порядке;</li>
                    <li><b>Разные звуки</b> — у каждой плитки свой
                        <code> game.sound</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле и 4 ноты</h3>
                <Step n="1">
                    Построй небольшую площадку из блоков (примерно 16×13).
                </Step>
                <Step n="2">
                    Поставь 4 примитива-<b>куба</b> в ряд: размер 2.4×0.4×2.4,
                    материал «Неон». Цвета — красный, жёлтый, зелёный, синий.
                    Имена «Нота_1»…«Нота_4». «Столкновение» включено.
                </Step>
                <Shot src="lesson38-scene.png"
                      caption="Ряд из четырёх цветных нот-плиток" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт хранит мелодию, проигрывает её и проверяет,
                    верно ли игрок повторяет.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «МУЗЫКАЛЬНАЯ ИГРА» — главный скрипт ===

const SOUNDS = ['coin', 'jump', 'click', 'hit'];   // плитки 1..4
// загаданная последовательность из 5 нот
const SEQ = [1, 3, 2, 4, 1];
let playerStep = 0;       // на каком шаге игрок
let won = false;
let canPress = false;

game.ui.showText('Слушай мелодию, потом повтори!', 3);

// проигрываем мелодию: нота за нотой каждые 0.8 сек
SEQ.forEach((note, i) => {
  game.after(1 + i * 0.8, () => {
    game.sound.play(SOUNDS[note - 1]);
    game.ui.showText('Нота ' + (i + 1) + ' из ' + SEQ.length, 0.7);
  });
});
// после мелодии разрешаем игроку повторять
game.after(1 + SEQ.length * 0.8 + 0.5, () => {
  canPress = true;
  game.ui.showText('Теперь повтори мелодию!', 3);
});

// Плитки сообщают сюда нажатую ноту через game.broadcast('press', { n }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('press', (d) => {
  if (won || !canPress) return;
  // правильная нота?
  if (d.n === SEQ[playerStep]) {
    playerStep = playerStep + 1;
    if (playerStep >= SEQ.length) {
      won = true;
      game.ui.showText('Победа! Мелодия повторена верно!', 5);
      game.sound.play('win');
      const p = game.player.position;
      game.scene.spawnParticles('confetti',
        { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
    }
  } else {
    // ошибка — сброс
    playerStep = 0;
    game.ui.showText('Ошибка! Слушай и пробуй снова.', 2);
    game.sound.play('lose');
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>SEQ = [1, 3, 2, 4, 1]</code> — загаданная мелодия:
                        номера нот по порядку;</li>
                    <li><code>SEQ.forEach((note, i) ={'>'} ...)</code> — для
                        каждой ноты ставим таймер: нота <code>i</code> прозвучит
                        через <code>1 + i * 0.8</code> секунд;</li>
                    <li><code>canPress</code> — флажок: нажимать можно только
                        после того, как мелодия доиграла;</li>
                    <li><code>game.onMessage('press', ...)</code> ловит сообщение
                        от плитки; <code>d.n</code> — номер нажатой ноты;</li>
                    <li><code>if (d.n === SEQ[playerStep])</code> — нажатая нота
                        совпала с нужной? Да — шаг вперёд; нет — сброс
                        <code> playerStep</code> в ноль.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт ноты-плитки</h3>
                <p>
                    Скрипт висит на каждой плитке. Тут показана нота №1 —
                    для остальных поменяй звук и число в <code>press</code>.
                </p>
                <ScriptKind kind="object" on="каждую ноту-плитку" />
                <Code>{`// === Скрипт ноты-плитки 1 ===
game.self.onInteract(() => {
  game.sound.play('coin');
  game.scene.spawnParticles('sparks', game.self.position,
    { duration: 0.4, color: '#e23b3b' });
  game.broadcast('press', { n: 1 });
}, { text: 'Сыграть ноту', distance: 3 });`}</Code>
                <p>
                    При нажатии <kbd className="kbd">E</kbd> плитка играет
                    свой звук, вспыхивает искрами и шлёт
                    <code> game.broadcast('press', {'{'} n: 1 {'}'})</code> —
                    сообщение со своим номером. У ноты №2 будет
                    <code> {'{'} n: 2 {'}'}</code>, у №3 —
                    <code> {'{'} n: 3 {'}'}</code> и т.д.
                </p>
                <Note>
                    Цвет искр в <code>spawnParticles</code> и звук должны
                    совпадать с цветом плитки — так игроку понятнее, что
                    он нажал.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>в начале игра сама проигрывает 5 нот по очереди;</li>
                    <li>дождись надписи «Теперь повтори мелодию»;</li>
                    <li>нажимай плитки в том же порядке;</li>
                    <li>повторил всё верно — «Победа», ошибся — сброс.</li>
                </ul>

                <Try>
                    удлини мелодию <code>SEQ</code> до 7-8 нот. Сделай 5-ю
                    плитку. Добавь подсветку: пусть плитка на миг меняет цвет,
                    когда игра её проигрывает.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 39 — «Башня — стройка»
    // ════════════════════════════════════════════════════
    'tower-build': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Над землёй висят 8 полупрозрачных «призрачных» блоков один
                    над другим. Игрок подходит к призраку и нажимает
                    <kbd className="kbd">E</kbd> — блок становится настоящим:
                    плотным и коричневым. Строить нужно строго снизу вверх.
                    Построил всю башню — победа.
                </p>
                <Shot src="lesson39-result.png"
                      caption="Башня собрана из 8 блоков снизу вверх" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Призрак-блок</b> — стеклянный примитив, который
                        нельзя задеть;</li>
                    <li><b>Превращение объекта</b> — как сделать призрак твёрдым
                        и непрозрачным (<code>passThrough</code>,
                        <code> setOpacity</code>, <code>setCollide</code>);</li>
                    <li><b>Порядок действий</b> — проверка «следующий по
                        очереди»;</li>
                    <li><b>Счётчик стройки</b> — сколько блоков уже поставлено.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле и места-призраки</h3>
                <Step n="1">
                    Построй площадку из травы (16×16 блоков).
                </Step>
                <Step n="2">
                    Поставь примитив-<b>куб</b> размером 2×2×2 на полу. Цвет
                    светло-синий <code>#88aaff</code>, материал
                    <b> «Стекло»</b> — куб станет полупрозрачным.
                    «Столкновение» выключи: сквозь призрак можно пройти.
                </Step>
                <Step n="3">
                    Дублируй куб 7 раз и поставь их <b>стопкой друг над другом</b>
                    — каждый следующий на 2 единицы выше. Имена
                    «Место_1»…«Место_8», снизу вверх.
                </Step>
                <Shot src="lesson39-scene.png"
                      caption="Стопка из восьми полупрозрачных мест-призраков" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «БАШНЯ — СТРОЙКА» — главный скрипт ===

const STEPS = 8;
let placed = 0;           // сколько блоков поставлено

game.ui.score = 0;
game.ui.showText('Подходи к местам и ставь блоки (E) снизу вверх', 4);

// Места-призраки сообщают сюда через game.broadcast('place', { n }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('place', (d) => {
  // блок поставлен в место номер d.n (1..STEPS);
  // ставить можно только следующее по порядку место
  if (d.n !== placed + 1) {
    game.ui.showText('Сначала поставь блок ниже!', 1.5);
    return;
  }
  placed = placed + 1;
  game.ui.score = placed;
  game.sound.play('click');
  if (placed >= STEPS) {
    game.ui.showText('Победа! Башня построена!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  } else {
    game.ui.showText('Блок ' + placed + ' из ' + STEPS, 1.5);
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>placed</code> — сколько блоков уже стоит;</li>
                    <li><code>game.onMessage('place', ...)</code> ловит сообщение
                        от места-призрака; <code>d.n</code> — его номер;</li>
                    <li><code>if (d.n !== placed + 1)</code> — если игрок жмёт
                        не на следующий по очереди блок, стройка не засчитывается;</li>
                    <li><code>game.ui.score = placed</code> — счётчик показывает
                        прогресс башни;</li>
                    <li>когда <code>placed</code> дошёл до 8 — победа.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт места-призрака</h3>
                <p>
                    Скрипт висит на каждом призраке. Тут показано место №1 —
                    у остальных поменяй число в <code>place</code>.
                </p>
                <ScriptKind kind="object" on="каждое место-призрак" />
                <Code>{`// === Скрипт места под блок 1 ===
let built = false;
game.self.onInteract(() => {
  if (built) return;
  // ставим настоящий блок — делаем призрак твёрдым и непрозрачным
  game.physics.passThrough(game.self.ref, false);
  game.scene.setOpacity(game.self.ref, 1);
  game.scene.setColor(game.self.ref, '#b5651d');
  game.scene.setCollide(game.self.ref, true);
  built = true;
  game.broadcast('place', { n: 1 });
}, { text: 'Поставить блок', distance: 4 });`}</Code>
                <p>Что происходит при нажатии:</p>
                <ul>
                    <li><code>passThrough(ref, false)</code> — сквозь блок
                        больше нельзя пройти;</li>
                    <li><code>setOpacity(ref, 1)</code> — блок становится
                        полностью непрозрачным;</li>
                    <li><code>setColor(ref, '#b5651d')</code> — перекрашиваем
                        в коричневый «деревянный»;</li>
                    <li><code>setCollide(ref, true)</code> — теперь на блок
                        можно встать.</li>
                </ul>
                <Note>
                    У каждого места свой номер <code>n</code> в сообщении:
                    место_1 шлёт <code>{'{'} n: 1 {'}'}</code>, место_2 —
                    <code> {'{'} n: 2 {'}'}</code> и так до 8.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>подойди к нижнему призраку, нажми
                        <kbd className="kbd">E</kbd> — он стал коричневым;</li>
                    <li>встань на новый блок и ставь следующий выше;</li>
                    <li>если жмёшь не по порядку — подсказка «сначала ниже»;</li>
                    <li>построил все 8 — «Победа».</li>
                </ul>

                <Try>
                    сделай башню выше — 12-15 блоков. Добавь блоки разного
                    цвета. Поставь монетку на самом верху как награду
                    за постройку.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 40 — «Выживание от волн»
    // ════════════════════════════════════════════════════
    'wave-survival': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Открытая арена. На игрока накатывают 3 волны врагов-NPC,
                    одна за другой. С каждой волной врагов всё больше. Кликай
                    по врагам, чтобы их уничтожать. Отбил все 3 волны — победа.
                </p>
                <Shot src="lesson40-result.png"
                      caption="Враги наступают волнами — отбей все три" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>NPC-враги</b> — как создавать персонажей-врагов
                        (<code>spawnNpc</code>) и заставлять их преследовать;</li>
                    <li><b>Волны</b> — как запускать врагов группами;</li>
                    <li><b>Рекурсивный вызов</b> — как функция волны запускает
                        саму себя для следующей волны;</li>
                    <li><b>Клик по цели</b> — проверка расстояния при
                        <code> game.onClick</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Арена</h3>
                <Step n="1">
                    Построй большую ровную площадку из камня (примерно 24×24
                    блока). Стены не нужны — врагов лучше видно на открытом поле.
                </Step>
                <Step n="2">
                    Поставь точку спавна игрока в центр арены.
                </Step>
                <Note>
                    Враги создаются прямо в скрипте функцией
                    <code> spawnNpc</code> — заранее ставить их не нужно.
                </Note>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Вся игра — в одном глобальном скрипте: он запускает волны
                    и считает врагов.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ВЫЖИВАНИЕ ОТ ВОЛН» — главный скрипт ===

const WAVES = 3;          // всего волн
let wave = 0;
let won = false;

game.ui.showText('Отбей 3 волны врагов! Кликай по ним', 3);

// запускаем очередную волну
function startWave() {
  if (won) return;
  wave = wave + 1;
  game.ui.showText('Волна ' + wave + ' из ' + WAVES + '!', 3);
  game.sound.play('hit');

  const count = wave + 2;          // врагов всё больше: 3, 4, 5
  let aliveInWave = count;

  for (let i = 0; i < count; i++) {
    // враг появляется по краю поля
    const angle = (i / count) * 6.28;
    const ex = Math.cos(angle) * 10;
    const ez = Math.sin(angle) * 10;
    const enemy = game.scene.spawnNpc('character-b', {
      x: ex, y: 1, z: ez, name: 'Враг', hp: 40, speed: 2,   // y=1 — на полу
    });
    enemy.follow('player');

    let dead = false;
    game.onClick(() => {
      if (dead || won) return;
      const p = game.player.position;
      const e = enemy.position;
      if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 5) {
        dead = true;
        enemy.remove();
        game.scene.spawnParticles('explosion', e, { count: 1 });
        game.sound.play('hit');
        aliveInWave = aliveInWave - 1;
        // вся волна перебита — следующая
        if (aliveInWave <= 0) {
          if (wave >= WAVES) {
            won = true;
            game.ui.showText('Победа! Все волны отбиты!', 5);
            game.sound.play('win');
            const pp = game.player.position;
            if (pp) game.scene.spawnParticles('confetti',
              { x: pp.x, y: pp.y + 3, z: pp.z }, { duration: 3, count: 3 });
          } else {
            game.after(2, startWave);
          }
        }
      }
    });
  }
}

game.after(2, startWave);   // первая волна через 2 секунды`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>startWave()</code> — функция одной волны. Она
                        создаёт <code>wave + 2</code> врагов: 3, потом 4, потом 5;</li>
                    <li><code>Math.cos / Math.sin</code> — расставляют врагов
                        по кругу вокруг арены;</li>
                    <li><code>enemy.follow('player')</code> — враг идёт
                        к игроку;</li>
                    <li><code>game.onClick(...)</code> — при клике проверяем
                        <code> Math.hypot(...) {'<'} 5</code>: враг рядом — он
                        уничтожен;</li>
                    <li><code>if (aliveInWave {'<='} 0) ... game.after(2, startWave)</code>
                        — вся волна перебита, через 2 секунды функция
                        <b> запускает сама себя</b> для следующей волны. Это
                        и есть рекурсия.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>через 2 секунды появляется первая волна из 3 врагов;</li>
                    <li>кликай по врагам рядом — они взрываются;</li>
                    <li>перебил всю волну — через 2 секунды идёт следующая;</li>
                    <li>отбил 3 волны — «Победа» и конфетти.</li>
                </ul>
                <Note>
                    Рекурсия — это когда функция вызывает саму себя.
                    Здесь <code>startWave</code> запускает <code>startWave</code>
                    снова, пока не кончатся волны. Главное — есть условие
                    остановки (<code>wave {'>='} WAVES</code>), иначе волны
                    шли бы бесконечно.
                </Note>

                <Try>
                    сделай 5 волн вместо 3. Дай врагам больше здоровья
                    (<code>hp</code>) или скорости (<code>speed</code>) —
                    станет сложнее. Добавь между волнами «передышку» подлиннее.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 41 — «Платформер-приключение»
    // ════════════════════════════════════════════════════
    'adventure-platformer': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Большой уровень-приключение: длинная цепочка платформ-паркура,
                    на пути — золотые монетки, посередине жёлтый чекпоинт,
                    а на самом верху — финиш-сокровище. Эта игра собирает вместе
                    почти всё, что ты прошёл в простых уроках.
                </p>
                <Shot src="lesson41-result.png"
                      caption="Большой платформер: паркур, монетки, чекпоинт, сокровище" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Собирать игру из кусочков</b> — как соединить паркур,
                        монетки, чекпоинт и финиш в один уровень;</li>
                    <li><b>Большой главный скрипт</b> — один скрипт ловит
                        сообщения от всех объектов игры;</li>
                    <li><b>Несколько целей сразу</b> — добраться до финиша
                        И собрать монетки.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Большой уровень</h3>
                <Step n="1">
                    Поставь стартовую площадку из травы внизу.
                </Step>
                <Step n="2">
                    Выстрой цепочку платформ-кубов (2.5×0.5×2.5) вверх и вперёд
                    — около 9 штук, каждая чуть выше предыдущей.
                </Step>
                <Step n="3">
                    Посередине подъёма сделай площадку из камня — это зона
                    чекпоинта. Сверху — финишную площадку.
                </Step>
                <Step n="4">
                    Расставь золотые сферы-монетки над некоторыми платформами.
                    Поставь жёлтый конус «Чекпоинт» и золотой куб «Сокровище».
                </Step>
                <Shot src="lesson41-scene.png"
                      caption="Уровень: паркур со старта вверх к сокровищу" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ПЛАТФОРМЕР-ПРИКЛЮЧЕНИЕ» — главный скрипт ===

let coins = 0;
let won = false;

game.ui.score = 0;
game.ui.showText('Доберись до сокровища! Собирай монетки', 4);

game.onTick(() => {
  if (won) return;
  // позиция игрока может быть ещё не готова первые кадры —
  // проверяем p перед обращением к p.y.
  const p = game.player.position;
  if (p && p.y < -3) {
    game.player.respawn();
    game.sound.play('lose');
  }
});

// Монетки, чекпоинт и сокровище сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('coin', () => {
  coins = coins + 1;
  game.ui.score = coins;
  game.sound.play('coin');
});

game.onMessage('checkpoint', () => {
  game.player.setSpawn({ x: -0.5, y: 7, z: 28 });
  game.ui.showText('Чекпоинт! Дальше — отсюда.', 2);
  game.sound.play('pickup');
});

game.onMessage('treasure', () => {
  if (won) return;
  won = true;
  game.ui.showText('Победа! Сокровище и ' + coins + ' монет!', 6);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.onMessage('coin', ...)</code> — пришла
                        монетка, +1 к счёту;</li>
                    <li><code>game.onMessage('checkpoint', ...)</code> —
                        перенести точку возрождения наверх;</li>
                    <li><code>game.onMessage('treasure', ...)</code> — победа:
                        в надписи показываем, сколько монеток успел собрать;</li>
                    <li><code>onTick</code> — следит за падением, как
                        в уроках 2 и 37.</li>
                </ul>
                <Note>
                    Главный скрипт большой, но устроен просто: он ждёт три
                    разных сообщения. Каждый объект уровня шлёт нужное ему
                    через <code>game.broadcast</code>.
                </Note>

                <h3 className="lessonH">Шаг 3. Скрипты монетки, чекпоинта и сокровища</h3>
                <ScriptKind kind="object" on="каждую монетку" />
                <Code>{`// === Скрипт монетки ===
game.self.onTouch(() => {
  game.broadcast('coin');
  game.self.delete();
});`}</Code>
                <ScriptKind kind="object" on="жёлтый чекпоинт" />
                <Code>{`// === Скрипт чекпоинта ===
game.self.onTouch(() => {
  game.broadcast('checkpoint');
});`}</Code>
                <ScriptKind kind="object" on="золотое сокровище" />
                <Code>{`// === Скрипт сокровища ===
game.self.onTouch(() => {
  game.broadcast('treasure');
});`}</Code>
                <p>
                    Монетка при касании засчитывается и исчезает. Чекпоинт
                    сохраняет место. Сокровище зовёт победу.
                </p>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>прыгай по платформам вверх, собирай монетки;</li>
                    <li>упал — респаун (на старт или на чекпоинт);</li>
                    <li>прошёл чекпоинт — дальше начинаешь отсюда;</li>
                    <li>добрался до сокровища — «Победа» с числом монеток.</li>
                </ul>

                <Try>
                    добавь шипы из урока 37 на сложные платформы. Сделай
                    врага-преследователя из урока 21. Добавь второй уровень
                    выше первого — длинное приключение.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 42 — «RPG-деревня»
    // ════════════════════════════════════════════════════
    'rpg-village': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Маленькая деревня с двумя жителями: старостой и кузнецом.
                    Поговори со старостой — он даст квест: найти потерянный
                    амулет за домом. Подними амулет и отнеси его кузнецу —
                    получишь награду. Это настоящая RPG с этапами квеста.
                </p>
                <Shot src="lesson42-result.png"
                      caption="RPG-деревня: квест от старосты к кузнецу" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>NPC-жители</b> — как создавать персонажей и заставлять
                        их говорить (<code>spawnNpc</code>, <code>say</code>);</li>
                    <li><b>Этапы квеста</b> — переменная <code>stage</code>,
                        которая помнит, на каком шаге игрок;</li>
                    <li><b>Инвентарь</b> — как класть и проверять предмет
                        (<code>game.inventory</code>);</li>
                    <li><b>Диалоги по условию</b> — NPC говорит разное в
                        зависимости от этапа.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Деревня</h3>
                <Step n="1">
                    Построй большую травяную площадку (примерно 32×32 блока).
                </Step>
                <Step n="2">
                    Сложи два домика из блоков — один деревянный, другой
                    из красного кирпича.
                </Step>
                <Step n="3">
                    Поставь две тумбы-кубы рядом с местами NPC: «Староста»
                    у входа, «Кузнец» у кирпичного дома. Это «кнопки» для
                    разговора.
                </Step>
                <Step n="4">
                    За дальним домом спрячь примитив-<b>тор</b> (бублик)
                    фиолетового цвета — это «Амулет».
                </Step>
                <Shot src="lesson42-scene.png"
                      caption="Деревня с двумя домами и спрятанным амулетом" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «RPG-ДЕРЕВНЯ» — главный скрипт ===

// этап: 0=начало, 1=ищем амулет, 2=несём кузнецу, 3=готово
let stage = 0;

game.ui.showText('Деревня. Поговори со старостой (E)', 4);

const elder = game.scene.spawnNpc('character-a', {
  x: 1.6, y: 1, z: 2, name: 'Староста', hp: 100, speed: 0,   // y=1 — на полу
});
const smith = game.scene.spawnNpc('character-b', {
  x: 12.6, y: 1, z: 7, name: 'Кузнец', hp: 100, speed: 0,    // y=1 — на полу
});

// Староста, амулет и кузнец сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('elderTalk', () => {
  if (stage === 0) {
    stage = 1;
    elder.say('Найди потерянный амулет за домом!', 4);
    game.ui.showText('Квест: найди фиолетовый амулет', 3);
  } else if (stage === 1) {
    elder.say('Амулет всё ещё не у тебя...', 3);
  } else {
    elder.say('Спасибо за помощь деревне!', 3);
  }
});

game.onMessage('takeAmulet', () => {
  if (stage !== 1) return;
  stage = 2;
  game.inventory.add({ name: 'Амулет' });
  game.sound.play('pickup');
  game.ui.showText('Амулет найден! Отнеси кузнецу.', 3);
});

game.onMessage('smithTalk', () => {
  if (stage === 2 && game.inventory.has('Амулет')) {
    stage = 3;
    game.inventory.remove('Амулет');
    smith.say('Отличный амулет! Вот награда, герой!', 4);
    game.ui.showText('Победа! Квест RPG-деревни выполнен!', 6);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  } else if (stage === 3) {
    smith.say('Доброго пути!', 3);
  } else {
    smith.say('Принеси мне амулет — поговори со старостой.', 4);
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>stage</code> — переменная-этап: 0 начало,
                        1 ищем амулет, 2 несём кузнецу, 3 квест выполнен;</li>
                    <li><code>spawnNpc(...)</code> со <code>speed: 0</code> —
                        житель стоит на месте;</li>
                    <li><code>elder.say(...)</code> — над NPC появляется
                        реплика;</li>
                    <li><code>game.inventory.add / has / remove</code> —
                        кладём амулет в сумку, проверяем и забираем;</li>
                    <li><code>game.onMessage('elderTalk', ...)</code> и
                        <code> game.onMessage('smithTalk', ...)</code> говорят
                        разное в зависимости от <code>stage</code> — это
                        и есть «живой» квест.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипты NPC и амулета</h3>
                <ScriptKind kind="object" on="тумбу старосты" />
                <Code>{`// === Скрипт старосты ===
game.self.onInteract(() => {
  game.broadcast('elderTalk');
}, { text: 'Поговорить со старостой', distance: 4 });`}</Code>
                <ScriptKind kind="object" on="тумбу кузнеца" />
                <Code>{`// === Скрипт кузнеца ===
game.self.onInteract(() => {
  game.broadcast('smithTalk');
}, { text: 'Поговорить с кузнецом', distance: 4 });`}</Code>
                <ScriptKind kind="object" on="фиолетовый амулет" />
                <Code>{`// === Скрипт амулета ===
game.self.onTouch(() => {
  game.broadcast('takeAmulet');
  game.self.delete();
});`}</Code>
                <Note>
                    Тумба-куб — это «кнопка разговора». Сам NPC создаётся
                    скриптом и стоит рядом с тумбой. Игрок жмёт
                    <kbd className="kbd">E</kbd> на тумбе — и NPC отвечает.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>поговори со старостой — он выдаст квест;</li>
                    <li>найди фиолетовый амулет за дальним домом, подними его;</li>
                    <li>отнеси амулет кузнецу, поговори с ним;</li>
                    <li>кузнец благодарит — «Победа».</li>
                </ul>

                <Try>
                    добавь третьего NPC с ещё одним предметом — цепочка
                    квестов станет длиннее. Сделай так, чтобы за квест
                    игрок получал монетки в счёт. Добавь жителям реплики
                    «просто так».
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 43 — «Гонка с препятствиями»
    // ════════════════════════════════════════════════════
    'obstacle-race': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Гоночная трасса на время. По дороге — синие плитки-бусты:
                    наступил, и игрок ускоряется. И красные шипы-ловушки:
                    задел — урон и замедление. Доберись до финиша как можно
                    быстрее, секундомер покажет твоё время.
                </p>
                <Shot src="lesson43-result.png"
                      caption="Гонка на время: синее ускоряет, красное мешает" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Скорость игрока</b> — как ускорять и замедлять
                        (<code>game.player.setSpeed</code>);</li>
                    <li><b>Временный эффект</b> — буст действует 3 секунды,
                        потом скорость возвращается;</li>
                    <li><b>Секундомер</b> — счёт времени через
                        <code> onTick</code> и <code>game.ui.timer</code>;</li>
                    <li><b>Два типа препятствий</b> — полезное и вредное.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Трасса</h3>
                <Step n="1">
                    Построй длинную дорожку из камня — 8 блоков в ширину,
                    около 70 в длину, с бортиками по бокам.
                </Step>
                <Step n="2">
                    Расставь синие кубы-бусты (4×0.3×2, материал «Неон»,
                    «Столкновение» выключено) поперёк дороги — 3 штуки.
                </Step>
                <Step n="3">
                    Расставь красные конусы-шипы (1.2×1.6×1.2) — 5 штук,
                    в шахматном порядке слева-справа.
                </Step>
                <Step n="4">
                    В конце поставь зелёный куб «Финиш».
                </Step>
                <Shot src="lesson43-scene.png"
                      caption="Трасса с бустами и шипами" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «ГОНКА С ПРЕПЯТСТВИЯМИ» — главный скрипт ===

let time = 0;
let won = false;

game.ui.timer = 0;
game.ui.showText('Гонка! Синее ускоряет, шипы мешают', 4);

game.onTick((dt) => {
  if (won) return;
  time = time + dt;
  game.ui.timer = time;
});

// Бусты, шипы и финиш сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('boost', () => {
  // ускоряем игрока на 3 секунды
  game.player.setSpeed(1.8);
  game.sound.play('pickup');
  game.ui.showText('УСКОРЕНИЕ!', 1);
  game.after(3, () => game.player.setSpeed(1));
});

game.onMessage('spike', () => {
  game.player.damage(15);
  game.player.setSpeed(0.5);   // шип замедляет
  game.sound.play('hit');
  game.after(1.5, () => game.player.setSpeed(1));
});

game.onMessage('finish', () => {
  if (won) return;
  won = true;
  const t = Math.round(time * 10) / 10;
  game.ui.showText('Финиш! Время: ' + t + ' сек', 6);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>onTick((dt) ={'>'} ...)</code> — <code>dt</code>
                        это секунды с прошлого кадра; складываем их в
                        <code> time</code> — получается секундомер;</li>
                    <li><code>game.onMessage('boost', ...)</code> —
                        <code> setSpeed(1.8)</code> делает игрока в 1.8 раза
                        быстрее, а <code>game.after(3, ...)</code> через
                        3 секунды возвращает обычную скорость
                        <code> setSpeed(1)</code>;</li>
                    <li><code>game.onMessage('spike', ...)</code> — отнимает
                        здоровье и наоборот замедляет до
                        <code> setSpeed(0.5)</code> на 1.5 секунды;</li>
                    <li><code>Math.round(time * 10) / 10</code> — округляем
                        время до десятых долей секунды.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипты буста, шипа и финиша</h3>
                <ScriptKind kind="object" on="каждый синий буст" />
                <Code>{`// === Скрипт буста ===
game.self.onTouch(() => {
  game.broadcast('boost');
});`}</Code>
                <ScriptKind kind="object" on="каждый красный шип" />
                <Code>{`// === Скрипт шипа-ловушки ===
game.self.onTouch(() => {
  game.broadcast('spike');
});`}</Code>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('finish');
});`}</Code>
                <Note>
                    <code>setSpeed</code> — множитель скорости. 1 — обычная,
                    1.8 — быстро, 0.5 — медленно. После эффекта всегда
                    возвращай <code>setSpeed(1)</code>, иначе игрок навсегда
                    останется быстрым или медленным.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>с началом игры идёт секундомер;</li>
                    <li>наехал на синюю плитку — ускорение на 3 секунды;</li>
                    <li>задел красный шип — урон и замедление;</li>
                    <li>домчал до финиша — игра покажет твоё время.</li>
                </ul>

                <Try>
                    добавь несколько кругов, как в гонке из урока 32. Сделай
                    бусты-«стрелки», которые ещё и подбрасывают. Поставь рекорд
                    времени и попробуй его побить.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 44 — «Tower Defense»
    // ════════════════════════════════════════════════════
    'tower-defense': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Враги идут по каменной дороге к твоей синей базе. По бокам
                    дороги — площадки: подойди и нажми <kbd className="kbd">E</kbd>,
                    чтобы построить башню. Башни сами стреляют по врагам рядом.
                    Уничтожь 14 врагов, не пропустив 8 — победа.
                </p>
                <Shot src="lesson44-result.png"
                      caption="Tower Defense: ставь башни, защищай базу" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Враги по маршруту</b> — NPC идут от старта к базе
                        (<code>moveTo</code>);</li>
                    <li><b>Постройка башен</b> — нажатие <kbd className="kbd">E</kbd>
                        создаёт объект (<code>game.scene.spawn</code>);</li>
                    <li><b>Авто-атака</b> — башни сами бьют врагов рядом
                        через <code>game.every</code>;</li>
                    <li><b>Счёт побед и прорывов</b> — два условия конца игры.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Дорога, площадки и база</h3>
                <Step n="1">
                    Построй большую травяную площадку. По центру выложи
                    каменную <b>дорогу</b> на 1 блок выше — по ней пойдут враги.
                </Step>
                <Step n="2">
                    По бокам дороги поставь 4 куба-площадки (3×2×3, серые) —
                    имена «Площадка_1»…«Площадка_4». Это места под башни.
                </Step>
                <Step n="3">
                    В конце дороги поставь большой синий куб «База».
                </Step>
                <Shot src="lesson44-scene.png"
                      caption="Дорога врагов, площадки под башни, синяя база" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <p>
                    Главный скрипт делает всё: спавнит врагов, заставляет башни
                    стрелять и проверяет, кто победил.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «TOWER DEFENSE» — главный скрипт ===

let leaked = 0;           // врагов прошло до базы
const MAX_LEAK = 8;
let killed = 0;
const GOAL = 14;          // победа — уничтожить 14 врагов
let over = false;

// список башен: {x, z}
const towers = [];
// список живых врагов: {npc, alive}
const enemies = [];

game.ui.score = 0;
game.ui.showText('Ставь башни (E)! Не пропусти врагов', 4);

// Площадки сообщают сюда о постройке башни через
// game.broadcast('addTower', { x, z }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('addTower', (d) => {
  towers.push({ x: d.x, z: d.z });
  game.sound.play('click');
  game.ui.showText('Башня построена!', 1.5);
});

// спавн врагов
let total = 0;
game.every(2.2, () => {
  if (over || total >= GOAL + MAX_LEAK) return;
  total = total + 1;
  const npc = game.scene.spawnNpc('character-b', {
    x: -0.5, y: 1, z: -3, name: 'Враг', hp: 50, speed: 2,
  });
  npc.moveTo(-0.5, 42);
  const rec = { npc: npc, alive: true };
  enemies.push(rec);
  npc.onDeath(() => {
    rec.alive = false;
    killed = killed + 1;
    game.ui.score = killed;
    if (killed >= GOAL && !over) {
      over = true;
      game.ui.showText('Победа! База защищена!', 5);
      game.sound.play('win');
    }
  });
});

// башни стреляют: каждые 0.8с бьём врага рядом с любой башней
game.every(0.8, () => {
  if (over) return;
  for (const t of towers) {
    for (const e of enemies) {
      if (!e.alive) continue;
      const p = e.npc.position;
      // позиция NPC появляется через кадр после spawn — пропускаем
      if (!p) continue;
      if (Math.hypot(p.x - t.x, p.z - t.z) < 7) {
        e.npc.damage(25);
        game.scene.spawnParticles('sparks', p, { duration: 0.3 });
        break;   // одна башня — один выстрел за тик
      }
    }
  }
});

// проверка прорыва к базе
game.every(0.5, () => {
  if (over) return;
  for (const e of enemies) {
    if (e.alive && e.npc.position && e.npc.position.z > 40) {
      e.alive = false;
      e.npc.remove();
      leaked = leaked + 1;
      game.sound.play('lose');
      game.ui.showText('Враг прорвался! (' + leaked + '/' + MAX_LEAK + ')', 2);
      if (leaked >= MAX_LEAK && !over) {
        over = true;
        game.ui.showText('База разрушена! Поражение.', 5);
      }
    }
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>towers</code> и <code>enemies</code> — два списка:
                        где стоят башни и какие враги живы;</li>
                    <li><code>game.onMessage('addTower', ...)</code> ловит
                        сообщение от площадки и кладёт координаты башни
                        в список <code>towers</code>;</li>
                    <li>первый <code>game.every(2.2, ...)</code> — каждые
                        2.2 секунды выпускает врага и зовёт
                        <code> npc.moveTo(...)</code> — враг идёт к базе;</li>
                    <li>второй <code>game.every(0.8, ...)</code> — каждая башня
                        ищет врага ближе 7 единиц и бьёт его;</li>
                    <li>третий <code>game.every(0.5, ...)</code> — проверяет,
                        не дошёл ли враг до базы (<code>position.z {'>'} 40</code>);</li>
                    <li>победа — <code>killed {'>='} GOAL</code>, поражение —
                        <code> leaked {'>='} MAX_LEAK</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт площадки под башню</h3>
                <ScriptKind kind="object" on="каждую площадку" />
                <Code>{`// === Скрипт площадки под башню ===
let built = false;
game.self.onInteract(() => {
  if (built) return;
  built = true;
  const pos = game.self.position;
  // ставим башню — жёлтый цилиндр поверх площадки
  game.scene.spawn('primitive:cylinder', {
    x: pos.x, y: pos.y + 2.5, z: pos.z,
    sx: 1.5, sy: 3, sz: 1.5,
    color: '#ffcc33',
  });
  game.broadcast('addTower', { x: pos.x, z: pos.z });
}, { text: 'Построить башню', distance: 4 });`}</Code>
                <p>
                    При нажатии <kbd className="kbd">E</kbd> скрипт создаёт
                    жёлтый цилиндр-башню над площадкой и шлёт сообщение
                    <code> game.broadcast('addTower', ...)</code> с координатами
                    башни — главный скрипт ловит его, кладёт башню в список,
                    и она начинает стрелять.
                </p>
                <Note>
                    Сама «стрельба» — это не пули, а проверка расстояния
                    в <code>game.every</code>. Так делают почти все простые
                    Tower Defense: башня просто бьёт ближайшего врага по таймеру.
                </Note>

                <h3 className="lessonH">Шаг 3.1. Башня — своя модель из редактора моделей</h3>
                <p>
                    Жёлтый цилиндр-башня — это просто заглушка. Можно поставить
                    вместо неё <b>свою воксельную модель</b>, которую ты сделал
                    в редакторе моделей проекта.
                </p>
                <ol>
                    <li>В редакторе проекта открой панель <b>«Мои модели»</b>
                        и создай башню в редакторе воксельных моделей.</li>
                    <li>Узнай её <b>id</b> — он показан рядом с названием модели
                        в панели «Мои модели» (например <code>3</code>).</li>
                    <li>В скрипте площадки замени блок
                        <code> game.scene.spawn('primitive:cylinder', ...)</code>
                        на такой:</li>
                </ol>
                <Code>{`// ставим башню — своя воксельная модель (id = 3)
game.scene.spawn('user:3', {
  x: pos.x, y: pos.y + 2, z: pos.z,
  rotationY: 0,
});`}</Code>
                <p>
                    У пользовательских моделей нет <code>sx/sy/sz</code> и
                    <code> color</code> — размер и цвет задаются в самом
                    редакторе модели. Из параметров только позиция и
                    <code> rotationY</code> (поворот по вертикальной оси).
                    Высоту <code>y</code> подбери под высоту своей модели,
                    чтобы она ровно встала на площадку.
                </p>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>враги идут по дороге к базе;</li>
                    <li>подойди к площадке, нажми <kbd className="kbd">E</kbd>
                        — встаёт башня;</li>
                    <li>башня сама стреляет искрами по врагам рядом;</li>
                    <li>уничтожил 14 — победа, пропустил 8 — поражение.</li>
                </ul>

                <Try>
                    добавь больше площадок под башни. Сделай врагам больше
                    здоровья на поздних волнах. Покрась башни разными цветами
                    — «обычная» и «сильная».
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 45 — «Стрелялка-арена»
    // ════════════════════════════════════════════════════
    'arena-shooter': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Закрытая арена. Со всех сторон к игроку сбегаются враги-NPC.
                    Кликай по врагам — они гибнут. Но и враги бьют тебя, если
                    подошли вплотную: у игрока есть здоровье. Перебей 15 врагов,
                    не потеряв всё HP.
                </p>
                <Shot src="lesson45-result.png"
                      caption="Стрелялка-арена: отбивайся от врагов кликами" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>«Стрельба» кликом</b> — клик уничтожает врага
                        рядом по проверке расстояния;</li>
                    <li><b>Урон игроку</b> — враг бьёт по таймеру
                        (<code>game.every</code> + <code>damage</code>);</li>
                    <li><b>Отслеживание здоровья</b> — конец игры при
                        <code> onHpChange</code>;</li>
                    <li><b>Отмена таймера</b> — <code>game.cancel</code>,
                        когда враг умер.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Арена со стенами</h3>
                <Step n="1">
                    Построй квадратную площадку из камня (примерно 26×26 блоков).
                </Step>
                <Step n="2">
                    По всему краю поставь стены в 3 блока высотой — чтобы враги
                    и игрок не убегали с арены.
                </Step>
                <Step n="3">
                    Поставь точку спавна игрока в центр.
                </Step>
                <Note>
                    Врагов снова создаёт скрипт — расставлять их заранее
                    не нужно. Поэтому у этой игры только скриншот результата.
                </Note>

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===

let score = 0;
const GOAL = 15;
let over = false;

game.ui.score = 0;
game.ui.showText('Перебей 15 врагов! Кликай по ним', 3);

// проигрыш, когда HP игрока кончилось
game.onHpChange((e) => {
  if (!over && e.hp <= 0) {
    over = true;
    game.ui.showText('Поражение! Тебя одолели враги.', 5);
  }
});

// спавним нового врага каждые 1.8 сек
game.every(1.8, () => {
  if (over || score >= GOAL) return;
  const angle = game.random(0, 6.28);
  const ex = Math.cos(angle) * 11;
  const ez = Math.sin(angle) * 11;
  const enemy = game.scene.spawnNpc('character-b', {
    x: ex, y: 1, z: ez, name: 'Враг', hp: 30, speed: 2.2,   // y=1 — на полу
  });
  enemy.follow('player');

  let dead = false;
  // враг бьёт игрока, если подошёл вплотную
  const dmgTimer = game.every(0.7, () => {
    if (dead || over) { game.cancel(dmgTimer); return; }
    const p = game.player.position;
    const e = enemy.position;
    if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 1.8) {
      game.player.damage(10);
      game.sound.play('hit');
    }
  });

  // клик по врагу — убить
  game.onClick(() => {
    if (dead || over) return;
    const p = game.player.position;
    const e = enemy.position;
    if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 6) {
      dead = true;
      game.cancel(dmgTimer);
      enemy.remove();
      game.scene.spawnParticles('explosion', e, { count: 1 });
      game.sound.play('hit');
      score = score + 1;
      game.ui.score = score;
      if (score >= GOAL && !over) {
        over = true;
        game.ui.showText('Победа! Арена зачищена!', 5);
        game.sound.play('win');
        game.scene.spawnParticles('confetti',
          { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
      }
    }
  });
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.onHpChange((e) ={'>'} ...)</code> —
                        срабатывает, когда здоровье игрока меняется. Если
                        <code> e.hp {'<='} 0</code> — поражение;</li>
                    <li><code>game.every(1.8, ...)</code> — каждые 1.8 секунды
                        появляется новый враг по краю арены и
                        <code> follow('player')</code> — идёт к игроку;</li>
                    <li><code>dmgTimer</code> — отдельный таймер: пока враг жив,
                        каждые 0.7 секунды проверяет, рядом ли он, и бьёт;</li>
                    <li><code>game.cancel(dmgTimer)</code> — когда враг умер,
                        его таймер урона выключается, чтобы не работал зря;</li>
                    <li>клик ближе 6 единиц к врагу — он уничтожен, +1 к счёту.</li>
                </ul>
                <Note>
                    Каждый враг — это маленький «механизм»: свой флажок
                    <code> dead</code>, свой таймер <code>dmgTimer</code>,
                    свой обработчик клика. Когда враг гибнет, его механизм
                    аккуратно выключается через <code>game.cancel</code>.
                </Note>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>враги сбегаются к тебе со всех сторон;</li>
                    <li>кликай по врагу рядом — он взрывается, +1 к счёту;</li>
                    <li>враг вплотную — отнимает здоровье;</li>
                    <li>перебил 15 — «Победа», потерял всё HP — «Поражение».</li>
                </ul>

                <Try>
                    добавь «аптечки» — сферы, которые лечат при касании.
                    Сделай врагов сильнее (больше <code>hp</code> и
                    <code> speed</code>). Увеличь цель до 25 врагов.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 46 — «Кликер»
    // ════════════════════════════════════════════════════
    'clicker': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    В центре площадки — большой жёлтый куб. Кликай по нему,
                    и копятся очки. По бокам — две кнопки-улучшения: красная
                    добавляет силу клика, синяя включает авто-доход (очки
                    капают сами). Накопи 200 очков — победа.
                </p>
                <Shot src="lesson46-result.png"
                      caption="Кликер: жми куб, копи очки, покупай улучшения" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Клик по объекту</b> — <code>game.self.onClick</code>;</li>
                    <li><b>Экономика улучшений</b> — тратим очки, чтобы
                        зарабатывать больше;</li>
                    <li><b>Авто-доход</b> — очки сами растут через
                        <code> game.every</code>;</li>
                    <li><b>Несколько мест проверки победы</b> — общая функция
                        <code> checkWin</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Куб и кнопки</h3>
                <Step n="1">
                    Построй небольшую площадку из блоков.
                </Step>
                <Step n="2">
                    В центр поставь большой жёлтый куб «Кликер» (3×3×3,
                    материал «Неон»).
                </Step>
                <Step n="3">
                    Слева поставь красный куб «УлучшениеСила» (2×2×2), справа
                    — синий куб «УлучшениеАвто».
                </Step>
                <Shot src="lesson46-scene.png"
                      caption="Куб-кликер в центре, две кнопки-улучшения по бокам" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «КЛИКЕР» — главный скрипт ===

let points = 0;           // очки
let perClick = 1;         // очков за клик
let autoIncome = 0;       // очков в секунду автоматически
const GOAL = 200;
let won = false;

game.ui.score = 0;
game.ui.showText('Кликай по жёлтому кубу! Цель: 200 очков', 4);

// авто-доход: каждую секунду прибавляем autoIncome
game.every(1, () => {
  if (won) return;
  if (autoIncome > 0) {
    points = points + autoIncome;
    game.ui.score = points;
    checkWin();
  }
});

function checkWin() {
  if (!won && points >= GOAL) {
    won = true;
    game.ui.showText('Победа! Накоплено 200 очков!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  }
}

// Куб-кликер и кнопки апгрейда сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('click', () => {
  if (won) return;
  points = points + perClick;
  game.ui.score = points;
  game.sound.play('click');
  checkWin();
});

game.onMessage('buyPower', () => {
  if (points < 20) {
    game.ui.showText('Нужно 20 очков для улучшения!', 1.5);
    return;
  }
  points = points - 20;
  perClick = perClick + 2;
  game.ui.score = points;
  game.sound.play('pickup');
  game.ui.showText('Сила клика: +' + perClick + ' за клик', 2);
});

game.onMessage('buyAuto', () => {
  if (points < 40) {
    game.ui.showText('Нужно 40 очков для авто-дохода!', 1.5);
    return;
  }
  points = points - 40;
  autoIncome = autoIncome + 3;
  game.ui.score = points;
  game.sound.play('pickup');
  game.ui.showText('Авто-доход: +' + autoIncome + ' в секунду', 2);
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>points</code> — очки, <code>perClick</code> —
                        сколько даёт один клик, <code>autoIncome</code> —
                        сколько капает само в секунду;</li>
                    <li><code>game.every(1, ...)</code> — каждую секунду
                        прибавляет <code>autoIncome</code> к очкам;</li>
                    <li><code>checkWin()</code> — общая функция: проверка победы
                        вызывается и из клика, и из авто-дохода;</li>
                    <li><code>game.onMessage('click', ...)</code> ловит клик
                        по кубу — +<code>perClick</code> очков;</li>
                    <li><code>game.onMessage('buyPower', ...)</code> — за 20 очков
                        добавляет +2 к силе клика;</li>
                    <li><code>game.onMessage('buyAuto', ...)</code> — за 40 очков
                        добавляет +3 к авто-доходу.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипты куба и кнопок</h3>
                <ScriptKind kind="object" on="жёлтый куб-кликер" />
                <Code>{`// === Скрипт куба-кликера ===
game.self.onClick(() => {
  game.broadcast('click');
  // куб слегка вспыхивает
  game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
});`}</Code>
                <ScriptKind kind="object" on="красную кнопку" />
                <Code>{`// === Скрипт улучшения «сила клика» (20 очков) ===
game.self.onInteract(() => {
  game.broadcast('buyPower');
}, { text: 'Купить +силу клика (20)', distance: 3 });`}</Code>
                <ScriptKind kind="object" on="синюю кнопку" />
                <Code>{`// === Скрипт улучшения «авто-доход» (40 очков) ===
game.self.onInteract(() => {
  game.broadcast('buyAuto');
}, { text: 'Купить авто-доход (40)', distance: 3 });`}</Code>
                <Note>
                    Главная идея кликера: сначала кликаешь руками, потом
                    покупаешь улучшения — и игра «играет сама». Это экономика:
                    тратишь очки, чтобы зарабатывать быстрее.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>кликай по жёлтому кубу — очки растут;</li>
                    <li>накопил 20 — купи силу клика, клики станут «жирнее»;</li>
                    <li>накопил 40 — купи авто-доход, очки капают сами;</li>
                    <li>дошёл до 200 — «Победа».</li>
                </ul>

                <Try>
                    добавь третье улучшение — например, ещё дороже и мощнее.
                    Сделай цель больше (1000 очков). Покажи <code>perClick</code>
                    и <code>autoIncome</code> отдельной надписью через
                    <code> game.ui.set</code>.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 47 — «Квест-побег»
    // ════════════════════════════════════════════════════
    'escape-quest': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Игрок заперт в комнате. Чтобы выбраться, нужно найти
                    и нажать 3 красные кнопки. Две на виду, а третья спрятана
                    за ящиком — её надо обойти. Нажал все три — дверь поднимается,
                    и можно выйти к финишу.
                </p>
                <Shot src="lesson47-result.png"
                      caption="Квест-побег: найди 3 кнопки и открой дверь" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Закрытая комната</b> — стены с проёмом под дверь;</li>
                    <li><b>Состояние головоломки</b> — счётчик нажатых кнопок;</li>
                    <li><b>Открытие двери</b> — твин по достижении условия;</li>
                    <li><b>Спрятанный объект</b> — кнопка за ящиком, которую
                        надо найти.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Комната, кнопки, дверь</h3>
                <Step n="1">
                    Построй пол и стены закрытой комнаты (примерно 20×20).
                    В одной стене оставь <b>проём</b> шириной 3 блока — под дверь.
                </Step>
                <Step n="2">
                    Закрой проём примитивом-кубом «Дверь» (1×4×3, коричневый).
                </Step>
                <Step n="3">
                    Поставь 3 цилиндра-кнопки красного цвета по углам комнаты.
                    Одну спрячь за большим кубом-ящиком.
                </Step>
                <Step n="4">
                    За дверью снаружи поставь зелёный куб «Финиш».
                </Step>
                <Shot src="lesson47-scene.png"
                      caption="Запертая комната с тремя кнопками и ящиком" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===

let pressed = 0;          // сколько кнопок нажато
const TOTAL = 3;
let escaped = false;

game.ui.showText('Найди и нажми 3 кнопки, чтобы выйти!', 4);

// Кнопки и финиш сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('pressButton', () => {
  pressed = pressed + 1;
  game.sound.play('click');
  game.ui.showText('Кнопка ' + pressed + ' из ' + TOTAL, 1.5);
  if (pressed >= TOTAL) {
    // все кнопки — открываем дверь
    const door = game.scene.findOne('Дверь');
    game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
    game.ui.showText('Все кнопки нажаты! Дверь открыта!', 3);
    game.sound.play('win');
  }
});

game.onMessage('escape', () => {
  if (escaped) return;
  escaped = true;
  game.ui.showText('Победа! Ты сбежал из комнаты!', 5);
  game.sound.play('win');
  const p = game.player.position;
  game.scene.spawnParticles('confetti',
    { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>pressed</code> — счётчик нажатых кнопок,
                        <code> TOTAL</code> — сколько их всего;</li>
                    <li><code>game.onMessage('pressButton', ...)</code> ловит
                        сообщение, которое кнопка шлёт при нажатии;</li>
                    <li><code>if (pressed {'>='} TOTAL)</code> — когда нажаты
                        все три, находим дверь по имени и поднимаем её твином
                        вверх;</li>
                    <li><code>game.onMessage('escape', ...)</code> — победа,
                        когда игрок прошёл через открытую дверь к финишу.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипты кнопки и финиша</h3>
                <ScriptKind kind="object" on="каждую красную кнопку" />
                <Code>{`// === Скрипт кнопки 1 ===
let used = false;
game.self.onInteract(() => {
  if (used) return;
  used = true;
  game.scene.setColor(game.self.ref, '#22dd55');   // нажата — зелёная
  game.broadcast('pressButton');
}, { text: 'Нажать кнопку', distance: 3 });`}</Code>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('escape');
});`}</Code>
                <p>
                    Кнопка при нажатии становится зелёной (видно, что нажата),
                    шлёт <code>game.broadcast('pressButton')</code> и больше
                    не срабатывает благодаря флажку <code>used</code>.
                </p>
                <Note>
                    Скрипт у всех трёх кнопок <b>одинаковый</b> — каждая просто
                    шлёт сообщение <code>'pressButton'</code>. Главный скрипт
                    сам считает, сколько кнопок нажато.
                </Note>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>нажми <kbd className="kbd">E</kbd> на видимых кнопках —
                        они становятся зелёными;</li>
                    <li>найди третью кнопку за ящиком;</li>
                    <li>после третьей дверь поднимается вверх;</li>
                    <li>выйди наружу на зелёный финиш — «Победа».</li>
                </ul>

                <Try>
                    спрячь все кнопки похитрее — например, в разных комнатах.
                    Добавь четвёртую кнопку. Сделай «ложную» кнопку, которая
                    сбрасывает счётчик.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 48 — «Мультиплеер: Салки»
    // ════════════════════════════════════════════════════
    'mp-tag': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Догонялки для нескольких игроков. Один игрок — водящий,
                    он догоняет остальных, а они убегают и прячутся за укрытиями.
                    Это <b>мультиплеерная</b> игра: чтобы играть с друзьями,
                    её нужно опубликовать с галочкой «Мультиплеер».
                </p>
                <Shot src="lesson48-result.png"
                      caption="Салки: водящий догоняет, остальные прячутся" />

                <Note>
                    <b>Важно про мультиплеер.</b> Чтобы играть с друзьями,
                    опубликуй игру и поставь галочку <b>«Мультиплеер»</b> —
                    тогда несколько игроков смогут зайти в одну комнату.
                    Если открыть игру в одиночку, она работает как
                    <b> демо</b>: ты увидишь правила, но догонять будет некого.
                </Note>

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Игроки комнаты</b> — <code>game.players</code>:
                        список всех, кто играет;</li>
                    <li><b>Общее состояние</b> — <code>game.room</code>: данные,
                        которые видят все игроки;</li>
                    <li><b>Вход и выход</b> — события
                        <code> onPlayerJoin</code> / <code>onPlayerLeave</code>;</li>
                    <li><b>Реакция на изменение</b> —
                        <code> game.room.onChange</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поле с укрытиями</h3>
                <Step n="1">
                    Построй большую травяную площадку (примерно 28×28 блоков).
                </Step>
                <Step n="2">
                    Расставь по полю несколько кубов-укрытий (3.5×4×1.5,
                    серые) — за ними убегающие смогут прятаться.
                </Step>
                <Shot src="lesson48-scene.png"
                      caption="Поле для салок с укрытиями" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
//
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
// игроков. В одиночку игра показывает только правила.

game.ui.showText('Салки! Опубликуй игру для игры с друзьями', 4);

// общий счётчик игроков комнаты — виден всем
function refresh() {
  const n = game.players.count();
  game.ui.set('info', 'Игроков в комнате: ' + n, { x: 50, y: 8 });

  // водящий выбирается так: первый зашедший игрок
  const all = game.players.all();
  if (all.length > 0) {
    game.room.set('tagger', all[0].sessionId);
  }
}

// при входе/выходе игрока — обновляем
game.onPlayerJoin((p) => {
  game.ui.showText(p.name + ' присоединился к салкам!', 2);
  refresh();
});
game.onPlayerLeave(() => refresh());

refresh();

// если ты водящий — догоняй других; если убегаешь — прячься.
game.room.onChange('tagger', (taggerId) => {
  const me = game.players.me();
  if (me && me.sessionId === taggerId) {
    game.ui.showText('Ты ВОДЯЩИЙ! Догоняй и осаль других!', 3);
  } else {
    game.ui.showText('Убегай от водящего!', 3);
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.players.count()</code> — сколько игроков
                        сейчас в комнате;</li>
                    <li><code>game.players.all()</code> — список всех игроков;
                        <code> me()</code> — это «я»;</li>
                    <li><code>game.room.set('tagger', ...)</code> — записываем
                        в общее состояние комнаты, кто водящий. Это видят
                        все игроки;</li>
                    <li><code>game.onPlayerJoin / onPlayerLeave</code> —
                        события входа и выхода игрока;</li>
                    <li><code>game.room.onChange('tagger', ...)</code> —
                        срабатывает у всех, когда водящий поменялся: каждый
                        узнаёт, он водящий или убегает.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Проверка</h3>
                <ul>
                    <li>в одиночку игра покажет правила (демо-режим);</li>
                    <li>опубликуй игру с галочкой «Мультиплеер»;</li>
                    <li>пусть друзья зайдут по ссылке в твою комнату;</li>
                    <li>первый зашедший — водящий, остальные убегают.</li>
                </ul>
                <Note>
                    Разница простой и мультиплеерной игры: обычные данные
                    (<code>let score</code>) есть только у тебя, а
                    <code> game.room</code> — общие для всей комнаты. Поэтому
                    «кто водящий» хранят именно в <code>game.room</code>.
                </Note>

                <Try>
                    добавь надпись с именем водящего через
                    <code> game.ui.set</code>. Сделай так, чтобы при касании
                    водящего с убегающим они менялись ролями. Добавь таймер
                    раунда.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 49 — «Мультиплеер: Гонка»
    // ════════════════════════════════════════════════════
    'mp-race': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Гонка нескольких игроков по одной трассе. Кто первым
                    добежит до финиша — его имя записывается в общий счёт
                    комнаты, и все игроки видят победителя. Это снова
                    <b> мультиплеерная</b> игра.
                </p>
                <Shot src="lesson49-result.png"
                      caption="Мультиплеер-гонка: первый на финише — победитель" />

                <Note>
                    <b>Важно про мультиплеер.</b> Чтобы соревноваться
                    с друзьями, опубликуй игру с галочкой
                    <b> «Мультиплеер»</b> — тогда несколько игроков попадут
                    в одну комнату и побегут вместе. В одиночку игра
                    открывается как <b>демо</b>.
                </Note>

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Общий счёт комнаты</b> — <code>game.room</code>
                        хранит имя победителя для всех;</li>
                    <li><b>Узнать себя</b> — <code>game.players.me()</code>
                        и имя игрока;</li>
                    <li><b>Один победитель</b> — как не дать записать второго;</li>
                    <li><b>Обновление у всех</b> — <code>game.room.onChange</code>.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Трасса</h3>
                <Step n="1">
                    Построй длинную прямую дорожку из камня (около 70 блоков)
                    с бортиками по бокам.
                </Step>
                <Step n="2">
                    В конце поставь широкий зелёный куб «Финиш».
                </Step>
                <Step n="3">
                    Точку спавна поставь в начале трассы.
                </Step>
                <Shot src="lesson49-scene.png"
                      caption="Прямая гоночная трасса с финишем" />

                <h3 className="lessonH">Шаг 2. Главный скрипт</h3>
                <ScriptKind kind="global" />
                <Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
//
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
// игру с галочкой «Мультиплеер».

game.ui.showText('Гонка! Беги к финишу первым', 3);

// показываем, сколько игроков и кто уже финишировал
function refresh() {
  const n = game.players.count();
  const winner = game.room.get('winner');
  let txt = 'Игроков: ' + n;
  if (winner) txt = txt + '  |  Победил: ' + winner;
  game.ui.set('info', txt, { x: 50, y: 8 });
}
refresh();

game.onPlayerJoin(() => refresh());
game.onPlayerLeave(() => refresh());

// когда кто-то финишировал — обновляем у всех
game.room.onChange('winner', () => refresh());

// Финиш сообщает сюда через game.broadcast('finish').
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('finish', () => {
  // если победитель ещё не определён — записываем себя
  if (!game.room.get('winner')) {
    const me = game.players.me();
    const myName = me ? me.name : 'Игрок';
    game.room.set('winner', myName);
    game.ui.showText('Ты пришёл первым! Победа!', 5);
    game.sound.play('win');
    const p = game.player.position;
    game.scene.spawnParticles('confetti',
      { x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
  } else {
    game.ui.showText('Финиш! Но кто-то был быстрее.', 4);
  }
});`}</Code>
                <p>Разберём:</p>
                <ul>
                    <li><code>game.room.get('winner')</code> — читаем общую
                        переменную комнаты: записан ли уже победитель;</li>
                    <li><code>refresh()</code> — рисует надпись со счётом:
                        число игроков и имя победителя;</li>
                    <li><code>game.onMessage('finish', ...)</code> — если
                        победителя ещё нет, игрок записывает <b>своё имя</b>
                        через <code>game.room.set('winner', myName)</code>;</li>
                    <li><code>if (!game.room.get('winner'))</code> — защита:
                        только <b>первый</b> добежавший станет победителем,
                        остальные увидят «кто-то был быстрее»;</li>
                    <li><code>game.room.onChange('winner', ...)</code> — как
                        только победитель записан, надпись обновляется
                        <b> у всех игроков сразу</b>.</li>
                </ul>

                <h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
                <ScriptKind kind="object" on="зелёный финиш" />
                <Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
  game.broadcast('finish');
});`}</Code>
                <p>
                    Когда любой игрок касается финиша, скрипт шлёт сообщение
                    <code> game.broadcast('finish')</code> — а главный скрипт
                    ловит его и уже решает, первый этот игрок или нет.
                </p>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <ul>
                    <li>в одиночку — демо: добежишь и станешь победителем сам;</li>
                    <li>опубликуй с галочкой «Мультиплеер»;</li>
                    <li>пусть друзья зайдут в комнату и побегут с тобой;</li>
                    <li>первый на финише — его имя в надписи у всех.</li>
                </ul>
                <Note>
                    Главная идея: <code>game.room</code> — это «общая доска»
                    комнаты. Один игрок пишет на неё имя победителя, и все
                    остальные тут же это видят.
                </Note>

                <Try>
                    добавь секундомер и записывай в комнату ещё и время
                    победителя. Сделай несколько кругов. Добавь места для
                    2-3 места, не только для первого.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // ИГРА 50 — «Своя игра»
    // ════════════════════════════════════════════════════
    'make-your-own': {
        body: (
            <>
                <h3 className="lessonH">Что это за урок</h3>
                <p>
                    Это последний урок — и он особенный. Здесь нет готовой
                    игры. Тебе даётся <b>пустая песочница</b>: ровная зелёная
                    площадка и один скрипт-приветствие. Твоя задача — придумать
                    и собрать <b>свою собственную игру</b> с нуля. Всё, что
                    нужно, ты уже знаешь из уроков 1-49.
                </p>
                <Shot src="lesson50-result.png"
                      caption="Пустая песочница — здесь рождается твоя игра" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Придумывать игру</b> — выбирать жанр и идею;</li>
                    <li><b>Планировать сцену</b> — что и где построить;</li>
                    <li><b>Ставить цель</b> — что игрок должен сделать,
                        чтобы выиграть;</li>
                    <li><b>Соединять механики</b> — брать кусочки из разных
                        уроков и складывать в свою игру.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Выбери жанр</h3>
                <p>
                    Сначала реши, <b>какая</b> у тебя игра. Жанр — это «вид»
                    игры. Вот какие ты уже умеешь делать:
                </p>
                <ul>
                    <li><b>Паркур</b> — прыжки по платформам (уроки 2, 10, 41);</li>
                    <li><b>Гонка на время</b> — добежать быстрее (уроки 8, 32, 43);</li>
                    <li><b>Сбор предметов</b> — собрать всё на уровне (уроки 1, 6, 34);</li>
                    <li><b>Стрелялка</b> — отбиваться от врагов (уроки 15, 40, 45);</li>
                    <li><b>Квест</b> — головоломки и задания (уроки 12, 30, 42, 47);</li>
                    <li><b>Tower Defense</b> — защита базы (уроки 31, 44);</li>
                    <li><b>Мультиплеер</b> — игра с друзьями (уроки 48, 49).</li>
                </ul>
                <Note>
                    Не бойся смешивать! Лучшие игры — это сочетание: паркур
                    с монетками и врагом, квест с гонкой. Возьми один главный
                    жанр и добавь к нему пару механик из других.
                </Note>

                <h3 className="lessonH">Шаг 2. Спланируй сцену</h3>
                <p>
                    Прежде чем строить — представь свою игру. Ответь себе
                    на вопросы:
                </p>
                <ul>
                    <li><b>Где начинает игрок?</b> — поставь точку спавна;</li>
                    <li><b>Где он будет ходить?</b> — построй пол, платформы,
                        дорогу из блоков;</li>
                    <li><b>Что мешает?</b> — шипы, ямы, враги, стены;</li>
                    <li><b>Что помогает?</b> — монетки, бусты, чекпоинты;</li>
                    <li><b>Где конец игры?</b> — финиш, сокровище или
                        нужный счёт.</li>
                </ul>
                <p>
                    Можно даже нарисовать план на бумаге — это помогает
                    не запутаться.
                </p>

                <h3 className="lessonH">Шаг 3. Поставь цель</h3>
                <p>
                    У каждой игры должна быть <b>понятная цель</b> — то, ради
                    чего игрок играет. Без цели игра скучная. Цель может быть
                    такой:
                </p>
                <ul>
                    <li>добраться до финиша;</li>
                    <li>собрать все предметы;</li>
                    <li>набрать нужное число очков;</li>
                    <li>продержаться какое-то время;</li>
                    <li>выполнить квест.</li>
                </ul>
                <p>
                    И обязательно покажи игроку, когда он <b>победил</b> —
                    надписью <code>game.ui.showText('Победа!', 5)</code>,
                    звуком <code>game.sound.play('win')</code> и конфетти.
                </p>

                <h3 className="lessonH">Шаг 4. Напиши скрипты</h3>
                <p>
                    Сцена сама по себе не «живая» — её оживляют скрипты.
                    Начинай с <b>главного скрипта</b>: в нём заводи переменные
                    (счёт, флажок победы) и <b>лови сообщения</b> через
                    <code> game.onMessage('имя', fn)</code>. На объекты вешай
                    небольшие скрипты — они шлют сообщения главному через
                    <code> game.broadcast('имя')</code>. Так главный скрипт
                    узнаёт, что монетку собрали или кнопку нажали. Ты делал
                    так в каждом уроке.
                </p>
                <Note>
                    Каждый скрипт работает в своей «песочнице» — переменные
                    одного скрипта не видны другому. Поэтому скрипты общаются
                    сообщениями: один шлёт <code>game.broadcast('имя')</code>,
                    другой ловит <code>game.onMessage('имя', fn)</code>. Можно
                    передать данные: <code>game.broadcast('имя', {'{'} ... {'}'})</code>.
                </Note>
                <p>Базовый набор инструментов, который ты знаешь:</p>
                <ul>
                    <li><code>game.self.onTouch</code> — реакция на касание;</li>
                    <li><code>game.self.onInteract</code> — реакция на
                        <kbd className="kbd">E</kbd>;</li>
                    <li><code>game.self.onClick</code> — реакция на клик;</li>
                    <li><code>game.broadcast</code> и <code>game.onMessage</code>
                        — связь между скриптами;</li>
                    <li><code>game.onTick</code> — каждый кадр;</li>
                    <li><code>game.after</code> и <code>game.every</code> —
                        таймеры;</li>
                    <li><code>game.tween</code> — плавное движение;</li>
                    <li><code>game.scene.spawnNpc</code> — враги и NPC;</li>
                    <li><code>game.ui.score</code> и
                        <code> game.ui.showText</code> — счёт и подсказки.</li>
                </ul>

                <h3 className="lessonH">Шаг 5. Проверяй и улучшай</h3>
                <p>
                    Не строй сразу всё. Делай по чуть-чуть и почаще нажимай
                    <kbd className="kbd">Запустить</kbd>: построил пол —
                    проверь, добавил врага — проверь. Так легче найти ошибку.
                    Если что-то не работает — открой <b>Консоль</b>, там видны
                    ошибки скриптов.
                </p>
                <Note>
                    Не существует «неправильной» игры. Если в неё весело
                    играть — она удалась. Начни с простого: маленький уровень,
                    одна цель. А потом добавляй новое: врага, монетки, второй
                    этаж. Большие игры всегда начинались с маленьких.
                </Note>

                <h3 className="lessonH">Поздравляем!</h3>
                <p>
                    Ты прошёл все 50 уроков и теперь умеешь строить сцены,
                    писать скрипты, оживлять врагов, делать квесты, гонки
                    и даже мультиплеер. Это настоящие навыки создателя игр.
                    Впереди — только твоя фантазия. Придумывай, строй,
                    показывай друзьям. Удачи, и пусть твои игры будут
                    лучшими!
                </p>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // РАЗБОР ИГР · Двор с табличкой (оригинал id=1991)
    // ════════════════════════════════════════════════════
    'guide-dvor': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Маленький уютный двор: зелёный газон, деревянный забор,
                    деревья и большая <b>3D-табличка</b> в центре. По табличке
                    можно нажать мышкой прямо в игре — и что-то произойдёт.
                    А ещё двор учит главному: как крутить камеру вокруг героя,
                    как в настоящем Roblox.
                </p>
                <Shot src="guide-dvor-scene.png"
                      caption="Двор в редакторе: газон в заборе, табличка и плитки" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Камера и мышь</b> — зажми <b>правую кнопку мыши</b> и
                        веди, чтобы осмотреться вокруг героя;</li>
                    <li><b>Зум</b> — колесо мыши приближает и отдаляет. Совсем
                        близко — игра сама переходит в вид «от первого лица»;</li>
                    <li><b>Shift-Lock</b> на клавише <kbd className="kbd">L</kbd> —
                        герой всегда смотрит туда же, куда камера;</li>
                    <li><b>Клик по 3D-табличке</b> — как сделать кнопку прямо
                        в игровом мире (<code>game.billboard.onClick</code>).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Двор</h3>
                <Step n="1">
                    Инструментом <kbd className="kbd">Блок</kbd> построй площадку
                    из травы примерно 10×10. Это газон.
                </Step>
                <Step n="2">
                    По краю поставь забор из блоков-брёвен в 2 блока высотой,
                    оставив спереди проход.
                </Step>
                <Step n="3">
                    Из палитры моделей добавь пару деревьев — для красоты.
                    На вкладке <b>Игра</b> поставь <b>точку спавна</b> в центре.
                </Step>

                <h3 className="lessonH">Шаг 2. Табличка</h3>
                <p>
                    Табличка — это особый примитив <b>«3D-табличка»</b>
                    (биллборд). У неё есть кнопка, на которую можно нажимать.
                </p>
                <Step n="1">
                    Выбери <kbd className="kbd">Примитив</kbd> →
                    категория <b>Геймплей</b> → <b>3D-табличка</b>. Поставь её
                    в центр двора.
                </Step>
                <Step n="2">
                    Выдели табличку. В инспекторе справа нажми
                    <b> «Редактировать табличку»</b> — откроется окно, где можно
                    задать текст, иконку, цвет и кнопку.
                </Step>
                <Step n="3">
                    Запомни <b>номер таблички</b> — кликни по ней в Иерархии,
                    он выглядит как <code>primitive:41</code> (число у тебя может
                    быть другое).
                </Step>

                <h3 className="lessonH">Шаг 3. Скрипт клика</h3>
                <p>
                    Теперь сделаем, чтобы по нажатию на табличку менялся цвет неба.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ДВОР С ТАБЛИЧКОЙ — главный скрипт ===

// Подписываемся на клик по кнопке таблички.
// 'primitive:41' — номер твоей таблички, 'buy' — её кнопка.
game.billboard.onClick('primitive:41', 'buy', () => {
  game.environment.setSkyColor('#88c0ff');   // небо стало голубым
  game.log('По табличке нажали!');
});`}</Code>
                <p>
                    <code>game.billboard.onClick(номер, кнопка, функция)</code> —
                    «когда нажмут на эту кнопку, выполни функцию». Внутри мы
                    меняем цвет неба командой
                    <code> game.environment.setSkyColor</code>.
                </p>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <Shot src="guide-dvor-play.png"
                      caption="Двор в игре: герой от третьего лица, камера крутится мышкой" />
                <Step n="1">
                    Нажми <kbd className="kbd">Играть</kbd>. Походи по двору
                    на <kbd className="kbd">W</kbd><kbd className="kbd">A</kbd><kbd className="kbd">S</kbd><kbd className="kbd">D</kbd>.
                </Step>
                <Step n="2">
                    Зажми <b>правую кнопку мыши</b> и веди — камера крутится
                    вокруг героя. Покрути колесо — приближается и отдаляется.
                </Step>
                <Step n="3">
                    Наведи курсор на кнопку таблички и кликни — небо поменяет цвет.
                </Step>
                <Note>
                    Камера и мышь — это фундамент почти любой игры. Разберёшься
                    здесь — дальше будет намного легче.
                </Note>
                <Try>
                    поставь рядом ещё две таблички с разными цветами неба и
                    сделай переключатель «утро — день — ночь» из трёх кнопок.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // РАЗБОР ИГР · Витрина GUI (оригинал id=1995)
    // ════════════════════════════════════════════════════
    'guide-vitrina': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Витрина магазина, как в играх-кликерах. 3D-мира почти нет —
                    весь экран это <b>интерфейс</b>: счётчик монет и яркие кнопки.
                    И все кнопки <b>живые</b>: пульсируют, крутятся, увеличиваются
                    при наведении и «вдавливаются» при нажатии.
                </p>
                <Shot src="guide-vitrina-scene.png"
                      caption="Витрина: счётчик монет, кнопки магазина, радужная «X2 ДЕНЕГ»" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>GUI-кнопки</b> с градиентом, обводкой текста и
                        скруглением;</li>
                    <li><b>Анимация-пресет</b> — кнопка пульсирует сама по себе
                        (свойство «Анимация: pulse»);</li>
                    <li><b>Твин</b> — плавное изменение: кнопка плавно крутится
                        с 0° до 360° (<code>game.gui.tween</code>);</li>
                    <li><b>Связь кнопок и счётчика</b> через сообщения
                        (<code>game.broadcast</code> и <code>game.onMessage</code>).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Счётчик и кнопки</h3>
                <Step n="1">
                    Поставь маленький пол и точку спавна (мир мы почти не видим).
                </Step>
                <Step n="2">
                    На вкладке <b>Интерфейс</b> добавь надпись-счётчик слева
                    сверху — это монеты.
                </Step>
                <Step n="3">
                    Добавь кнопку. В инспекторе задай ей <b>градиент фона</b>,
                    <b> обводку текста</b> и скругление углов. Размер задаётся
                    в процентах: ширина <code>18</code> — это 18% экрана.
                </Step>
                <Note>
                    Поля кнопок задаются <b>в процентах</b> от экрана, а не в
                    пикселях. Так интерфейс одинаково выглядит на любом мониторе.
                    Если поставить ширину 220 — кнопка растянется на весь экран!
                </Note>

                <h3 className="lessonH">Шаг 2. Живые анимации</h3>
                <Step n="1">
                    Выдели кнопку и в инспекторе выбери свойство
                    <b> «Анимация: pulse»</b> — в игре она начнёт пульсировать сама.
                </Step>
                <Step n="2">
                    Там же есть <b>hover</b> (что делать при наведении мышкой,
                    например увеличиться) и <b>active</b> (при нажатии — сжаться).
                </Step>

                <h3 className="lessonH">Шаг 3. Скрипт кнопки «X2»</h3>
                <p>
                    Повесим на кнопку «X2 денег» скрипт: при клике она
                    эффектно крутится и на 5 секунд удваивает награду.
                </p>
                <ScriptKind kind="object" on="кнопку «X2 денег»" />
                <Code>{`// === Скрипт кнопки X2 ===
game.self.onClick(() => {
  // сначала вернём поворот в 0, потом плавно крутанём на 360°
  game.gui.update(game.self, { rotation: 0 });
  game.gui.tween(game.self, { rotation: 360 }, { duration: 0.5 });

  // включаем множитель x2 на 5 секунд
  game.broadcast('multiplier_set', 2);
  game.after(5, () => game.broadcast('multiplier_set', 1));
});`}</Code>
                <p>
                    <code>game.gui.tween(объект, что-меняем, как-долго)</code> —
                    это и есть плавная анимация. Без неё кнопка прыгнула бы резко,
                    а с твином крутится гладко, как в дорогих играх.
                </p>

                <h3 className="lessonH">Шаг 4. Главный скрипт-счётчик</h3>
                <ScriptKind kind="global" />
                <Code>{`// === Витрина GUI — главный скрипт ===
let coins = 0;        // монеты
let multiplier = 1;   // множитель награды

game.hud.setHotbarVisible(false);  // в этой игре инвентарь не нужен

// Кнопки шлют 'coin_add' — добавляем монеты с учётом множителя.
game.onMessage('coin_add', (amount) => {
  coins = coins + amount * multiplier;
  game.gui.update('hud_coins', { text: 'Монеты: ' + coins });
});

// Кнопка X2 шлёт 'multiplier_set' — меняем множитель.
game.onMessage('multiplier_set', (m) => { multiplier = m; });`}</Code>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <Step n="1">Нажми <kbd className="kbd">Играть</kbd>.</Step>
                <Step n="2">Жми кнопки — счётчик монет растёт.</Step>
                <Step n="3">Нажми «X2» — кнопка крутится, и 5 секунд монеты идут вдвое быстрее.</Step>
                <Try>
                    сделай кнопку, которая при наведении мышкой меняет цвет
                    (свойство <b>hover</b>), а при клике плавно уезжает за край
                    экрана через твин по координате X.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // РАЗБОР ИГР · Тайна старого сундука (оригинал id=2037)
    // ════════════════════════════════════════════════════
    'guide-sunduk': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Маленькое приключение. Лесная поляна с каменными руинами,
                    светящийся сундук в центре и страж рядом. Игра показывает
                    <b> модальные сцены</b> — это когда мир затемняется, всё
                    замирает, и на экране появляется что-то важное: диалог,
                    выбор приза или большая надпись.
                </p>
                <Shot src="guide-sunduk-scene.png"
                      caption="Поляна с руинами: страж слева, светящийся сундук в центре" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Диалог</b> по фразам с кнопкой ▶
                        (<code>game.modal.dialog</code>);</li>
                    <li><b>Кат-сцена</b> — затемнить мир, оставить «прожектор»
                        на объекте, подлететь камерой (<code>game.modal.open</code>);</li>
                    <li><b>Вопрос Да/Нет</b> (<code>game.modal.confirmation</code>);</li>
                    <li><b>Лутбокс</b> — выбор приза из карточек
                        (<code>game.modal.lootbox</code>);</li>
                    <li>как <b>находить объект по имени</b> и следить за
                        расстоянием до него.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поляна, сундук и страж</h3>
                <Step n="1">
                    Построй каменную площадку (greystone) с обломками стен и
                    колоннами вокруг — это руины.
                </Step>
                <Step n="2">
                    В центр поставь сундук (можно собрать из примитива-куба) и
                    в инспекторе дай ему <b>имя «chest»</b>.
                </Step>
                <Step n="3">
                    Слева поставь стража (из примитивов: цилиндр-тело, сфера-голова,
                    конус-шлем) с <b>именем «guard»</b>.
                </Step>

                <h3 className="lessonH">Шаг 2. Диалог со стражем</h3>
                <p>
                    В обычном Roblox такую сцену собирают из 5-6 кусков вручную.
                    У нас — одна команда. Главный скрипт следит за расстоянием
                    до стража и запускает диалог.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ТАЙНА СУНДУКА — главный скрипт ===
game.hud.setHotbarVisible(false);

let phase = 'start';   // этап квеста

game.onTick((dt) => {
  if (game.modal.isOpen()) return;   // во время модала ничего не триггерим
  const p = game.player.position;

  // расстояние до стража (он в точке -6, 4)
  if (phase === 'start') {
    const d = Math.hypot(p.x - (-6), p.z - 4);
    if (d < 4) {
      phase = 'talked';
      game.modal.dialog('Страж Руин', [
        'Стой, путник. Это место хранит тайну веков...',
        'В центре руин дремлет Старый сундук.',
        'Он заперт магией. Подойди — и он сам откроется.',
      ], () => game.log('Иди к сундуку!'));
    }
  }
});`}</Code>
                <Shot src="guide-sunduk-play.png"
                      caption="Диалог в игре: фраза стража внизу и кнопка ▶ для продолжения" />
                <Note>
                    Имя объекта (например «chest») ищи не сразу при старте, а
                    внутри <code>game.onTick</code> — сцена «появляется» не
                    мгновенно, и в первую секунду объект ещё не найден.
                </Note>

                <h3 className="lessonH">Шаг 3. Кат-сцена сундука</h3>
                <p>
                    Подошёл к сундуку — затемняем мир, но сундук оставляем ярким
                    (вокруг него прожектор), камера сама подлетает.
                </p>
                <Code>{`// (продолжение onTick) — подошёл к сундуку
if (phase === 'talked') {
  const d = Math.hypot(p.x - 0, p.z - (-7));
  if (d < 5) {
    phase = 'open';
    const chest = game.scene.findOne('chest');
    game.modal.open({
      darken: 0.72,                 // затемнить мир на 72%
      spotlights: [chest],          // сундук остаётся ярким (прожектор)
      cameraOverride: { target: chest, distance: 9, duration: 0.7 },
      blockInput: true,             // заблокировать управление
      content: { elements: [
        { kind: 'text', x: 50, y: 20, text: 'Старый сундук',
          textSize: 50, textColor: '#ffd700', animationPreset: 'glow' },
      ]},
    });
  }
}`}</Code>

                <h3 className="lessonH">Шаг 4. Выбор приза и победа</h3>
                <p>
                    Готовые сцены, которые вызываются одной строкой:
                </p>
                <ul>
                    <li><code>game.modal.confirmation('Открыть?', 'текст', наДа, наНет)</code> — вопрос Да/Нет;</li>
                    <li><code>game.modal.lootbox([призы], выбор)</code> — карточки призов;</li>
                    <li><code>game.modal.bossIntro(имя, hp, [враги])</code> — заставка перед боссом.</li>
                </ul>
                <Code>{`// Лутбокс — четыре приза, игрок выбирает один
game.modal.lootbox([
  { name: 'Меч зари',    icon: '⚔', color: '#c0392b' },
  { name: 'Щит луны',    icon: '🛡', color: '#2c5fb3' },
  { name: 'Кошель злата', icon: '💰', color: '#b8860b' },
  { name: 'Перо феникса', icon: '🔥', color: '#8e44ad' },
], (item) => {
  game.log('Игрок выбрал: ' + item.name);
});`}</Code>

                <h3 className="lessonH">Шаг 5. Проверка</h3>
                <Step n="1">Нажми <kbd className="kbd">Играть</kbd> и подойди к стражу — пойдёт диалог.</Step>
                <Step n="2">Иди к сундуку — мир затемнится, камера подлетит к нему.</Step>
                <Step n="3">Ответь «Да», выбери приз — увидишь финальную надпись.</Step>
                <Note>
                    Кнопка диалога на последней фразе сама превращается из ▶
                    в галочку ✓ — это значит «закрыть диалог».
                </Note>
                <Try>
                    добавь второго стража с другим квестом и сделай так, чтобы
                    сундук открывался только после разговора с обоими.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // РАЗБОР ИГР · Парк животных (оригинал id=2046)
    // ════════════════════════════════════════════════════
    'guide-zoo': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Самая весёлая игра! Ты начинаешь её в виде... <b>пончика</b>.
                    По парку стоят таблички: нажми на кнопку таблички — и твой
                    герой <b>превращается</b> в бургер, болид, пришельца, рыбу
                    или человечка. А по клавише <kbd className="kbd">B</kbd>
                    открывается <b>магазин скинов</b>.
                </p>
                <Shot src="guide-zoo-scene.png"
                      caption="Парк: 6 табличек со скинами и баннер «Примерочная скинов»" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>Кастомный скин</b> — герой может быть любой 3D-моделью,
                        не только человечком (<code>game.player.setSkin</code>);</li>
                    <li><b>Смена скина в игре</b> — без перезапуска, прямо на ходу;</li>
                    <li><b>Магазин скинов</b> — встроенный, открывается клавишей
                        <kbd className="kbd">B</kbd>;</li>
                    <li>снова <b>3D-таблички</b> — те же, что в игре «Двор», но
                        теперь их кнопка меняет скин.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Парк и стартовый скин</h3>
                <Step n="1">
                    Построй полянку с деревьями и поставь точку спавна.
                </Step>
                <Step n="2">
                    Нажми кнопку <b>«Скины»</b> в шапке редактора. Выбери
                    <b> стартовый скин</b> (например пончик), включи галочку
                    <b> «Магазин скинов»</b> и задай стартовые рублики (например 200).
                </Step>
                <Step n="3">
                    Поставь несколько 3D-табличек (как в игре «Двор») — по одной
                    на каждый скин. Запомни их номера (<code>primitive:10</code> и т.д.).
                </Step>

                <h3 className="lessonH">Шаг 2. Скрипт превращений</h3>
                <p>
                    Главный скрипт подписывается на клик по каждой табличке и
                    меняет скин одной командой.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ПАРК ЖИВОТНЫХ — главный скрипт ===
game.player.setSkinCoins(200);   // 200 рубликов на магазин

// Каждая табличка при клике надевает свой скин.
game.billboard.onClick('primitive:10', 'buy', () => game.player.setSkin('burger'));
game.billboard.onClick('primitive:11', 'buy', () => game.player.setSkin('car-race'));
game.billboard.onClick('primitive:12', 'buy', () => game.player.setSkin('alien'));
game.billboard.onClick('primitive:13', 'buy', () => game.player.setSkin('fish'));
game.billboard.onClick('primitive:14', 'buy', () => game.player.setSkin('character-a'));

// Узнать, когда скин сменился:
game.player.onSkinChange((newSkin) => {
  game.log('Теперь я: ' + newSkin);
});`}</Code>
                <p>
                    <code>game.player.setSkin('burger')</code> — одна строчка
                    меняет всё тело героя на новую модель. Имена скинов
                    (burger, car-race, alien…) видны в окне «Скины».
                </p>

                <h3 className="lessonH">Шаг 3. Магазин скинов</h3>
                <p>
                    Магазин уже встроен — мы включили его галочкой в окне «Скины».
                    В игре он открывается клавишей <kbd className="kbd">B</kbd>.
                    Командами скинами тоже можно управлять:
                </p>
                <Code>{`game.player.unlockSkin('alien');   // открыть скин игроку
game.player.openSkinShop();        // открыть магазин из кода
game.player.setSkinCoins(500);     // задать баланс рубликов`}</Code>
                <Shot src="guide-zoo-play.png"
                      caption="Старт игры: твой герой — пончик! Он покачивается на ходу" />

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <Step n="1">Нажми <kbd className="kbd">Играть</kbd> — ты появишься пончиком.</Step>
                <Step n="2">Походи — пончик смешно покачивается на ходу.</Step>
                <Step n="3">Нажми на кнопку таблички — превратишься в другого героя.</Step>
                <Step n="4">Нажми <kbd className="kbd">B</kbd> — откроется магазин скинов.</Step>
                <Note>
                    Скины бывают двух видов: <b>человечки</b> (умеют махать,
                    прыгать, танцевать) и <b>модели</b> (пончик, машинка — они
                    просто покачиваются). Свою модель <code>.glb</code> тоже можно
                    загрузить в окне «Скины».
                </Note>
                <Try>
                    сделай скин платным: дай игроку 100 рубликов, поставь скину
                    цену 50 в магазине и проверь — хватит ли денег его купить.
                </Try>
            </>
        ),
    },

    // ════════════════════════════════════════════════════
    // РАЗБОР ИГР · Туториал — собери монетки (оригинал id=333)
    // ════════════════════════════════════════════════════
    'guide-strelka': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Простой туториал-уровень: ровный газон и три цели —
                    <b> красный куб</b>, <b>синяя сфера</b> и <b>золотой сундук</b>.
                    Над целью парит светящаяся <b>стрелка-указатель «иди сюда»</b>,
                    а к ней по земле бежит дорожка из шевронов — точь-в-точь как
                    маркеры заданий в Roblox. Дошёл до цели — стрелка сама
                    перепрыгивает на следующую и меняет цвет.
                </p>

                <Shot src="guide-strelka-play.png" wide
                      caption="Готовый туториал: красная стрелка-указатель парит над целью, к ней бежит дорожка из шевронов, сверху подсказка «Иди за стрелкой к цели!»" />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.fx.pointer</b> — создавать 3D-указатель к цели
                        (лента шевронов + парящий маркер над объектом);</li>
                    <li><b>пресеты стрелки</b> — готовые стили
                        <code> guide</code> (красная), <code>quest</code> (жёлтая),
                        <code> gift</code> (радужная);</li>
                    <li><b>setTarget / update</b> — перенацеливать стрелку на новую
                        цель и менять её стиль на лету;</li>
                    <li><b>onTouch</b> — ловить, когда игрок дошёл до цели.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Сцена</h3>
                <Shot src="guide-strelka-scene.png" wide
                      caption="Сцена в редакторе: газон-пол, три цели (красный куб, синяя сфера, золотой куб), игрок и один скрипт-оркестратор в Иерархии" />
                <Step n="1">
                    Инструментом <kbd className="kbd">Примитив</kbd> положи большой
                    плоский <b>куб</b> — это газон (пол).
                </Step>
                <Step n="2">
                    Поставь три примитива-цели и дай им <b>имена</b> в инспекторе:
                    <code> red-cube</code> (куб), <code>blue-sphere</code> (сфера),
                    <code> gold-chest</code> (куб). Имя — это как раз то, по чему
                    скрипт найдёт объект.
                </Step>
                <Step n="3">
                    На вкладке <b>Игра</b> поставь <b>точку спавна</b> перед целями.
                </Step>

                <h3 className="lessonH">Шаг 2. Стрелка к первой цели</h3>
                <p>
                    Стрелку создаёт одна команда. <b>Важно:</b> ищем цель не сразу,
                    а через <code>game.after</code> — на старте объекты сцены ещё
                    могут быть не готовы.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// === ТУТОРИАЛ — СОБЕРИ МОНЕТКИ — главный скрипт ===

const targets = ['red-cube', 'blue-sphere', 'gold-chest'];
const presets = ['guide', 'quest', 'gift'];   // цвета стрелки по очереди
let step = 0;
let arrow = null;

game.ui.set('hint', 'Иди за стрелкой к цели!', { x: 50, y: 6, anchor: 'top' });

// Через 0.4 сек — создаём стрелку от игрока к первой цели.
game.after(0.4, () => {
  const first = game.scene.findOne(targets[0]);
  arrow = game.fx.pointer({ from: 'player', to: first, preset: 'guide' });
});`}</Code>
                <p>
                    <code>game.fx.pointer(&#123; from, to, preset &#125;)</code> —
                    <code> from: 'player'</code> значит «от игрока»,
                    <code> to</code> — объект-цель (нашли через
                    <code> findOne</code>), <code>preset</code> — стиль.
                </p>

                <h3 className="lessonH">Шаг 3. Переход к следующей цели</h3>
                <p>
                    На каждую цель вешаем <code>onTouch</code>: дошёл — берём
                    следующую и перенацеливаем ту же стрелку через
                    <code> setTarget</code>, а стиль меняем через
                    <code> update</code>.
                </p>
                <Code>{`for (let i = 0; i < targets.length; i++) {
  const obj = game.scene.findOne(targets[i]);
  if (!obj) continue;
  obj.onTouch(() => {
    if (i !== step) return;          // только текущая по порядку цель
    step++;
    if (step >= targets.length) {
      if (arrow) arrow.remove();     // все цели собраны — убираем стрелку
      game.ui.set('hint', 'Молодец! Все цели собраны!', { x: 50, y: 6, anchor: 'top' });
      return;
    }
    const next = game.scene.findOne(targets[step]);
    arrow.setTarget(next);                  // стрелка теперь ведёт к следующей
    arrow.update({ preset: presets[step] }); // и меняет цвет
  });
}`}</Code>

                <h3 className="lessonH">Шаг 4. Проверка</h3>
                <Step n="1">
                    Нажми <kbd className="kbd">Играть</kbd>. Над красным кубом
                    появится парящая красная стрелка и дорожка-шевроны к нему.
                </Step>
                <Step n="2">
                    Подойди к кубу — стрелка перепрыгнет на синюю сферу (станет
                    жёлтой), потом на сундук (радужная).
                </Step>
                <Note>
                    Стрелка-указатель — лучший способ вести новичка по твоей игре:
                    куда идти, что нажать, где следующее задание.
                </Note>
                <Try>
                    сделай «кастомную» стрелку: добавь GUI-кнопку и по нажатию
                    создай <code>game.fx.pointer</code> с
                    <code> texture: 'lightning'</code>, <code>color: '#22ff66'</code>
                    и <code>curved: true</code> — получится изогнутая зелёная
                    молниевая стрелка.
                </Try>
            </>
        ),
    },

    'guide-lego': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Лего-полигон в стиле Roblox: <b>зелёный пол</b> и <b>оранжевая
                    стена</b> из окрашиваемых блоков, <b>жёлтая ступенька</b>, синий
                    куб и фиолетовые кубики — все с узнаваемой текстурой
                    <b> «studs»</b> (лего-кружки). В углу — готовый <b>лего-сет</b>:
                    деревья, дом и машина. Один и тот же материал, любой цвет — как
                    настоящий конструктор.
                </p>

                <Shot src="guide-lego-play.png" wide
                      caption="Лего-полигон в игре: пол и стена из studs-блоков, разноцветные кубы, готовые лего-дерево/дом/машина в углу." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>material: 'studs'</b> — лего-текстура на любом примитиве
                        (куб, сфера, цилиндр), цвет берётся из обычного color;</li>
                    <li><b>studs-block</b> — окрашиваемый блок: один тип, цвет
                        задаётся на каждый блок (per-instance), тысячи блоков — один
                        draw call;</li>
                    <li><b>тайлинг по размеру</b> — кружки не растягиваются: куб 4×4
                        покажет 4×4 studs, куб 1×1 — один;</li>
                    <li><b>лего-сет</b> — готовые модели (кирпичи, плиты, скаты,
                        дерево, дом, машина, человечек) из studs-примитивов.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Пол и стена из studs-блоков</h3>
                <Shot src="guide-lego-scene.png" wide
                      caption="Сцена в редакторе: зелёный пол и оранжевая стена из окрашиваемых блоков, разноцветные studs-кубы, лего-модели в углу." />
                <Step n="1">
                    В палитре блоков открой категорию <b>«Окрашиваемые»</b> — там
                    блок <kbd className="kbd">Лего-кирпич</kbd> (studs-block). Под
                    палитрой появится <b>выбор цвета</b>.
                </Step>
                <Step n="2">
                    Поставь зелёным цветом большой <b>пол</b> (30×30), затем оранжевым —
                    <b> стену</b> вдоль дальнего края. Цвет каждого блока сохраняется
                    отдельно — стена и пол из одного типа блока.
                </Step>

                <h3 className="lessonH">Шаг 2. Studs на примитивах</h3>
                <Step n="3">
                    Поставь <b>куб</b> (Примитив → cube). В инспекторе справа выбери
                    материал <b>«Studs»</b> (пятый рядом с Матовый/Металл/Стекло/Неон)
                    и задай цвет. Готово — лего-кружки на всех гранях.
                </Step>
                <Note>
                    Размер кружков считается автоматически от размера меша: растяни
                    куб — studs останутся того же масштаба, просто их станет больше.
                </Note>

                <h3 className="lessonH">Шаг 3. Через код</h3>
                <ScriptKind kind="global" />
                <Code>{`// Окрашиваемый блок — пол:
for (let x = -15; x < 15; x++)
  for (let z = -15; z < 15; z++)
    game.scene.spawn('block:studs-block', { x, y: 0, z, color: '#5cba35' });

// Примитив с лего-текстурой:
game.scene.spawn('primitive:cube', {
  x: 0, y: 1, z: 0, sx: 2, sy: 2, sz: 2,
  color: '#3a7aff', material: 'studs',
});

// Готовая модель из лего-сета:
game.scene.spawn('model:lego-house-small', { x: 10, y: 0, z: -10 });

// Сменить цвет блока на лету:
game.scene.setColor('block:0,0,0', '#ff0000');`}</Code>

                <Try>
                    Собери из <b>лего-кирпичей</b> разных размеров (1×2, 2×4, 2×8) свою
                    постройку, покрась каждый ряд в свой цвет — и поставь сверху
                    готовую <b>лего-машину</b> из набора.
                </Try>
            </>
        ),
    },

    'guide-dynamic-label': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Часовая башня с <b>живыми надписями прямо в 3D</b> и <b>витриной-
                    лутбоксом</b>: над башней — <b>таймер обратного отсчёта</b> в
                    жёлто-синей рамке (как в Roblox); ряд из <b>трёх подиумов</b>, на
                    каждом парит и вращается предмет (меч, кубок, ключ), а перед ним —
                    <b> наклонная табличка-ценник</b> с названием; <b>счётчик монет</b>
                    «1 230 рубликов» с золотой монетой (клик +10); над зомби — <b>полоса
                    HP</b>. Все надписи обновляются сами, без мигания.
                </p>

                <Shot src="guide-dynamic-label-play.png" wide
                      caption="Витрина-лутбокс: на подиумах вращаются предметы, перед каждым наклонная табличка с названием; слева счётчик монет, над башней таймер." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>scene.bindLabel(объект, fn, opts)</b> — привязать надпись к
                        функции: что вернёт функция, то и на плашке. Обновляется раз в
                        <code>interval</code> сек, текстура перерисовывается только
                        когда текст реально изменился (диф-чек);</li>
                    <li><b>scene.bindTimer(объект, времяКонца, opts)</b> — готовый
                        таймер обратного отсчёта с <code>onEnd</code>-колбэком;</li>
                    <li><b>attachFace</b> — прикрепить надпись ПЛОСКО к грани примитива
                        (<code>'front'/'top'/...</code>): наклоняешь сам примитив —
                        текст лежит в его плоскости и наклонён вместе с ним;</li>
                    <li><b>5 пресетов плашек</b> — <code>gameui</code>, <code>boss-hp</code>,
                        <code>reward</code> (золото), <code>warning</code>, <code>plain</code>;</li>
                    <li><b>richText</b> — разноцветные части: <code>{'<color=#hex>…</color>'}</code>;</li>
                    <li><b>game.format</b> — <code>time</code> (00:15:59),
                        <code>money</code> («1 334 рублика» — разделитель + склонение);</li>
                    <li><b>obj.move / obj.rotate</b> — двигать и вращать предмет (парение
                        и крутёж на подиуме);</li>
                    <li><b>надпись из инспектора</b> — раздел «Подпись над объектом»:
                        таймер/счётчик/HP без кода.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Надпись из инспектора (без кода)</h3>
                <Shot src="guide-dynamic-label-scene.png" wide
                      caption="Сцена в редакторе: часовая башня из studs, витрина с подиумами, антураж (сосны, камни, цветы)." />
                <Step n="1">
                    Выдели объект → в инспекторе справа раздел
                    <b> «Подпись над объектом»</b> → включи галочку.
                </Step>
                <Step n="2">
                    В списке <b>«Связать с…»</b> выбери <b>Таймер</b>, длительность
                    (960 = 16 минут), формат <code>hh:mm:ss</code>, стиль <b>gameui</b>.
                    Жми Play — цифры пошли вниз.
                </Step>

                <h3 className="lessonH">Шаг 2. Таблички-ценники: наклони примитив</h3>
                <p>
                    Правильная логика — <b>наклоняешь сам примитив-планшет</b>, а текст
                    крепишь к его передней грани через <code>attachFace: 'front'</code>.
                    Текст ляжет ТОЧНО в плоскость планшета и наклонится вместе с ним —
                    как ценник на витрине. Размер планшета сделай чуть больше текста,
                    чтобы надпись не вылезала за края.
                </p>
                <ScriptKind kind="object" />
                <Code>{`// Планшет наклонён в редакторе (поворот по X ≈ -29°, верх назад).
// Табличка лежит ПЛОСКО на его передней грани (БЕЗ отдельного наклона):
const plate = game.scene.findOne('Планшет1');
game.scene.bindLabel(plate, () => 'Золотой кубок',
  { preset: 'gameui', size: 0.8, attachFace: 'front' });

// Предмет на подиуме — парит и вращается:
const obj = game.scene.findOne('Предмет1');
const x = obj.position.x, z = obj.position.z;
let t = 0;
game.onTick((dt) => {
  t += dt;
  obj.move(x, 1.9 + Math.sin(t * 2) * 0.18, z);  // парение
  obj.rotate(t * 1.2);                            // вращение вокруг Y
});

// Клик по предмету — "взять":
obj.onClick(() => game.ui.set('grab', 'Ты взял: Золотой кубок!',
  { x: 50, y: 14, anchor: 'top', color: '#ffd23a', size: 22 }));`}</Code>

                <h3 className="lessonH">Шаг 3. Таймер, счётчик, HP</h3>
                <Code>{`// Таймер над башней (на передней грани верхнего яруса):
const endTs = Date.now() + 16 * 60 * 1000;
game.scene.bindTimer(game.scene.findOne('ВерхБашни'), endTs, {
  prefix: 'Сбросится через ', format: 'hh:mm:ss', preset: 'gameui', attachFace: 'front',
});

// Счётчик монет (формат с разделителем и склонением), клик +10:
const plate = game.scene.findOne('ПланшетМонет');
let coins = 1230;
game.scene.bindLabel(plate, () => game.format.money(coins),
  { preset: 'reward', attachFace: 'front' });
game.scene.findOne('МонетаСчёт').onClick(() => { coins += 10; });

// HP над зомби — billboard (всегда к камере), меняется по клику:
const zombie = game.scene.findOne('Зомби');
let hp = 100;
game.scene.bindLabel(zombie, () => 'Зомби  HP: ' + hp + '/100', { preset: 'boss-hp' });
zombie.onClick(() => { hp = Math.max(0, hp - 10); });`}</Code>
                <Note>
                    <b>findOne сразу на старте может вернуть null</b> — сцена приходит
                    чуть позже. Оберни поиск в <code>game.after(0.3, () =&gt; {'{ … }'})</code>.
                    Без <code>attachFace</code> плашка висит билбордом над верхом
                    объекта (как HP-полоса) — это удобно для NPC.
                </Note>

                <h3 className="lessonH">Почему это не тормозит</h3>
                <p>
                    <b>Диф-чек:</b> <code>bindLabel</code> перерисовывает текстуру
                    только когда строка изменилась — таймер обновляется раз в секунду,
                    а не каждый кадр. Привязка <b>сама отменяется</b> при
                    <code> scene.delete</code> — утечек нет.
                </p>

                <Try>
                    Сделай ещё один подиум со своим предметом из палитры моделей.
                    Наклони планшет под тем же углом, повесь название через
                    <code> attachFace: 'front'</code>, а предмет заставь парить и
                    крутиться через <code>obj.move</code> + <code>obj.rotate</code>.
                </Try>
            </>
        ),
    },

    'guide-zavod': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Маленький <b>tycoon-завод</b> на воксельном острове: трава, холмы,
                    деревья, пруд. Снизу — <b>магазин-инвентарь</b> из трёх слотов
                    (ящик, дерево, печь). Кликаешь слот → за курсором летит
                    <b> полупрозрачная тень</b> предмета, повторяющая его форму. ЛКМ
                    ставит предмет на свой участок (можно <b>класть стопкой</b>),
                    <b> R</b> или колесо — поворот, <b>ПКМ/Esc</b> — отмена. Денег не
                    хватает → слот <b>серый</b>, поставить нельзя. Это базовый
                    gameplay-loop любой топ-игры: <i>купи → поставь → развивай</i>
                    (Pet Simulator, Lumber Tycoon, Build A Boat).
                </p>

                <Shot src="guide-zavod-play.png" wide
                      caption="Завод: магазин снизу, тень-превью предмета летит за курсором, вокруг — воксельный мир с деревьями и прудом." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.inventoryUi.create(opts)</b> — готовый магазин-инвентарь:
                        нижняя панель слотов с иконкой/ценой; клик по слоту зовёт
                        <code> onSlotClick(item)</code>. Слот сам сереет, если валюты мало;</li>
                    <li><b>game.placement.start(itemKey, opts)</b> — войти в режим
                        расстановки: тень предмета за курсором, снап к сетке, проверка
                        зоны и баланса, подсветка валидности (зелёный/красный);</li>
                    <li><b>placement.onPlace(fn)</b> — колбэк «предмет поставлен»:
                        тут спавним реальный объект и списываем деньги;</li>
                    <li><b>тень-превью формой модели</b> — для воксельных моделей тень
                        повторяет их геометрию (не куб) и основание точно под курсором;</li>
                    <li><b>стопка и снап</b> — предметы ложатся друг на друга и
                        прилипают к сетке;</li>
                    <li><b>inventoryUi.setBalance(валюта, сумма)</b> — обновляешь баланс →
                        дорогие слоты автоматически серые (нельзя уйти в минус);</li>
                    <li><b>воксельные модели + ландшафт</b> — мир собран из voxel-
                        моделей и террейна (трава/деревья/вода), а не голых кубов.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Магазин-инвентарь снизу</h3>
                <p>
                    <code>game.inventoryUi.create</code> рисует панель слотов. Каждый
                    слот — это товар: ключ, название, иконка, цена и тип модели.
                    В <code>onSlotClick</code> запускаем расстановку этого товара.
                </p>
                <ScriptKind kind="global" />
                <Code>{`let rubles = 1000;
function wallet() {
  game.ui.set('wallet', game.format.money(rubles) + ' рубликов',
    { x: 50, y: 6, anchor: 'top', color: '#ffd23a', size: 24 });
  // Обновляем баланс магазина — слоты дороже денег станут серыми:
  game.inventoryUi.setBalance('rubles', rubles);
}
wallet();

game.after(0.4, () => {
  game.inventoryUi.create({
    items: [
      { key: 'crate', name: 'Ящик',   icon: 'crate', cost: 50,  modelType: 'user:444' },
      { key: 'tree',  name: 'Дерево', icon: 'plant', cost: 100, modelType: 'user:445' },
      { key: 'oven',  name: 'Печь',   icon: 'oven',  cost: 500, modelType: 'user:446' },
    ],
    position: 'bottom', showCost: true, showCurrency: 'rubles',
    onSlotClick: (item) => startPlacing(item.key),  // ← см. Шаг 2
  });
  wallet();
});`}</Code>

                <h3 className="lessonH">Шаг 2. Режим расстановки (тень за курсором)</h3>
                <p>
                    <code>game.placement.start</code> создаёт полупрозрачную тень и
                    включает режим: тень едет за курсором, снаппится к сетке, краснеет
                    вне зоны. <code>previewScale</code> для куба — размер в юнитах, а
                    для воксельной модели задавай <code>modelScale</code> = масштаб
                    модели — тогда тень будет точной формой предмета.
                </p>
                <ScriptKind kind="global" />
                <Code>{`const SHOP = {
  crate: { id: 'user:444', cost: 50,  scale: 1.6 },
  tree:  { id: 'user:445', cost: 100, scale: 7.0 },
  oven:  { id: 'user:446', cost: 500, scale: 2.0 },
};

function startPlacing(key) {
  const s = SHOP[key];
  game.placement.start(key, {
    previewType: s.id,          // 'user:<id>' — воксельная модель
    modelScale:  s.scale,       // тень той же формы и размера, что предмет
    surfaceMode: 'ground',      // ставим на землю ИЛИ на верх другого объекта (стопка)
    grid: 0.5,                  // снап к полусетке
    cost: s.cost, currency: 'rubles',  // нет денег → ставить нельзя
    targetZone: game.scene.findOne('player-plot'),  // свой участок
    chainPlace: true,           // ставить подряд, не выходя из режима
    previewPulse: true,         // тень слегка пульсирует
  });
}`}</Code>
                <Note>
                    Управление в режиме расстановки: <b>ЛКМ</b> — поставить,
                    <b> R</b> или <b>колесо мыши</b> — повернуть на 90°,
                    <b> ПКМ / Esc</b> — отменить. С <code>chainPlace: true</code>
                    после установки режим не закрывается — удобно строить рядами.
                </Note>

                <h3 className="lessonH">Шаг 3. Поставили — спавним и платим</h3>
                <p>
                    Когда игрок жмёт ЛКМ на валидном месте, движок зовёт
                    <code> onPlace</code> с позицией и поворотом. Здесь создаём
                    <b> настоящий</b> объект (тень — лишь подсказка) и списываем монеты.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.placement.onPlace(({ itemKey, position, rotationY }) => {
  const s = SHOP[itemKey];
  game.scene.spawn(s.id, {
    x: position.x, y: position.y, z: position.z,
    rotationY: rotationY, scale: s.scale,
    name: itemKey + '_' + Date.now(),
  });
  rubles -= s.cost;
  wallet();   // обновили счётчик и серость слотов
});

// (необязательно) реакция на отмену:
game.placement.onCancel(() => game.ui.set('hint', '', {}));`}</Code>

                <Shot src="guide-zavod-scene.png" wide
                      caption="Сцена в редакторе: воксельный остров с травой, деревьями и прудом; в центре — участок завода, где расставляются предметы." />

                <h3 className="lessonH">Почему это «честная» механика</h3>
                <p>
                    <b>Тень ≠ предмет.</b> Пока ты в режиме расстановки — на сцене
                    только полупрозрачная подсказка. Реальный объект появляется
                    <i> один раз</i> в <code>onPlace</code>. Поэтому ничего не дублируется,
                    а всё, что наспавнено за игру, <b>удаляется при выходе (Esc/Стоп)</b> —
                    игровая сессия не «протекает» в сохранение. Деньги списываются только
                    при успешной установке, а недоступные по цене слоты серые — <b>в минус
                    уйти нельзя</b>.
                </p>

                <Try>
                    Добавь четвёртый товар в магазин (например, фонарь — модель
                    <code> user:448</code>) со своей ценой. Сделай ему дорогую стоимость
                    и проверь: пока денег мало — слот серый и не ставится, а как накопишь —
                    станет активным. Попробуй построить из ящиков башню (стопкой).
                </Try>
            </>
        ),
    },

    'guide-taxi': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    <b>Три локации в одном проекте</b>, между ними — красивая загрузка.
                    Игрок в <b>гараже</b> у жёлтого такси жмёт «Поехать в город» → весь
                    экран <b>плавно затемняется</b>, в центре снимок сцены, <b>жёлтый
                    прогресс-бар</b> заполняется за 4 секунды, крупно <b>процент</b>,
                    кнопка <b>«Пропустить»</b> и спиннер <b>«Загрузка»</b>. Через 4 секунды
                    экран исчезает — игрок уже <b>в городе</b> с высотками и закатным небом.
                    В городе кнопка <b>«Магазин»</b> делает короткий переход <b>внутрь
                    закрытого магазина</b> (ряды стеллажей, кассы), а «Назад» возвращает на
                    улицу. Это та самая «загрузка между мирами» из больших игр (Taxi Boss,
                    Brookhaven, Jailbreak) — несколько миров без отдельных «уровней».
                </p>

                <Shot src="guide-taxi-play.png" wide
                      caption="Экран загрузки: затемнение, снимок сцены, прогресс-бар с процентом, кнопка «Пропустить» и спиннер «Загрузка» — как в больших играх при телепорте." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.loading.transition(opts)</b> — готовый переход: показал
                        экран, заполнил бар за <code>duration</code> секунд, сам скрыл.
                        Возвращает <code>Promise</code> — пишешь <code>await</code> и
                        продолжаешь код уже «на новом уровне»;</li>
                    <li><b>game.loading.show(opts)</b> — ручной режим: возвращает
                        хэндл с <code>setProgress(0..1)</code>, <code>setText()</code>,
                        <code> close()</code> — для реальной загрузки ресурсов;</li>
                    <li><b>cover: {`{ sceneSnapshot: true }`}</b> — кадр текущей сцены
                        автоматически становится картинкой-превью;</li>
                    <li><b>кнопка «Пропустить»</b> (<code>skipButton</code>) и
                        <b> спиннер</b> (<code>spinner</code>) — включаются одной опцией;</li>
                    <li><b>blockInput + пауза</b> — во время загрузки WASD/мышь не
                        работают, а симуляция замирает (NPC не двигаются), автоматически.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Магазин по умолчанию — настройки игры</h3>
                <p>
                    В <b>Настройки → Экран загрузки</b> один раз задаёшь <b>логотип
                    игры</b>, <b>цвет акцента</b> (бар и кнопка) и галочки «спиннер» /
                    «кнопка Пропустить» по умолчанию. Дальше любой <code>game.loading</code>
                    в этой игре берёт их сам — не повторяешь стиль в каждом вызове.
                </p>

                <h3 className="lessonH">Шаг 2. Кнопка «Поехать» → переход в город</h3>
                <p>
                    Делаем кнопку и вешаем на неё переход. <code>await
                    game.loading.transition(...)</code> «замораживает» код, пока крутится
                    загрузка, и продолжается, когда она закончилась (или игрок нажал
                    «Пропустить»). После — телепорт и смена окружения: игрок «оказался» в
                    новом мире.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.gui.create('button', {
  id: 'btn_go', x: 50, y: 92, w: 26, h: 9, anchor: 'center',
  text: 'Поехать в город', textColor: '#3a2a00', textSize: 20, fontWeight: 800,
  bgGradient: { stops: ['#ffd23a', '#e0a000'], angle: 90 }, borderRadius: 12,
});
game.gui.onClick('btn_go', async () => {
  game.gui.remove('btn_go');
  await game.loading.transition({
    cover: { sceneSnapshot: true },   // снимок текущей сцены как картинка
    duration: 4,                       // бар заполняется 4 секунды
    text: 'Едем в центр города...',
    skipButton: true,                  // можно пропустить ожидание
    spinner: true,                     // спиннер «Загрузка» справа
  });
  // Этот код выполнится ПОСЛЕ загрузки (экран уже скрыт):
  game.player.teleport(100, 2, 100);   // телепорт в город
  game.scene.environment = 'sunset';   // закатное небо
});`}</Code>
                <Note>
                    <code>transition</code> — «фейковый» прогресс на заданное время (для
                    красивого перехода). Точно так же сделана кнопка <b>«Магазин»</b>:
                    переход <code>1.5</code>с → <code>teleport</code> внутрь закрытого
                    зала-магазина (отдельная локация со стеллажами и кассами) → кнопка
                    «Назад» возвращает в город. Для <b>реальной</b> загрузки ресурсов есть
                    <code> show</code> + <code>setProgress</code> — см. Шаг 3.
                </Note>

                <h3 className="lessonH">Шаг 3. Ручной прогресс (реальная загрузка)</h3>
                <p>
                    Если грузишь много объектов и хочешь показать <b>настоящий</b>
                    прогресс — открой экран через <code>show</code> и двигай бар сам
                    через <code>setProgress</code>. Закрой через <code>close()</code>.
                </p>
                <ScriptKind kind="object" />
                <Code>{`const lo = game.loading.show({ progressBar: true, spinner: true });
const total = 10;
let i = 0;
const step = () => {
  i++;
  // ... подгрузить i-й кусок мира ...
  lo.setProgress(i / total);          // двигаем бар вручную
  if (i < total) game.after(0.2, step);  // следующий шаг через 0.2с
  else lo.close();                     // всё загружено — спрятать экран
};
step();`}</Code>

                <Shot src="guide-taxi-scene.png" wide
                      caption="Город изнутри: перекрёсток с высотками по обеим сторонам и Макдональдс в конце улицы — одна из трёх локаций проекта (гараж · город · интерьер магазина)." />

                <h3 className="lessonH">Почему это удобно</h3>
                <p>
                    Один проект — <b>три мира</b> (гараж · город · интерьер магазина),
                    собранные в разных углах сцены, а переключение между ними прячется за
                    красивым экраном загрузки. Игрок не видит «телепорт рывком» — видит
                    плавную загрузку, как в больших играх. А <code>await</code> делает код
                    линейным: «показать загрузку → дождаться → телепортировать → сменить
                    небо». Так из одного проекта получается целая игра с локациями, без
                    отдельных уровней-файлов.
                </p>

                <Try>
                    Добавь <b>четвёртую локацию</b> — например «парк». Поставь её в ещё
                    одном углу сцены (скажем, <code>x = -100</code>), сделай кнопку «В парк»
                    и переход на неё через <code>loading.transition</code> (свой текст и
                    <code> duration</code>). Не забудь кнопку «Назад», которая возвращает в
                    город. Подсказка: каждый переход = <code>await transition</code> →
                    <code> teleport(...)</code> → <code>environment = '...'</code>.
                </Try>
            </>
        ),
    },

    'guide-garage': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    <b>Главное меню как в топ-играх Roblox.</b> Игрок попадает в тёмный
                    гараж, где <b>камера кинематографично облетает</b> премиум-машину
                    (модель из набора Kenney). Сверху — логотип игры, справа — <b>список
                    обновлений (патч-ноуты)</b>, снизу — большая жёлтая кнопка <b>«ИГРАТЬ»</b>,
                    играет фоновая музыка. Управление заблокировано — игрок только смотрит.
                    Клик «ИГРАТЬ» → плавная загрузка → ты в самой игре. Первые 5 секунд
                    решают, останется игрок или уйдёт — поэтому красивое меню = лицо игры.
                </p>

                <Shot src="guide-garage-play.png" wide
                      caption="Главное меню: облёт камеры вокруг машины в гараже, патч-ноуты справа, кнопка ИГРАТЬ — как в премиум-симуляторах Roblox." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.mainMenu.show(opts)</b> — одним вызовом строится всё меню:
                        камера-облёт, логотип, патч-ноуты, кнопка ИГРАТЬ, музыка, блок
                        управления;</li>
                    <li><b>cinematic-камера</b> — плавный пролёт по точкам (waypoints) или
                        облёт по кругу (orbit) вокруг витрины;</li>
                    <li><b>патч-ноуты</b> — карточка «версия + список изменений» одной
                        строкой <code>setPatchNotes</code>;</li>
                    <li><b>onPlay</b> — что происходит по кнопке ИГРАТЬ: прячем меню →
                        загрузка (задача 12) → телепорт в игровую зону;</li>
                    <li>меню — <b>отдельная зона</b> сцены (гараж), а игра — другая зона
                        (поляна); переключение прячется за загрузкой.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Показать меню при старте</h3>
                <p>
                    Весь экран строится одним <code>game.mainMenu.show</code>. Камера в
                    режиме <code>cinematic</code> летит по точкам <code>waypoints</code>
                    вокруг машины (центр <code>target</code>). Каждая точка — позиция
                    камеры + куда она смотрит.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.mainMenu.show({
  title: 'ГАРАЖ БОССА',
  camera: {
    mode: 'cinematic', duration: 16,
    waypoints: [
      { position: { x: 6,  y: 2.6, z: 5.5 }, target: { x: 0, y: 1.3, z: 0 } },
      { position: { x: 0,  y: 1.6, z: 6 },   target: { x: 0, y: 1.3, z: 0 } },
      { position: { x: -6, y: 3.2, z: -4.5 },target: { x: 0, y: 1.3, z: 0 } },
      { position: { x: 6,  y: 2.6, z: 5.5 }, target: { x: 0, y: 1.3, z: 0 } }, // петля
    ],
  },
  patchNotes: {
    version: '3.10', title: 'ГАРАЖ БОССА',
    items: ['Новая система контрактов', '2 ремоделинга', '2 новых обвеса',
            'Улучшенные звуки', 'Багфиксы и многое другое...'],
  },
  music: 'epoch_01_main',
  playButtonText: 'ИГРАТЬ',
  onPlay: onPlay,   // ← см. Шаг 2
});`}</Code>
                <Note>
                    Камера НЕ должна вылетать за стены — держи точки внутри сцены меню.
                    Другие режимы: <code>orbit</code> (круговой облёт: center/radius/height),
                    <code> static</code> (одна точка), <code>preset-cuts</code> (резкие
                    смены ракурса, как в трейлере).
                </Note>

                <h3 className="lessonH">Шаг 2. Кнопка ИГРАТЬ → вход в игру</h3>
                <p>
                    По кнопке прячем меню (это снимает блок управления, музыку и камеру),
                    показываем загрузку (задача 12) и телепортируем игрока в игровую зону.
                </p>
                <ScriptKind kind="global" />
                <Code>{`async function onPlay() {
  game.mainMenu.hide();                  // убрать меню, вернуть управление
  await game.loading.transition({        // красивая загрузка (задача 12)
    cover: { sceneSnapshot: true }, duration: 1.5, text: 'Загружаем мир...',
  });
  game.player.teleport(0, 2, 100);       // в игровую зону (поляна)
  game.scene.environment = 'day';        // дневное небо
}`}</Code>

                <Shot src="guide-garage-scene.png" wide
                      caption="Сцена меню в гараже: премиум-машина по центру, NPC-босс рядом, прожекторы под потолком — камера облетает эту витрину." />

                <h3 className="lessonH">Почему это «лицо игры»</h3>
                <p>
                    Меню — <b>отдельная зона</b> проекта (гараж-витрина), игра — другая
                    (поляна). До клика ИГРАТЬ человек видит живую кинематографичную
                    картинку, а не «прототип со спавном». Камера, патч-ноуты и музыка
                    создают ощущение большой игры. А весь переход — это просто
                    <code> hide</code> → <code>transition</code> → <code>teleport</code>.
                </p>

                <Try>
                    Поменяй режим камеры на <code>orbit</code> (круговой облёт):
                    <code> camera: {`{ mode: 'orbit', center: { x:0, y:1, z:0 }, radius: 6, height: 2.5, duration: 12 }`}</code>.
                    И обнови патч-ноуты под свою игру через
                    <code> game.mainMenu.setPatchNotes(...)</code>.
                </Try>
            </>
        ),
    },

    'guide-taxisim': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    <b>Настоящие машины, на которых можно ездить.</b> Подходишь к
                    автомобилю → над ним появляется подсказка <b>«[F] Enter»</b>, держишь
                    F → садишься за руль. <b>WASD</b> рулят (газ/тормоз/задний ход +
                    повороты), камера плавно <b>следует за машиной</b>, снизу — <b>спидометр</b>
                    с передачей (D/R/N). <b>V</b> меняет ракурс камеры, <b>E</b> — выйти.
                    Машина сталкивается со стенами и столбами. Это основа таксопарков,
                    гонок, доставки — 30% жанров Roblox держатся на транспорте.
                </p>

                <Shot src="guide-taxisim-play.png" wide
                      caption="Игрок за рулём такси: камера за машиной, спидометр снизу, подсказки клавиш. Рядом — второй автомобиль (готовые 3D-модели)." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.scene.spawn('vehicle:car', opts)</b> — создать машину:
                        модель, цвет, имя (в подсказке), параметры (скорость/руль/тормоз);</li>
                    <li><b>вход/выход</b> — у машины автоматически появляется сиденье:
                        подошёл → держишь F → за рулём; E — выйти;</li>
                    <li><b>аркадная физика</b> — газ/тормоз/реверс + повороты (на скорости),
                        столкновения с миром, передние колёса доворачивают;</li>
                    <li><b>камера машины</b> — следует за авто; V циклит follow / капот /
                        кинематографичный ракурс;</li>
                    <li><b>HUD водителя</b> — спидометр (км/ч) и передача появляются сами;</li>
                    <li><b>onVehicleEnter / onVehicleExit</b> — события для логики (квесты,
                        деньги за поездку).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Заспавнить машину</h3>
                <p>
                    Одна строка создаёт готовый к езде автомобиль. <code>model</code> —
                    одна из встроенных 3D-моделей (car-taxi / car-sedan / car-truck /
                    car-suv-luxury и др.), <code>name</code> показывается в подсказке F.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.scene.spawn('vehicle:car', {
  x: -28, y: 0.5, z: -22, rotationY: 0,
  model: 'car-taxi', color: '#ffd23a', name: 'Natsune Alltima',
  params: { maxSpeed: 14, turnSpeed: 1.8, enginePower: 16, brake: 28 },
});`}</Code>
                <Note>
                    Сиденье водителя создаётся автоматически — отдельно регистрировать
                    «вход» не нужно. Подошёл к машине → подсказка «[F] Enter» с её именем.
                    Управление: <b>W</b> газ, <b>S</b> тормоз/назад, <b>A/D</b> руль,
                    <b> Space</b> ручник, <b>V</b> камера, <b>E</b> выйти.
                </Note>

                <h3 className="lessonH">Шаг 2. Реакция на посадку (квесты, деньги)</h3>
                <p>
                    Глобальные события <code>onVehicleEnter</code> / <code>onVehicleExit</code>
                    дают зацепку для логики игры — например, показать инструкцию или
                    начать отсчёт заказа такси.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.onVehicleEnter((vehicleRef) => {
  game.ui.set('hint', 'Поехали! Отвези клиента.', { x: 50, y: 88, anchor: 'bottom' });
});
game.onVehicleExit((vehicleRef) => {
  game.ui.set('hint', 'Ты вышел из машины.', { x: 50, y: 88, anchor: 'bottom' });
});`}</Code>

                <Shot src="guide-taxisim-scene.png" wide
                      caption="Гараж с машинами: подсказка «[F] Enter» появляется у автомобиля, когда игрок рядом. Вокруг — город из многоэтажек, фонарей и дорог." />

                <h3 className="lessonH">Шаг 3. Hold-to-action (защита от случайных нажатий)</h3>
                <p>
                    Любое взаимодействие можно сделать «по удержанию» — игрок должен
                    подержать клавишу, а не случайно ткнуть. Полезно для важных действий
                    (подобрать клиента, продать машину). Опция <code>holdDuration</code> в
                    <code> onInteract</code>:
                </p>
                <ScriptKind kind="object" />
                <Code>{`game.self.onInteract(() => {
  game.ui.set('hint', 'Клиент сел!', { x: 50, y: 88, anchor: 'bottom' });
}, { text: 'Подобрать клиента', distance: 5, key: 'f', holdDuration: 0.5 });`}</Code>

                <h3 className="lessonH">Что движок делает сам</h3>
                <p>
                    Тебе не нужно писать «низкоуровневую» возню — движок берёт её на себя:
                </p>
                <ul>
                    <li><b>Машина сама встаёт на землю.</b> Где бы ты ни задал спавн,
                        авто опускается на пол/дорогу — не висит в воздухе и не тонет;</li>
                    <li><b>Звук мотора.</b> Пока едешь — слышен живой рокот двигателя:
                        чем быстрее, тем плотнее и громче. На стоянке — тихо;</li>
                    <li><b>Водитель скрывается за рулём</b> и появляется сбоку при выходе;</li>
                    <li><b>Падение в бездну</b> = автоматический выход + респавн на старте.</li>
                </ul>

                <h3 className="lessonH">Почему это важно</h3>
                <p>
                    Машина — отдельная подсистема: пока ты за рулём, WASD управляют
                    <i> автомобилем</i>, а не ходьбой, и камера автоматически переключается
                    на «погоню за машиной». Выход возвращает обычное управление. На этой
                    базе строятся целые игры: такси, гонки, доставка, мафия-симулятор.
                </p>

                <Try>
                    Заспавни вторую машину другой модели (<code>model: 'car-suv-luxury'</code>)
                    с другим цветом и параметрами (быстрее: <code>maxSpeed: 20</code>).
                    Поставь рядом столб (<code>game.scene.spawn('primitive:cylinder', ...)</code>)
                    и проверь — машина останавливается, столб стоит.
                </Try>
            </>
        ),
    },

    'guide-skybox': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    <b>Красивое небо в одну строку.</b> Вместо плоского цветного фона —
                    градиентный купол с солнцем, плывущими облаками, дымкой у горизонта
                    и далёкими горами (low-poly стиль, как в топовых Roblox-играх). Небо
                    меняется кнопками: <b>день</b>, <b>закат</b>, <b>звёздная ночь</b>,
                    <b> космос</b> — с плавным переходом за пару секунд. И главное: небо —
                    это <b>единый источник света</b>: меняешь пресет → вместе с небом
                    меняется и освещение всей сцены (на закате теплеет, ночью темнеет).
                </p>

                <Shot src="guide-skybox-scene.png" wide
                      caption="Дневное небо (пресет «Lowpoly»): голубой градиент, облака, дымка у горизонта, далёкие горы — town выглядит живым." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.scene.setSkybox({'{'} preset {'}'})</b> — поставить готовое небо
                        одной строкой (6 пресетов);</li>
                    <li><b>game.scene.setClouds(...)</b> — облака: плотность, скорость дрейфа, цвет;</li>
                    <li><b>game.scene.setFog(...)</b> — атмосферный туман: дальние объекты выцветают в небо;</li>
                    <li><b>game.scene.skybox.fadeTo(opts, сек)</b> — плавный переход между небесами;</li>
                    <li><b>game.scene.skybox.setSunDirection(...)</b> — двигать солнце (анимация дуги).</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Поставить небо</h3>
                <p>
                    Самое простое — выбрать пресет. Доступны:
                    <code>clear-summer-day</code>, <code>lowpoly-roblox</code>,
                    <code>cloudy</code>, <code>sunset</code>, <code>starry-night</code>,
                    <code>space</code>.
                </p>
                <ScriptKind kind="global" />
                <Code>{`// Голубое low-poly небо с облаками, дымкой и горами (как на скрине):
game.scene.setSkybox({ preset: 'lowpoly-roblox' });
game.scene.setClouds({ enabled: true, cover: 0.45, speed: 0.014 });
game.scene.setFog({ color: '#e2eef7', density: 0.005 });`}</Code>
                <Note>
                    Небо само выставляет освещение сцены под выбранный пресет — отдельно
                    свет настраивать не нужно. Купол бесконечно далёкий, поэтому ходить
                    «до края неба» нельзя — оно всегда вокруг игрока.
                </Note>

                <h3 className="lessonH">Шаг 2. Плавная смена неба (день → закат → ночь)</h3>
                <p>
                    <code>skybox.fadeTo</code> переводит небо к новому пресету за указанное
                    число секунд — цвета купола, солнце, облака, туман и свет сцены
                    меняются плавно. Удобно вешать на кнопки или события.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.gui.onClick('btn-sunset', () => game.scene.skybox.fadeTo({ preset: 'sunset' }, 2));
game.gui.onClick('btn-night',  () => game.scene.skybox.fadeTo({ preset: 'starry-night' }, 2));
game.gui.onClick('btn-space',  () => game.scene.skybox.fadeTo({ preset: 'space' }, 2));`}</Code>

                <Shot src="guide-skybox-night.png" wide
                      caption="Пресет «Звёздная ночь»: тёмно-синий купол со звёздами, свет сцены приглушён — town погружается в ночь. Переход плавный (fadeTo)." />

                <Shot src="guide-skybox-space.png" wide
                      caption="Пресет «Космос»: почти чёрное звёздное небо, туман отключён — дальние объекты видны чётко." />

                <h3 className="lessonH">Шаг 3. Своё небо (gradient)</h3>
                <p>
                    Можно не брать пресет, а задать цвета купола вручную — верх, низ,
                    горизонт и солнце.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.scene.setSkybox({
  mode: 'gradient',
  topColor: '#3d7fe0',      // зенит
  bottomColor: '#dcebf7',   // у земли
  horizonColor: '#bcd9f2',  // линия горизонта
  sunDirection: { x: 0.3, y: 0.85, z: 0.4 },
  sunColor: '#fff6d8',
  sunSize: 0.035,
});`}</Code>

                <h3 className="lessonH">Почему это важно</h3>
                <p>
                    Небо — половина визуального впечатления от мира. С плоским фоном все
                    игры выглядят одинаково и дёшево; с кастомным небом — атмосферно и
                    «дорого». А связка неба с освещением даёт бесплатный приём: смена
                    времени суток одной строкой мгновенно меняет настроение всей сцены.
                </p>

                <Try>
                    Сделай день/ночь цикл: по таймеру каждые 10 секунд переключай
                    <code>fadeTo</code> между <code>'clear-summer-day'</code> и
                    <code>'starry-night'</code>. Добавь облака погуще на день и убери на ночь.
                </Try>
            </>
        ),
    },

    'guide-leaderstats': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Игра «собери монеты» с двумя системами удержания, как в Roblox:
                    <b> таблица лидеров</b> справа-сверху (монеты, время, уровень) и
                    <b> достижения</b> — всплывающие награды с редкостью, звуком и
                    страницей-витриной. Прогресс <b>сохраняется в базе</b> — закрыл
                    игру, вернулся завтра, а монеты и открытые ачивки на месте.
                </p>

                <Shot src="guide-leaderstats-play.png" wide
                      caption="Таблица лидеров справа-сверху + всплывающая плашка достижения «Терпеливый» при игре. Слева-снизу — кубок, открывающий страницу всех достижений." />

                <h3 className="lessonH">Чему научишься</h3>
                <ul>
                    <li><b>game.leaderstats.define(name, opts)</b> — столбец таблицы:
                        иконка, цвет, формат (число / время / 1.2K), primary (по нему сортировка);</li>
                    <li><b>game.leaderstats.me.add('Монеты', 1)</b> — изменить стат игрока
                        (ячейка жёлто мигает);</li>
                    <li><b>game.achievements.define([...])</b> — объявить достижения
                        (id, название, описание, редкость, очки, скрытое);</li>
                    <li><b>game.achievements.unlock(id)</b> — выдать достижение (плашка + звук);</li>
                    <li><b>game.achievements.bindToStat(id, 'Монеты', {'{'} gte: 10 {'}'})</b> —
                        авто-награда при достижении значения стата;</li>
                    <li>прогресс сохраняется в БД и подгружается в новой сессии.</li>
                </ul>

                <h3 className="lessonH">Шаг 1. Таблица лидеров</h3>
                <p>
                    Объяви столбцы. Первый <code>primary: true</code> — по нему сортируются
                    игроки в топе. Дальше меняй значения через <code>me.add / me.set</code>.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.leaderstats.define('Монеты',  { initial: 0, format: 'number', icon: '🪙', color: '#ffd23a', primary: true });
game.leaderstats.define('Время',   { initial: 0, format: 'time',   icon: '⏱', color: '#7fd0ff' });
game.leaderstats.define('Уровень', { initial: 1, format: 'number', icon: '⭐', color: '#b48bff' });

// Время идёт само, монеты — за подбор
game.every(1, () => game.leaderstats.me.add('Время', 1));
game.leaderstats.me.add('Монеты', 1);`}</Code>

                <h3 className="lessonH">Шаг 2. Достижения</h3>
                <p>
                    Объяви список достижений с редкостью (<code>common / rare / epic /
                    legendary</code> — разный цвет плашки и звук). Выдавай явно через
                    <code>unlock</code> или автоматически через <code>bindToStat</code>.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.achievements.define([
  { id:'first_coin',  name:'Первая монета', description:'Подобрать монету', icon:'🪙', rarity:'common', points:5 },
  { id:'fifty_coins', name:'Полная сумка',  description:'Собрать 50 монет', icon:'💰', rarity:'rare',   points:25 },
]);

// Авто-награда: как только Монеты >= 50 — плашка «Полная сумка» сама выедет
game.achievements.bindToStat('fifty_coins', 'Монеты', { gte: 50 });

// Явная выдача (на первой монете)
game.achievements.unlock('first_coin');`}</Code>

                <p>
                    Кубок слева-снизу открывает страницу <b>«Мои достижения»</b>:
                    открытые — цветные с рамкой по редкости, закрытые — серые с
                    замком, скрытые — «?», сверху прогресс-бар «N из M (очки)».
                </p>

                <Note>
                    Прогресс игрока (значения статов + открытые достижения) автоматически
                    сохраняется в базу и подгружается при следующем входе — ничего
                    дописывать не нужно. Уже открытое достижение второй раз плашкой не
                    показывается (оно «навсегда»).
                </Note>

                <h3 className="lessonH">Почему это важно</h3>
                <p>
                    Лидерборды и достижения — главный механизм удержания: ребёнок
                    возвращается в игру за новым рекордом и новой ачивкой. Это основа
                    симуляторов, ферм и PvP — в Roblox столбец «Coins / Wins / Level»
                    есть почти в каждой игре.
                </p>

                <Try>
                    Добавь стат «Рекорд» и достижение <code>'speedrun'</code>, которое
                    выдаётся через <code>bindToStat('Время', {'{'} lte: 30 {'}'})</code>,
                    если собрать все монеты быстрее 30 секунд.
                </Try>
            </>
        ),
    },

    'guide-floaters': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Мини-шутер: волны <b>зомби бегут к игроку</b>, ты отстреливаешь
                    их из <b>бластера</b>, а над каждой целью всплывает <b>облачко
                    урона</b> — как в Roblox-RPG (Pet Sim, Anime Adventures). Зомби
                    гибнут, счётчик растёт, волны усиливаются.
                </p>

                <Shot src="guide-floaters-scene.png" wide
                      caption="Зомби сбегаются к игроку, бластер стреляет — над целью всплывают красные облачка урона «-25»." />

                <h3 className="lessonH">Шаг 1. Бластер + авто-облачка над мобами</h3>
                <p>
                    Две строки превращают игру в шутер с фидбеком урона: выдаём
                    бластер и включаем <b>авто-floater</b> — теперь <i>любой</i> урон
                    по NPC сам рисует «-N» над целью, вручную вызывать ничего не надо.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.player.giveTool('blaster-blaster-a', { equip: true }); // бластер в руки
game.fx.autoMobFloaters(true);   // облачко урона над любым мобом при попадании`}</Code>

                <h3 className="lessonH">Шаг 2. Волны зомби, идущих к игроку</h3>
                <Code>{`function spawnWave(n){
  const pl = game.player.position;
  for (let i = 0; i < n; i++){
    const a = (i / n) * Math.PI * 2;
    const e = game.scene.spawnNpc('skin_retro-zombie', {
      x: pl.x + Math.cos(a)*18, z: pl.z + Math.sin(a)*18,
      name: 'Зомби', hp: 100, speed: 2.6,
    });
    if (e && e.follow) e.follow('player');   // зомби преследует игрока
  }
}
game.after(1.5, () => spawnWave(5));
game.every(14, () => spawnWave(8));`}</Code>
                <p>
                    Стрелять из бластера — ЛКМ. В режиме от 3-го лица пуля летит
                    <b> туда, куда кликнул</b> курсором. Попал по зомби → облачко
                    урона (благодаря <code>autoMobFloaters</code>), убил → засчитан.
                </p>

                <h3 className="lessonH">Ручной floater — все типы</h3>
                <p>Когда нужен полный контроль — рисуй цифру сам:</p>
                <Code>{`game.fx.damageFloater(pos, 25);                  // красный — обычный урон
game.fx.damageFloater(pos, 80, { isCrit: true }); // жёлтый, больше + подскок
game.fx.damageFloater(pos, 30, { isHeal: true }); // зелёный — лечение (+30)
game.fx.damageFloater(pos, 50, { isMana: true }); // синий — мана
game.fx.damageFloater(pos, 'Промах', { isMiss: true }); // серый текст`}</Code>
                <p>
                    <b>position</b> — <code>{'{x,y,z}'}</code>, ссылка на объект или
                    <code>'player'</code>; <b>value</b> — число или строка.
                </p>

                <h3 className="lessonH">Стек и комикс-стиль</h3>
                <Code>{`// общий stackKey → удары сливаются в «-25 ×N» вместо кучи цифр
game.fx.damageFloater(enemy.position, 25, { stackKey: 'aoe_' + enemy.id });
// comicStyle → BAM! (>50), KAPOW! (>100), POW! (крит) на жёлтой звезде
game.fx.damageFloater(pos, 120, { comicStyle: true });`}</Code>

                <Note>
                    Под капотом — пул из 30 переиспользуемых билборд-планов
                    (object pool), поэтому даже при толпе зомби и спаме цифр FPS не
                    проседает. Цифры всегда поверх геометрии и повёрнуты к камере.
                </Note>

                <h3 className="lessonH">Почему это важно</h3>
                <p>
                    Без облачек урона стрельба ощущается «впустую». Это базовый
                    боевой фидбек: игрок видит, сколько нанёс, был ли крит, попал ли.
                    Связка <code>бластер + autoMobFloaters + волны NPC</code> — готовый
                    каркас любого шутера/выживания.
                </p>

                <Try>
                    Сделай «огненный» урон: <code>damageFloater(pos, 15, {'{'} color:
                    '#ff7a2a' {'}'})</code> каждые 0.5 сек 3 раза — эффект горения.
                    Или увеличь HP зомби и добавь крит каждый 5-й выстрел.
                </Try>
            </>
        ),
    },

    'guide-inventory': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    Полноценный инвентарь как в Minecraft и RPG: <b>сетка 8×5</b> +
                    <b> хотбар на 9 слотов</b>, предметы со <b>стаками</b> и
                    <b> редкостями</b>, перетаскивание мышью, ПКМ-меню, всплывающие
                    подсказки. Собираешь предметы на поляне — стаки растут, открываешь
                    инвентарь клавишей <b>I</b> и раскладываешь добычу.
                </p>

                <Shot src="guide-inventory-scene.png" wide
                      caption="Окно инвентаря (I): сетка 8×5 + хотбар 9. Стаки (×12 ягод), цвет рамки = редкость (голубая rare, зелёная uncommon)." />

                <h3 className="lessonH">Шаг 1. Определи предметы</h3>
                <p>
                    Каждый предмет описывается один раз: имя, иконка-эмодзи, редкость,
                    размер стака, эффект использования.
                </p>
                <ScriptKind kind="global" />
                <Code>{`game.items.define([
  { id:'berry',  name:'Ягоды',  emoji:'🍓', rarity:'common',    maxStack:16, value:2 },
  { id:'iron',   name:'Руда',   emoji:'⛏️', rarity:'uncommon',  maxStack:16, value:8 },
  { id:'potion', name:'Зелье',  emoji:'🧪', rarity:'rare',      maxStack:8,  onUseEffect:'heal:50' },
  { id:'sword',  name:'Меч',    emoji:'⚔️', rarity:'legendary', maxStack:1,  value:500 },
]);`}</Code>
                <p>
                    Редкости: <code>common</code> (серый), <code>uncommon</code>
                    (зелёный), <code>rare</code> (голубой), <code>epic</code>
                    (фиолетовый), <code>legendary</code> (золотой) — это цвет рамки слота.
                </p>

                <h3 className="lessonH">Шаг 2. Выдавай и собирай предметы</h3>
                <Code>{`game.inventory.give('sword', 1);     // в стартовый набор
game.inventory.give('berry', 5);     // стак до maxStack, дальше — новый слот

// сбор предмета с земли (на объекте-ягоде):
game.self.onInteract(() => {
  game.inventory.give('berry', 2);
  game.self.delete();                // убрать собранный предмет
}, { text:'Собрать', key:'e', distance:3 });`}</Code>
                <p>
                    Собранное попадает <b>сначала в хотбар</b> (виден внизу экрана),
                    одинаковые предметы складываются в стак с учётом <code>maxStack</code>.
                </p>

                <Shot src="guide-inventory-play.png" wide
                      caption="Хотбар внизу наполняется собранным (меч, яблоки ×3, зелье). Подсказка «E Собрать» у ближайшего предмета." />

                <h3 className="lessonH">Шаг 3. Окно инвентаря</h3>
                <ul>
                    <li><b>I</b> — открыть/закрыть окно (Esc тоже закрывает);</li>
                    <li><b>Перетаскивание</b> мышью — поменять слоты местами или
                        слить стаки;</li>
                    <li><b>ПКМ</b> по слоту — меню: использовать / разделить / выбросить;</li>
                    <li><b>Наведение</b> — tooltip (название цветом редкости, описание, цена);</li>
                    <li><b>Сорт.</b> — расставить по редкости;</li>
                    <li><b>1-9</b> — выбрать активный слот хотбара.</li>
                </ul>

                <Note>
                    Всё хранится в движке и сериализуется в проект автоматически —
                    дописывать сохранение не нужно. Предметы с тегом
                    <code>'quest'</code> нельзя выбросить.
                </Note>

                <h3 className="lessonH">Почему это важно</h3>
                <p>
                    Инвентарь — несущая конструкция RPG, выживания и симуляторов.
                    Сетка + хотбар + стаки + редкости — стандарт, который игроки
                    узнают мгновенно. Сочетается с крафтом, квестами и магазином.
                </p>

                <Try>
                    Добавь предмет <code>'apple'</code> с
                    <code> onUseEffect:'heal:15'</code>, положи в хотбар и нажми ПКМ →
                    «Использовать» — HP восстановится, яблоко убавится на 1.
                </Try>
            </>
        ),
    },

    'guide-loadingscreen': {
        body: (
            <>
                <h3 className="lessonH">Что получится</h3>
                <p>
                    <b>Красивый экран загрузки игры</b> — то, что видит игрок при входе
                    в игру (после клика «Играть»), пока грузится сцена. Композиция как в
                    Roblox: размытый фон с медленным движением (<b>Ken Burns</b>),
                    <b> карточка-витрина</b> по центру, крупное <b>название места</b> и
                    <b> автор с verified-галочкой</b>, прогресс-бар и спиннер. Когда сцена
                    загрузилась — экран плавно исчезает.
                </p>

                <Shot src="guide-loadingscreen-scene.png" wide
                      caption="Экран загрузки игры: размытый Ken-Burns фон, карточка-витрина, «Открыть фабрику», автор с verified-галочкой, прогресс и спиннер." />

                <h3 className="lessonH">Шаг 1. Настроить в свойствах проекта</h3>
                <p>
                    Без кода: <b>Настройки игры → вкладка «Стартовый экран входа (Ken Burns)»</b>.
                    Задай фон (размытое изображение игры), карточку, название места, имя
                    автора, галочку verified, стиль анимации и длительность. Этот экран
                    автоматически покажется игроку при заходе.
                </p>
                <ul>
                    <li><b>Фон</b> — размытое изображение игры (или её обложка);</li>
                    <li><b>Карточка</b> — витрина по центру (необязательно);</li>
                    <li><b>Название места</b> + <b>автор</b> + <b>verified</b>;</li>
                    <li><b>Стиль:</b> Ken Burns / статичный / параллакс / частицы;</li>
                    <li><b>Длительность</b> и <b>прогресс-бар</b>.</li>
                </ul>
                <p>
                    Если ничего не задано — экран всё равно красивый: берёт обложку,
                    название и автора игры автоматически.
                </p>

                <h3 className="lessonH">Шаг 2. Переходы между мирами из скрипта</h3>
                <p>Для смены главы/мира вызывай экран вручную:</p>
                <ScriptKind kind="global" />
                <Code>{`game.loading.show({
  style: 'particles',
  placeName: 'Алмазная глава',
  studioName: 'Виктория — Майнкрафтия',
  verified: true,
  duration: 2,
});
game.after(0.6, () => {
  game.environment.setTimeOfDay(0);       // меняем мир «за кулисами»
  game.environment.setSkyColor('#0a1024');
});
game.loading.onHide(() => {
  game.ui.set('hi', 'Добро пожаловать!', { x:50, y:6, anchor:'top' });
});`}</Code>

                <Note>
                    Стили: <b>Ken Burns</b> — медленный pan+zoom фона (классика Roblox);
                    <b> параллакс</b> — фон смещается за мышью; <b>частицы</b> — летящие
                    искры; <b>статичный</b> — без анимации. Verified-галочка — синий кружок
                    с белым чеком рядом с автором.
                </Note>

                <Try>
                    Открой настройки игры → «Стартовый экран», впиши название места и автора,
                    выбери стиль «Частицы» — запусти игру и посмотри, как экран загрузки
                    встречает игрока.
                </Try>
            </>
        ),
    },

};

/** Есть ли готовый текст урока для игры с таким id. */
export function hasLesson(id) {
    return !!LESSONS[id];
}
