studio/src/community/docsLessons.jsx
МИН 42be04def9
Some checks failed
CI / Lint (pull_request) Failing after 1m10s
CI / Build (pull_request) Successful in 2m2s
CI / Secret scan (pull_request) Successful in 2m36s
CI / PR size check (pull_request) Successful in 6s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
feat(week4): модальные сцены, кастомные скины игрока и вики-гайды по 4 играм
Задача 04 — модальные сцены (затемнение + GUI/3D-анимация + блок ввода):
- engine/ModalManager.js (state-машина, fade, spotlight-проекция, HighlightLayer)
- editor/ModalOverlay.jsx (CSS-overlay + mask-image для spotlight)
- PlayerController.setInputBlocked/setCameraFrozen/captureCameraState/focusOnTarget
- game.modal.open/close/update/isOpen/onClose + пресеты bossIntro/lootbox/dialog/confirmation
- Фиксы ядра: клик GUI-кнопок (realId↔localId), modalOpened через globalEvent,
  guard от двойного срабатывания колбэков, кнопка ✓ на последней строке диалога

Задача 07 — кастомные скины игрока + магазин:
- non-humanoid скины (любая 3D-модель): загрузчик, процедурная анимация,
  настраиваемый AABB, центрирование через pivot-node
- PlayerController.reloadSkin (смена скина в Play)
- game.player.setSkin/unlockSkin/getAvailableSkins/onSkinChange/openSkinShop + локальная валюта
- editor/SkinShopOverlay.jsx (встроенный магазин, клавиша B) + SkinManagerModal.jsx (вкладка «Скины»)
- сериализация scene.skins, кнопка «Скины» в тулбаре

Вики — категория «Разбор готовых игр»:
- docsGames.js: группа g5 + 4 карточки (Двор/Витрина GUI/Сундук/Парк)
- docsLessons.jsx: 4 подробные статьи-урока для детей
- «Открыть мою копию» создаёт копию проекта (оригинал не трогается)

Адаптивная ширина кнопки billboard под длинный текст.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 00:50:56 +03:00

7785 lines
441 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { Code, ScriptKind, Step, Note, Try, Shot } from './docsData';
/**
* 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" />
<Code>{`// === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт ===
// Этот скрипт глобальный: считает собранные монетки и проверяет победу.
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 });
}
});`}</Code>
<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="каждую монетку" />
<Code>{`// === Скрипт монетки ===
// game.self — это сама монетка, на которой висит скрипт.
game.self.onTouch(() => {
// игрок коснулся монетки — сообщаем главному скрипту
game.broadcast('coin');
game.self.delete(); // монетка исчезает со сцены
});`}</Code>
<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" />
<Code>{`// === ИГРА «ПРЫГАЙ ПО ПЛАТФОРМАМ» — главный скрипт ===
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 });
});`}</Code>
<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="зелёную финишную площадку" />
<Code>{`// === Скрипт финиша ===
// Висит на невидимой зоне над зелёной площадкой.
// Игрок встал на площадку — его тело внутри зоны — победа.
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «НЕ УПАДИ» — главный скрипт ===
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 });
});`}</Code>
<h3 className="lessonH">Шаг 3. Скрипт исчезающей плитки</h3>
<p>
Самое главное повесить на <b>каждую плитку</b> скрипт,
который убирает её через секунду после касания.
</p>
<ScriptKind kind="object" on="каждую плитку" />
<Code>{`// === Скрипт исчезающей плитки ===
let triggered = false; // плитка уже запущена на исчезновение?
game.self.onTouch(() => {
if (triggered) return; // если уже запущена — выходим
triggered = true;
game.sound.play('click');
// через 1.2 секунды плитка пропадает
game.after(1.2, () => {
game.self.delete();
});
});`}</Code>
<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" />
<Code>{`// === ИГРА «КНОПКА-ОТКРЫВАШКА» — главный скрипт ===
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 });
});`}</Code>
<h3 className="lessonH">Шаг 4. Скрипт кнопки главное</h3>
<p>
Скрипт кнопки делает всю работу: ловит нажатие E и
открывает дверь.
</p>
<ScriptKind kind="object" on="красную кнопку" />
<Code>{`// === Скрипт кнопки ===
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 // на сколько метров подойти
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «ЛАБИРИНТ» — главный скрипт ===
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 });
});`}</Code>
<ScriptKind kind="object" on="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «ЦВЕТНЫЕ ПЛИТКИ» — главный скрипт ===
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 });
}
});`}</Code>
<Note>
Замени число <code>36</code> на столько плиток, сколько
реально поставил. Если сетка 5×5 будет 25.
</Note>
<h3 className="lessonH">Шаг 3. Скрипт плитки</h3>
<ScriptKind kind="object" on="каждую плитку" />
<Code>{`// === Скрипт цветной плитки ===
let painted = false; // плитка уже раскрашена?
game.self.onTouch(() => {
if (painted) return;
painted = true;
// меняем цвет плитки на ярко-зелёный
game.scene.setColor(game.self.ref, '#33dd55');
game.broadcast('paint'); // сообщаем главному скрипту о покраске
});`}</Code>
<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" />
<Code>{`// === ИГРА «ПОЙМАЙ ПАДАЮЩЕЕ» — главный скрипт ===
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 });
}
});`}</Code>
<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" />
<Code>{`// === ИГРА «БЕГИ К ФИНИШУ» — главный скрипт ===
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 });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о финише
});`}</Code>
<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" />
<Code>{`// === ИГРА «СВЕТОФОР» — главный скрипт ===
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 });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «ПРЫЖОК-ПРУЖИНА» — главный скрипт ===
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 });
});`}</Code>
<h3 className="lessonH">Шаг 4. Скрипт батута</h3>
<p>Повесь этот скрипт на <b>каждый</b> батут он одинаковый.</p>
<ScriptKind kind="object" on="каждый батут" />
<Code>{`// === Скрипт батута ===
// Игрок встал на батут — мощный подброс вверх.
game.self.onTouch(() => {
game.player.boostJump(3.2); // 3.2 = в 3 раза выше обычного прыжка
game.sound.play('jump');
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «ЭХО-КОМНАТА» — главный скрипт ===
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 });
});`}</Code>
<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="каждую цветную плитку" />
<Code>{`// === Скрипт звуковой плитки ===
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'); // сообщаем главному скрипту о новой плитке
}
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о финише
});`}</Code>
<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" />
<Code>{`// === ИГРА «ДВЕРЬ ПО КОДУ» — главный скрипт ===
// СЕКРЕТНЫЙ КОД — порядок кнопок. Поменяй на свой!
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 });
});`}</Code>
<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 (на остальных — поменяй цифру)" />
<Code>{`// === Скрипт кнопки-цифры 1 ===
game.self.onInteract(() => {
// сообщаем главному скрипту номер нажатой кнопки
game.broadcast('press', { num: 1 }); // ← номер кнопки
}, {
text: 'Нажать кнопку 1',
distance: 3
});`}</Code>
<p>
Для «Кнопки_2» поставь <code>{'{ num: 2 }'}</code> и текст
«Нажать кнопку 2», и так далее.
</p>
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
<ScriptKind kind="object" on="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «ТОРГОВЕЦ» — главный скрипт ===
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 });
});`}</Code>
<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="прилавок" />
<Code>{`// === Скрипт прилавка ===
game.self.onInteract(() => {
game.broadcast('talk'); // сообщаем главному скрипту: говорим с торговцем
}, {
text: 'Поговорить с торговцем',
distance: 4
});`}</Code>
<h3 className="lessonH">Шаг 4. Скрипт двери</h3>
<ScriptKind kind="object" on="дверь" />
<Code>{`// === Скрипт двери ===
game.self.onInteract(() => {
game.broadcast('openDoor'); // сообщаем главному скрипту: открыть дверь
}, {
text: 'Открыть дверь',
distance: 4
});`}</Code>
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
<ScriptKind kind="object" on="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «СОБЕРИ ПО ТЕГАМ» — главный скрипт ===
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 });
}
});`}</Code>
<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="каждую звезду" />
<Code>{`// === Скрипт звезды ===
game.self.onTouch(() => {
// снимаем тег и удаляем звезду
game.scene.untag(game.self.ref, 'звезда');
game.self.delete();
game.broadcast('collected'); // сообщаем главному скрипту о сборе
});`}</Code>
<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" />
<Code>{`// === ИГРА «ТИР» — главный скрипт ===
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 });
}
});`}</Code>
<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="каждую мишень" />
<Code>{`// === Скрипт мишени ===
// Клик по 3D-объекту = выстрел в него.
game.self.onClick(() => {
// взрыв искр на месте мишени
game.scene.spawnParticles('explosion', game.self.position,
{ count: 1, color: '#ff6633' });
game.self.delete(); // мишень сбита
game.broadcast('hit'); // сообщаем главному скрипту о попадании
});`}</Code>
<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" />
<Code>{`// === ИГРА «ЛАВА-ПОЛ» — главный скрипт ===
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 });
});`}</Code>
<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="невидимую зону «ЛаваЗона»" />
<Code>{`// === Скрипт лавы ===
// Игрок коснулся лавы — урон. У damage есть защита (i-frames),
// так что урон не каждый кадр, а раз в ~0.5 секунды.
game.self.onTouch(() => {
game.player.damage(20);
game.sound.play('hit');
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}</Code>
<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" />
<Code>{`// === ИГРА «КЛЮЧ И СУНДУК» — главный скрипт ===
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 });
});`}</Code>
<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="ключ" />
<Code>{`// === Скрипт ключа ===
game.self.onTouch(() => {
game.broadcast('takeKey'); // сообщаем главному скрипту: ключ найден
game.self.delete(); // ключ подобран
});`}</Code>
<p>
Игрок коснулся ключа скрипт шлёт сообщение
<code> game.broadcast('takeKey')</code> (главный скрипт
положит ключ в инвентарь), а <code>game.self.delete()</code>
убирает ключ с поляны: он же теперь в рюкзаке.
</p>
<h3 className="lessonH">Шаг 5. Скрипт сундука</h3>
<ScriptKind kind="object" on="сундук" />
<Code>{`// === Скрипт сундука ===
game.self.onInteract(() => {
game.broadcast('openChest'); // сообщаем главному скрипту: открыть сундук
}, { text: 'Открыть сундук', distance: 4 });`}</Code>
<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" />
<Code>{`// === ИГРА «КАЧЕЛИ» — главный скрипт ===
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 });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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" />
<Code>{`// === ИГРА «ЛИФТ» — главный скрипт ===
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 });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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" />
<Code>{`// === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт ===
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 });
}
});
});`}</Code>
<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" />
<Code>{`// === ИГРА «ПРЕСЛЕДОВАТЕЛЬ» — главный скрипт ===
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 });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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" />
<Code>{`// === ИГРА «ЗОНА ОПАСНОСТИ» — главный скрипт ===
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 });
});`}</Code>
<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="красную зону" />
<Code>{`// === Скрипт зоны опасности ===
// onTouch — игрок вошёл, onUntouch — вышел.
game.self.onTouch(() => {
game.broadcast('zone-enter');
});
game.self.onUntouch(() => {
game.broadcast('zone-leave');
});`}</Code>
<p>
Здесь важна <b>пара событий</b>: <code>onTouch</code>
срабатывает, когда игрок входит в зону, а
<code> onUntouch</code> когда выходит. Так зона точно
знает, внутри игрок или нет.
</p>
<h3 className="lessonH">Шаг 4. Скрипт аптечки</h3>
<ScriptKind kind="object" on="зелёную аптечку" />
<Code>{`// === Скрипт аптечки ===
game.self.onTouch(() => {
game.player.heal(60);
game.ui.showText('+60 HP', 1.5);
game.sound.play('pickup');
game.self.delete();
});`}</Code>
<p>
<code>game.player.heal(60)</code> добавляет игроку
60 единиц здоровья. Аптечку взяли один раз
<code> game.self.delete()</code> убирает её.
</p>
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
<ScriptKind kind="object" on="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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" />
<Code>{`// === ИГРА «ПЕРЕКЛЮЧАТЕЛИ» — главный скрипт ===
// правильный порядок рычагов
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 });
});`}</Code>
<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" />
<Code>{`// === Скрипт рычага 1 ===
game.self.onInteract(() => {
game.broadcast('lever', { num: 1 });
}, { text: 'Дёрнуть рычаг 1', distance: 3 });`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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" />
<Code>{`// === ИГРА «ПАДАЮЩИЙ МОСТ» — главный скрипт ===
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 });
});`}</Code>
<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="каждую доску" />
<Code>{`// === Скрипт доски моста ===
let cracking = false;
game.self.onTouch(() => {
if (cracking) return;
cracking = true;
game.sound.play('click');
game.after(1, () => { game.self.delete(); });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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" />
<Code>{`// === ИГРА «КАМЕРА-ОБЛЁТ» — главный скрипт ===
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 });
});`}</Code>
<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="зелёный финиш" />
<Code>{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}</Code>
<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. */
export function hasLesson(id) {
return !!LESSONS[id];
}