studio/src/community/docsGamesBuilders.js
МИН 61fba4e174
Some checks failed
CI / Lint + Format (push) Failing after 32s
CI / Build (push) Failing after 37s
CI / Secret scan (push) Failing after 37s
CI / PR size check (push) Has been skipped
fix: починка билда + studio.rublox.pro инфра
Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026).
Содержит изменения которые делались в процессе подготовки прод-окружения:

Фиксы импортов после выноса из minecraftia:
- Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/)
- Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/
- API.js скопирован из минки целиком (было 8 экспортов, стало 312)
- Добавлены PLAYER_URL, MyButton_1, недостающие компоненты
- Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require)

Структура ассетов:
- public/kubikon-templates/ → public/assets/kubikon-templates/
- public/kubikon-learn/ → public/assets/kubikon-learn/
- (код искал в /assets/, файлы лежали без /assets/)

Навигация роутов внутри студии:
- /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced)
- /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X

UI:
- Новый компонент StudioHeader (61px, как в минке) + копия favicon
- WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера
- SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке)
- Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык)

Документация:
- docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR
- docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API
- API_USAGE.md — список эндпоинтов backend
- README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/

.gitignore:
- public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 05:01:13 +03:00

6166 lines
243 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.

/**
* docsGamesBuilders.js — генераторы project_data для игр-уроков вики (раздел K).
*
* Каждая функция-билдер возвращает объект, совместимый с
* BabylonScene.serialize() — как в templates.js. Игра-урок НЕ хранится
* в БД отдельной записью: это чистый код-генератор. Когда ученик жмёт
* «Открыть игру в редакторе», вика вызывает createProject со свежим
* project_data из билдера → создаётся НОВАЯ запись на ученика. Так
* исходник урока физически нельзя испортить — он живёт в этом коде,
* а каждый получает свою редактируемую копию.
*
* Ключ билдера в объекте GAME_BUILDERS совпадает с id игры в docsGames.js.
*/
// Обёртка scene → полный project_data (как wrap() в templates.js).
function wrap(scene) {
return {
version: 1,
scene: {
blocks: [],
models: [],
primitives: [],
folders: [],
scripts: [],
spawnPoint: { x: 0, y: 5, z: 0 },
playerModelType: 'character-a',
worldSize: 100,
shadowQuality: 'soft',
environment: null,
audio: null,
...scene,
},
editorCamera: null,
settings: {},
};
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 1 — «Собери монетки»
// Травяная площадка, 8 жёлтых сфер-монеток. Касание монетки —
// она исчезает, +1 очко. Собрал все — победа.
// ══════════════════════════════════════════════════════════════════
function game1CollectCoins() {
const blocks = [];
// Травяная площадка 14×14 (от -7 до 6 по X и Z), пол на y=0.
for (let x = -7; x <= 6; x++) {
for (let z = -7; z <= 6; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Бортик по краю площадки из камня — чтобы не упасть.
for (let i = -7; i <= 6; i++) {
blocks.push({ x: i, y: 1, z: -7, type: 'greystone' });
blocks.push({ x: i, y: 1, z: 6, type: 'greystone' });
blocks.push({ x: -7, y: 1, z: i, type: 'greystone' });
blocks.push({ x: 6, y: 1, z: i, type: 'greystone' });
}
// 8 монеток — жёлтые сферы, расставлены по площадке.
const coinSpots = [
{ x: -4, z: -4 }, { x: 0, z: -4 }, { x: 4, z: -4 },
{ x: -4, z: 0 }, { x: 4, z: 0 },
{ x: -4, z: 4 }, { x: 0, z: 4 }, { x: 4, z: 4 },
];
const primitives = coinSpots.map((c, i) => ({
id: i + 1,
type: 'sphere',
name: 'Монетка_' + (i + 1),
x: c.x, y: 1.2, z: c.z,
sx: 0.6, sy: 0.6, sz: 0.6,
color: '#ffd700',
material: 'neon',
canCollide: false, // сквозь монетку проходим — касание ловит скрипт
visible: true,
anchored: true,
mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
}));
const TOTAL = coinSpots.length;
const scripts = [];
// Главный (глобальный) скрипт — счётчик и проверка победы.
scripts.push({
id: 'g1_main',
name: 'Главный скрипт',
target: null,
code:
`// === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт ===
// Этот скрипт глобальный: считает собранные монетки и проверяет победу.
let score = 0; // сколько монеток собрано
const TOTAL = ${TOTAL}; // всего монеток на уровне
game.ui.score = score; // показать счёт 0 в углу
game.ui.showText('Собери все монетки!', 2); // подсказка на старте
// Монетки сообщают сюда о сборе через game.broadcast('coin').
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/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 });
}
});`,
});
// Скрипт на каждую монетку — реагирует на касание игроком.
for (let i = 0; i < TOTAL; i++) {
scripts.push({
id: 'g1_coin_' + (i + 1),
name: 'Монетка_' + (i + 1),
target: { kind: 'primitive', id: i + 1 },
code:
`// === Скрипт монетки ===
// Висит на монетке. game.self — это сама монетка.
game.self.onTouch(() => {
// игрок коснулся монетки — сообщить главному скрипту
game.broadcast('coin');
game.self.delete(); // монетка исчезает
});`,
});
}
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: 0 }, // игрок появляется в центре площадки
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 2 — «Прыгай по платформам»
// Паркур: 8 платформ-кубов в воздухе с разрывами. Допрыгай до финиша.
// Упал вниз — респаун на старте.
// ══════════════════════════════════════════════════════════════════
function game2PlatformJump() {
// Пола нет — игра целиком на платформах в воздухе. Упал — респаун.
const blocks = [];
// Платформы-примитивы: куб 2×0.5×2, ступеньками вперёд по +Z
// с подъёмом и разрывами. Прыгать с одной на другую.
const plats = [
{ x: 0, y: 1.5, z: 4 },
{ x: 0, y: 2.5, z: 8 },
{ x: 2, y: 3.5, z: 12 },
{ x: -2, y: 4.0, z: 16 },
{ x: 0, y: 4.5, z: 20 },
{ x: 2, y: 5.5, z: 24 },
{ x: 0, y: 6.0, z: 28 },
];
const primitives = plats.map((p, i) => ({
id: i + 1,
type: 'cube',
name: 'Платформа_' + (i + 1),
x: p.x, y: p.y, z: p.z,
sx: 2, sy: 0.5, sz: 2,
color: '#9b6b3e',
material: 'matte',
canCollide: true, // на платформу можно встать
visible: true,
anchored: true, // платформа не падает
mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
}));
// Финишная площадка-куб — большая, зелёная неоновая. На неё встают.
const FINISH_ID = plats.length + 1;
primitives.push({
id: FINISH_ID,
type: 'cube',
name: 'Финиш',
x: 0, y: 6.5, z: 33,
sx: 4, sy: 0.5, sz: 4,
color: '#22dd55',
material: 'neon',
canCollide: true,
visible: true,
anchored: true,
mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх площадки.
// ВАЖНО: onTouch тонкой плитки НЕ срабатывает, когда игрок просто
// СТОИТ на ней — капсула касается верха плитки, но не пересекает
// её объём. Поэтому ловим игрока высокой невидимой зоной
// (canCollide:false — игрок проходит сквозь, тело внутри зоны).
const FINISH_ZONE_ID = plats.length + 2;
primitives.push({
id: FINISH_ZONE_ID,
type: 'cube',
name: 'ФинишЗона',
x: 0, y: 6.5 + 1.75, z: 33, // центр зоны на 1.75 над плиткой
sx: 4, sy: 3, sz: 4, // высота 3 — игрок попадает телом
color: '#22dd55',
material: 'neon',
canCollide: false, // сквозь зону проходим
visible: false, // зона невидима
anchored: true,
mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Стартовая платформа — игрок появляется на ней (пола в игре нет).
const START_ID = plats.length + 3;
primitives.push({
id: START_ID,
type: 'cube',
name: 'Старт',
x: 0, y: 1.0, z: 0,
sx: 3, sy: 0.5, sz: 3,
color: '#6b7280',
material: 'matte',
canCollide: true,
visible: true,
anchored: true,
mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
// Главный скрипт — следит, не упал ли игрок, и обрабатывает победу.
scripts.push({
id: 'g2_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
});
// Скрипт на финиш-ЗОНУ — ловит касание (игрок стоит на площадке,
// тело внутри невидимой зоны).
scripts.push({
id: 'g2_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
// Висит на невидимой зоне над зелёной площадкой.
// Игрок встал на площадку — его тело внутри зоны — победа.
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о победе
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 80,
floorEnabled: false, // пола нет — игра в воздухе
spawnPoint: { x: 0, y: 1.3, z: 0 }, // на стартовой платформе
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 3 — «Не упади»
// Дорожка из плиток. Встал на плитку — через 1.2с она исчезает.
// Нужно всё время бежать вперёд. В конце — финиш.
// ══════════════════════════════════════════════════════════════════
function game3DontFall() {
// Пола нет — дорожка из плиток висит в воздухе. Упал — респаун.
const blocks = [];
// Дорожка из 14 плиток-примитивов (куб 2×0.4×2) вперёд по +Z.
// Все на y=1.0 — игрок ходит по ним как по полу.
const TILES = 14;
const TILE_Y = 1.0;
const primitives = [];
for (let i = 0; i < TILES; i++) {
// лёгкий зигзаг влево-вправо, чтобы было интереснее
const x = (i % 4 === 1) ? 2 : (i % 4 === 3) ? -2 : 0;
primitives.push({
id: i + 1,
type: 'cube',
name: 'Плитка_' + (i + 1),
x, y: TILE_Y, z: 3 + i * 3,
sx: 2, sy: 0.4, sz: 2,
color: '#d9a441',
material: 'matte',
canCollide: true,
visible: true,
anchored: true,
mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Финишная площадка после последней плитки — на уровне плиток.
const FINISH_ID = TILES + 1;
primitives.push({
id: FINISH_ID,
type: 'cube',
name: 'Финиш',
x: 0, y: TILE_Y, z: 3 + TILES * 3,
sx: 4, sy: 0.5, sz: 4,
color: '#22dd55',
material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх площадки — ловит игрока, стоящего
// на финише (тонкую плитку онтач не ловит, нужна объёмная зона).
const FINISH_ZONE_ID = TILES + 2;
primitives.push({
id: FINISH_ZONE_ID,
type: 'cube',
name: 'ФинишЗона',
x: 0, y: TILE_Y + 0.25 + 1.5, z: 3 + TILES * 3,
sx: 4, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Стартовая платформа — игрок появляется на ней (пола нет).
const START_ID = TILES + 3;
primitives.push({
id: START_ID,
type: 'cube',
name: 'Старт',
x: 0, y: TILE_Y, z: -1,
sx: 4, sy: 0.5, sz: 4,
color: '#6b7280',
material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
// Главный скрипт — падение/респаун и победа.
scripts.push({
id: 'g3_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
});
// Скрипт на каждую плитку — исчезает через 1.2с после касания.
for (let i = 0; i < TILES; i++) {
scripts.push({
id: 'g3_tile_' + (i + 1),
name: 'Плитка_' + (i + 1),
target: { kind: 'primitive', id: i + 1 },
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();
});
});`,
});
}
// Финиш — скрипт на невидимой зоне (игрок входит телом).
scripts.push({
id: 'g3_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о победе
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 80,
floorEnabled: false, // пола нет — плитки в воздухе
spawnPoint: { x: 0, y: TILE_Y + 0.3, z: -1 }, // на стартовой платформе
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 4 — «Кнопка-открывашка»
// Комната перегорожена стеной с дверью. Подойди к кнопке, нажми E —
// дверь уедет вверх. За дверью — финиш.
// ══════════════════════════════════════════════════════════════════
function game4ButtonDoor() {
// Пол комнаты 16×24 из камня.
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -4; z <= 19; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let pid = 0;
const next = () => ++pid;
// Стена-перегородка поперёк комнаты на z=8: два куба по краям,
// в центре проём шириной 3, который закрыт дверью.
primitives.push({
id: next(), type: 'cube', name: 'Стенаево',
x: -4.5, y: 2, z: 8, sx: 7, sy: 5, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
primitives.push({
id: next(), type: 'cube', name: 'Стена_право',
x: 4.5, y: 2, z: 8, sx: 7, sy: 5, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Дверь — куб в проёме. Её id запомним для скрипта.
const DOOR_ID = next();
primitives.push({
id: DOOR_ID, type: 'cube', name: 'Дверь',
x: 0, y: 2, z: 8, sx: 3, sy: 5, sz: 0.8,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Кнопка — цилиндр-тумба перед дверью.
const BUTTON_ID = next();
primitives.push({
id: BUTTON_ID, type: 'cylinder', name: 'Кнопка',
x: 3, y: 0.6, z: 3, sx: 1.2, sy: 1.2, sz: 1.2,
color: '#e23b3b', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — за дверью.
const FINISH_ID = next();
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 0.3, z: 16, sx: 4, sy: 0.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
// Главный скрипт — победа.
scripts.push({
id: 'g4_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
});
// Скрипт кнопки — взаимодействие по E открывает дверь.
scripts.push({
id: 'g4_button',
name: 'Кнопка',
target: { kind: 'primitive', id: BUTTON_ID },
code:
`// === Скрипт кнопки ===
// Висит на красной кнопке-цилиндре.
let opened = false;
game.self.onInteract(() => {
if (opened) return;
opened = true;
game.sound.play('click');
// находим дверь по имени и плавно поднимаем её вверх
const door = game.scene.findOne('Дверь');
game.tween(door, { y: 8 }, { duration: 1.2, easing: 'ease' });
game.ui.showText('Дверь открывается!', 2);
}, {
text: 'Открыть дверь',
distance: 4
});`,
});
// Скрипт финиша.
scripts.push({
id: 'g4_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 5 — «Лабиринт»
// Лабиринт из стен. Дойди от старта (S) до выхода (F).
// Карта рисуется текстом: # стена, пробел проход, S старт, F финиш.
// ══════════════════════════════════════════════════════════════════
function game5Maze() {
// Карта лабиринта 15×15. Каждый символ = клетка 2×2 единицы.
// # — стена (3 блока в высоту)
// ' ' — проход
// S — старт игрока
// F — финиш
const MAP = [
'###############',
'#S # #',
'# ### # ##### #',
'# # # # #',
'# # ####### # #',
'# # # # #',
'# ####### # # #',
'# # # #',
'##### # # #####',
'# # # # #',
'# # # ##### # #',
'# # # # # #',
'# # ##### # # #',
'# # F#',
'###############',
];
const CELL = 2; // размер клетки в единицах мира
const blocks = [];
let spawn = { x: 0, y: 1, z: 0 };
let finish = { x: 0, z: 0 };
for (let r = 0; r < MAP.length; r++) {
for (let c = 0; c < MAP[r].length; c++) {
const ch = MAP[r][c];
const wx = (c - 7) * CELL; // центрируем карту вокруг 0
const wz = (r - 7) * CELL;
// пол под каждой клеткой
for (let dx = 0; dx < CELL; dx++) {
for (let dz = 0; dz < CELL; dz++) {
blocks.push({ x: wx + dx, y: 0, z: wz + dz, type: 'greystone' });
}
}
if (ch === '#') {
// стена — 3 блока в высоту над клеткой
for (let h = 1; h <= 3; h++) {
for (let dx = 0; dx < CELL; dx++) {
for (let dz = 0; dz < CELL; dz++) {
blocks.push({ x: wx + dx, y: h, z: wz + dz, type: 'rock' });
}
}
}
} else if (ch === 'S') {
spawn = { x: wx + 0.5, y: 1, z: wz + 0.5 };
} else if (ch === 'F') {
finish = { x: wx + 0.5, z: wz + 0.5 };
}
}
}
// Финиш — зелёный коврик НА полу клетки F (пол лабиринта = блоки
// y=0, их верх y=1, поэтому коврик кладём на y≈1.1).
const FINISH_ID = 1;
// Невидимая ФИНИШ-ЗОНА в полный рост игрока над ковриком.
// ВАЖНО: тонкий коврик игрок не «касается» телом — его ноги на
// полу, коврик под ногами. Ловим высокой невидимой зоной
// (от пола вверх на 2.5), сквозь которую игрок проходит телом.
const FINISH_ZONE_ID = 2;
const primitives = [
{
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: finish.x, y: 1.1, z: finish.z, sx: 1.6, sy: 0.2, sz: 1.6,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
},
{
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: finish.x, y: 1 + 1.25, z: finish.z, sx: 1.8, sy: 2.5, sz: 1.8,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
},
];
const scripts = [
{
id: 'g5_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
},
{
id: 'g5_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
},
];
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
spawnPoint: spawn,
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 6 — «Цветные плитки»
// Сетка серых плиток. Наступил на плитку — она стала яркой.
// Раскрась все плитки — победа.
// ══════════════════════════════════════════════════════════════════
function game6ColorTiles() {
// Пол-рамка из травы, по которой стоят плитки.
const blocks = [];
for (let x = -6; x <= 5; x++) {
for (let z = -6; z <= 5; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Сетка 6×6 плиток-примитивов (плоские кубы), серые.
// Пол игры — блоки травы y=0 (их верх y=1). Плитки кладём НА пол
// (y=1.15), иначе игрок ходит выше плиток и не касается их —
// onTouch не срабатывает, плитки не красятся.
const GRID = 6;
const primitives = [];
let id = 0;
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
id++;
primitives.push({
id,
type: 'cube',
name: 'Плитка_' + id,
x: -4 + c * 2, y: 1.15, z: -4 + r * 2,
sx: 1.8, sy: 0.3, sz: 1.8,
color: '#9aa0aa', // серый — не раскрашена
material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
}
const TOTAL = id;
const scripts = [];
// Главный скрипт — счётчик раскрашенных плиток.
scripts.push({
id: 'g6_main',
name: 'Главный скрипт',
target: null,
code:
`// === ИГРА «ЦВЕТНЫЕ ПЛИТКИ» — главный скрипт ===
let painted = 0; // сколько плиток раскрашено
const TOTAL = ${TOTAL}; // всего плиток
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 });
}
});`,
});
// Скрипт на каждую плитку — меняет цвет при касании.
for (let i = 1; i <= TOTAL; i++) {
scripts.push({
id: 'g6_tile_' + i,
name: 'Плитка_' + i,
target: { kind: 'primitive', id: i },
code:
`// === Скрипт цветной плитки ===
let painted = false; // плитка уже раскрашена?
game.self.onTouch(() => {
if (painted) return;
painted = true;
// меняем цвет плитки на ярко-зелёный
game.scene.setColor(game.self.ref, '#33dd55');
game.broadcast('paint'); // сообщаем главному скрипту о покраске
});`,
});
}
return wrap({
blocks,
primitives,
scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 1, z: -5 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 7 — «Поймай падающее»
// С неба каждые 1.5с падает куб. Лови его (коснись) — +1 очко.
// Куб упал на землю — исчезает. Набери 15 очков.
// ══════════════════════════════════════════════════════════════════
function game7CatchFalling() {
// Площадка 16×16 из травы с бортиком.
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -8; z <= 7; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
for (let i = -8; i <= 7; i++) {
blocks.push({ x: i, y: 1, z: -8, type: 'greystone' });
blocks.push({ x: i, y: 1, z: 7, type: 'greystone' });
blocks.push({ x: -8, y: 1, z: i, type: 'greystone' });
blocks.push({ x: 7, y: 1, z: i, type: 'greystone' });
}
const scripts = [{
id: 'g7_main',
name: 'Главный скрипт',
target: null,
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 });
}
});`,
}];
return wrap({
blocks,
primitives: [],
scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 8 — «Беги к финишу»
// Длинная дорожка. Со старта запускается секундомер. Добеги до финиша
// — секундомер остановится, покажет твоё время.
// ══════════════════════════════════════════════════════════════════
function game8RunToFinish() {
// Дорожка-трасса: 6 в ширину, 60 в длину.
const blocks = [];
for (let x = -3; x <= 2; x++) {
for (let z = 0; z <= 60; z++) {
// чередуем оттенок дорожки полосами по 4 для наглядности скорости
const t = (Math.floor(z / 4) % 2 === 0) ? 'greystone' : 'rock';
blocks.push({ x, y: 0, z, type: t });
}
}
// Невысокие бортики вдоль трассы.
for (let z = 0; z <= 60; z++) {
blocks.push({ x: -3, y: 1, z, type: 'rock' });
blocks.push({ x: 2, y: 1, z, type: 'rock' });
}
// Финишная плитка-коврик в конце (на полу: пол-блоки y=0, верх y=1).
const FINISH_ID = 1;
const FINISH_ZONE_ID = 2;
const primitives = [
{
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 58, sx: 6, sy: 0.2, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
},
{
// невидимая зона в полный рост — ловит игрока телом
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1 + 1.25, z: 58, sx: 6, sy: 2.5, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
},
];
const scripts = [
{
id: 'g8_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
},
{
id: 'g8_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о финише
});`,
},
];
return wrap({
blocks,
primitives,
scripts,
worldSize: 90,
spawnPoint: { x: -0.5, y: 1, z: 1 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 9 — «Светофор»
// Большой светофор меняет цвет. Зелёный — беги к финишу. Красный —
// замри! Двинулся на красный — возврат на старт.
// ══════════════════════════════════════════════════════════════════
function game9TrafficLight() {
// Дорожка 8 в ширину, 50 в длину.
const blocks = [];
for (let x = -4; x <= 3; x++) {
for (let z = 0; z <= 50; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
// Светофор — большой куб в конце дорожки, за финишем, виден издалека.
const LIGHT_ID = 1;
primitives.push({
id: LIGHT_ID, type: 'sphere', name: 'Светофор',
x: -0.5, y: 6, z: 52, sx: 3, sy: 3, sz: 3,
color: '#e23b3b', material: 'neon', // старт — красный
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — коврик на полу (пол-блоки y=0, верх y=1).
const FINISH_ID = 2;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 48, sx: 8, sy: 0.2, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая финиш-зона в полный рост — ловит игрока телом.
const FINISH_ZONE_ID = 3;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1 + 1.25, z: 48, sx: 8, sy: 2.5, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g9_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
},
{
id: 'g9_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
},
];
return wrap({
blocks,
primitives,
scripts,
worldSize: 80,
spawnPoint: { x: -0.5, y: 1, z: 1 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 10 — «Прыжок-пружина»
// Башня из площадок на разной высоте. Между ними — батуты-пружины.
// Встал на батут — он подбрасывает вверх. Допрыгни до верха.
// ══════════════════════════════════════════════════════════════════
function game10SpringJump() {
// Стартовая площадка.
const blocks = [];
for (let x = -3; x <= 2; x++) {
for (let z = -3; z <= 2; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
const next = () => ++id;
// Площадки-этажи на высотах 7, 14, 21 (промежуточные).
const floors = [
{ x: 0, y: 7, z: 6 },
{ x: 0, y: 14, z: 12 },
{ x: 0, y: 21, z: 18 },
];
const floorIds = floors.map((f) => {
const fid = next();
primitives.push({
id: fid, type: 'cube', name: 'Этаж_' + fid,
x: f.x, y: f.y, z: f.z, sx: 4, sy: 0.5, sz: 4,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return fid;
});
// Батуты — оранжевые низкие цилиндры (sy=0.4). Лежат НА поверхности,
// по которой ходит игрок: старт-пол верх y=1, этажи sy=0.5 → их верх
// y_этажа+0.25. Центр батута = поверхность + 0.2 (половина sy).
const trampSpots = [
{ x: 0, y: 1.2, z: 0 }, // на старте (пол верх y=1)
{ x: 0, y: 7.45, z: 6 }, // на 1 этаже (верх 7.25)
{ x: 0, y: 14.45, z: 12 }, // на 2 этаже (верх 14.25)
];
const trampIds = trampSpots.map((t) => {
const tid = next();
primitives.push({
id: tid, type: 'cylinder', name: атут_' + tid,
x: t.x, y: t.y, z: t.z, sx: 2, sy: 0.4, sz: 2,
color: '#ff8c1a', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return tid;
});
// Финиш — на самом верху, на верхнем этаже (y=21, верх 21.25).
const FINISH_ID = next();
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 21.5, z: 18, sx: 3, sy: 0.5, sz: 3,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая финиш-зона в полный рост — ловит игрока телом.
const FINISH_ZONE_ID = next();
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 21.25 + 1.25, z: 18, sx: 3, sy: 2.5, sz: 3,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g10_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
});
// Скрипт на каждый батут — подбрасывает игрока.
for (const tid of trampIds) {
scripts.push({
id: 'g10_tramp_' + tid,
name: атут_' + tid,
target: { kind: 'primitive', id: tid },
code:
`// === Скрипт батута ===
// Игрок встал на батут — мощный подброс вверх.
game.self.onTouch(() => {
game.player.boostJump(3.2); // 3.2 = в 3 раза выше обычного прыжка
game.sound.play('jump');
});`,
});
}
// Финиш — скрипт на невидимой зоне (игрок входит телом).
scripts.push({
id: 'g10_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
floorEnabled: false, // пола нет — батуты в воздухе
spawnPoint: { x: 0, y: 1, z: -2 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 11 — «Эхо-комната»
// Комната с 6 звуковыми плитками. Наступил — плитка вспыхивает
// и звучит. Пройди все 6 плиток, потом встань на финиш.
// ══════════════════════════════════════════════════════════════════
function game11EchoRoom() {
// Пол комнаты 14×14.
const blocks = [];
for (let x = -7; x <= 6; x++) {
for (let z = -7; z <= 6; z++) {
blocks.push({ x, y: 0, z, type: 'cotton-blue' });
}
}
// Стены комнаты — для «эха» (визуально замкнутое пространство).
for (let i = -7; i <= 6; i++) {
for (let h = 1; h <= 3; h++) {
blocks.push({ x: i, y: h, z: -7, type: 'greystone' });
blocks.push({ x: i, y: h, z: 6, type: 'greystone' });
blocks.push({ x: -7, y: h, z: i, type: 'greystone' });
blocks.push({ x: 6, y: h, z: i, type: 'greystone' });
}
}
// 6 звуковых плиток — разноцветные, каждая со своим звуком.
const tiles = [
{ x: -4, z: -3, color: '#e23b3b', sound: 'coin' },
{ x: 0, z: -3, color: '#f59e0b', sound: 'jump' },
{ x: 4, z: -3, color: '#facc15', sound: 'pickup' },
{ x: -4, z: 3, color: '#22c55e', sound: 'click' },
{ x: 0, z: 3, color: '#3b82f6', sound: 'hit' },
{ x: 4, z: 3, color: '#a855f7', sound: 'coin' },
];
// Плитки кладём НА пол (пол-блоки y=0, верх y=1 → плитка y=1.15),
// иначе игрок ходит выше плиток и не касается их.
const primitives = tiles.map((t, i) => ({
id: i + 1, type: 'cylinder', name: 'Плитка_' + (i + 1),
x: t.x, y: 1.15, z: t.z, sx: 2.2, sy: 0.3, sz: 2.2,
color: t.color, material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
}));
const TOTAL = tiles.length;
// Финиш — коврик на полу.
const FINISH_ID = TOTAL + 1;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 0, sx: 2.5, sy: 0.2, sz: 2.5,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая финиш-зона в полный рост — ловит игрока телом.
const FINISH_ZONE_ID = TOTAL + 2;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 1 + 1.25, z: 0, sx: 2.5, sy: 2.5, sz: 2.5,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g11_main',
name: 'Главный скрипт',
target: null,
code:
`// === ИГРА «ЭХО-КОМНАТА» — главный скрипт ===
let stepped = 0; // на сколько плиток наступили
const TOTAL = ${TOTAL};
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 });
});`,
});
// Скрипт на каждую плитку — свой звук.
tiles.forEach((t, i) => {
scripts.push({
id: 'g11_tile_' + (i + 1),
name: 'Плитка_' + (i + 1),
target: { kind: 'primitive', id: i + 1 },
code:
`// === Скрипт звуковой плитки ===
let used = false; // на эту плитку уже наступали?
game.self.onTouch(() => {
// звук играет КАЖДЫЙ раз — это эхо-комната
game.sound.play('${t.sound}');
// вспышка частиц над плиткой
game.scene.spawnParticles('sparks', game.self.position,
{ duration: 0.6, color: '${t.color}' });
// засчитываем плитку только в первый раз
if (!used) {
used = true;
game.broadcast('step'); // сообщаем главному скрипту о новой плитке
}
});`,
});
});
// Финиш — скрипт на невидимой зоне (игрок входит телом).
scripts.push({
id: 'g11_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о финише
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 1, z: 5 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 12 — «Дверь по коду»
// Четыре кнопки-цифры. Нажимай их (клавишей E) в правильном порядке —
// дверь откроется. Ошибся — код сбрасывается.
// ══════════════════════════════════════════════════════════════════
function game12CodeDoor() {
// Пол комнаты.
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -4; z <= 17; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
const next = () => ++id;
// Стена с проёмом на z=10.
primitives.push({
id: next(), type: 'cube', name: 'Стенаево',
x: -4.5, y: 2.5, z: 10, sx: 7, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
primitives.push({
id: next(), type: 'cube', name: 'Стена_право',
x: 4.5, y: 2.5, z: 10, sx: 7, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Дверь.
const DOOR_ID = next();
primitives.push({
id: DOOR_ID, type: 'cube', name: 'Дверь',
x: 0, y: 2.5, z: 10, sx: 3, sy: 6, sz: 0.8,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// 4 кнопки-цифры — цилиндры в ряд перед дверью.
const buttonIds = [];
for (let n = 1; n <= 4; n++) {
const bid = next();
buttonIds.push(bid);
primitives.push({
id: bid, type: 'cylinder', name: 'Кнопка_' + n,
x: -4.5 + (n - 1) * 3, y: 1, z: 4, sx: 1.4, sy: 1.6, sz: 1.4,
color: '#3357ff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Финиш за дверью — коврик на полу (пол-блоки y=0, верх y=1).
const FINISH_ID = next();
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 14, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая финиш-зона в полный рост — ловит игрока телом.
const FINISH_ZONE_ID = next();
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 1 + 1.25, z: 14, sx: 4, sy: 2.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
// Главный скрипт — хранит код и проверяет последовательность.
scripts.push({
id: 'g12_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
});
// Скрипт на каждую кнопку.
buttonIds.forEach((bid, idx) => {
const num = idx + 1;
scripts.push({
id: 'g12_btn_' + num,
name: 'Кнопка_' + num,
target: { kind: 'primitive', id: bid },
code:
`// === Скрипт кнопки-цифры ${num} ===
game.self.onInteract(() => {
// сообщаем главному скрипту номер нажатой кнопки
game.broadcast('press', { num: ${num} });
}, {
text: 'Нажать кнопку ${num}',
distance: 3
});`,
});
});
// Финиш — скрипт на невидимой зоне (игрок входит телом).
scripts.push({
id: 'g12_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 13 — «Торговец»
// NPC-торговец стоит за прилавком. Подойди (E) — он поговорит
// и подарит ключ. С ключом открой дверь и дойди до финиша.
// ══════════════════════════════════════════════════════════════════
function game13Trader() {
// Пол комнаты-лавки.
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -4; z <= 19; z++) {
blocks.push({ x, y: 0, z, type: 'wood' });
}
}
const primitives = [];
let id = 0;
const next = () => ++id;
// Прилавок — длинный куб.
const COUNTER_ID = next();
primitives.push({
id: COUNTER_ID, type: 'cube', name: 'Прилавок',
x: 0, y: 0.8, z: 3, sx: 5, sy: 1.6, sz: 1.5,
color: '#7a4a26', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Стена с дверью на z=11.
primitives.push({
id: next(), type: 'cube', name: 'Стенаево',
x: -4.5, y: 2.5, z: 11, sx: 7, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
primitives.push({
id: next(), type: 'cube', name: 'Стена_право',
x: 4.5, y: 2.5, z: 11, sx: 7, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const DOOR_ID = next();
primitives.push({
id: DOOR_ID, type: 'cube', name: 'Дверь',
x: 0, y: 2.5, z: 11, sx: 3, sy: 6, sz: 0.8,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш за дверью — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = next();
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 15, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = next();
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 2.25, z: 15, sx: 4, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
// Главный скрипт — спавнит NPC-торговца и хранит состояние.
scripts.push({
id: 'g13_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
});
// Скрипт прилавка — взаимодействие = разговор с торговцем.
scripts.push({
id: 'g13_counter',
name: 'Прилавок',
target: { kind: 'primitive', id: COUNTER_ID },
code:
`// === Скрипт прилавка ===
game.self.onInteract(() => {
game.broadcast('talk'); // сообщаем главному скрипту: говорим с торговцем
}, {
text: 'Поговорить с торговцем',
distance: 4
});`,
});
// Скрипт двери — взаимодействие = попытка открыть.
scripts.push({
id: 'g13_door',
name: 'Дверь',
target: { kind: 'primitive', id: DOOR_ID },
code:
`// === Скрипт двери ===
game.self.onInteract(() => {
game.broadcast('openDoor'); // сообщаем главному скрипту: открыть дверь
}, {
text: 'Открыть дверь',
distance: 4
});`,
});
// Финиш — скрипт висит на невидимой зоне над ковриком.
scripts.push({
id: 'g13_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
});
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 14 — «Собери по тегам»
// На площадке звёзды (с тегом «звезда») и кубы-обманки (без тега).
// Собери все звёзды. Тег помогает находить нужные объекты.
// ══════════════════════════════════════════════════════════════════
function game14CollectByTag() {
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -8; z <= 7; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
const primitives = [];
let id = 0;
// 7 звёзд — жёлтые конусы.
const starSpots = [
{ x: -5, z: -5 }, { x: 0, z: -5 }, { x: 5, z: -5 },
{ x: -5, z: 1 }, { x: 5, z: 1 },
{ x: -3, z: 5 }, { x: 4, z: 5 },
];
const starIds = starSpots.map((s) => {
id++;
primitives.push({
id, type: 'cone', name: 'Звезда_' + id,
x: s.x, y: 1, z: s.z, sx: 1, sy: 1.4, sz: 1,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// 5 кубов-обманок — синие, без тега. Их собирать не надо.
const decoySpots = [
{ x: -2, z: -2 }, { x: 3, z: -2 }, { x: 0, z: 3 },
{ x: -6, z: 4 }, { x: 6, z: -6 },
];
for (const d of decoySpots) {
id++;
primitives.push({
id, type: 'cube', name: 'Куб_' + id,
x: d.x, y: 0.8, z: d.z, sx: 1.2, sy: 1.2, sz: 1.2,
color: '#3b82f6', material: 'matte',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
const scripts = [];
// Главный скрипт: помечает звёзды тегом и считает остаток.
scripts.push({
id: 'g14_main',
name: 'Главный скрипт',
target: null,
code:
`// === ИГРА «СОБЕРИ ПО ТЕГАМ» — главный скрипт ===
game.ui.showText('Собери все ЖЁЛТЫЕ звёзды!', 3);
game.ui.score = 0;
// помечаем все звёзды тегом 'звезда'.
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже,
// поэтому простановку тегов делаем через game.after.
game.after(0.2, () => {
for (let i = 1; i <= ${starIds.length}; 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 = ${starIds.length} - 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 });
}
});`,
});
// Скрипт на каждую звезду.
for (const sid of starIds) {
scripts.push({
id: 'g14_star_' + sid,
name: 'Звезда_' + sid,
target: { kind: 'primitive', id: sid },
code:
`// === Скрипт звезды ===
game.self.onTouch(() => {
// снимаем тег и удаляем звезду
game.scene.untag(game.self.ref, 'звезда');
game.self.delete();
game.broadcast('collected'); // сообщаем главному скрипту о сборе
});`,
});
}
return wrap({
blocks,
primitives,
scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 15 — «Тир»
// Мишени-сферы на постаментах. Кликай по мишеням — за каждое
// попадание очко. Выбей все 8 мишеней.
// ══════════════════════════════════════════════════════════════════
function game15ShootingRange() {
// Площадка тира.
const blocks = [];
for (let x = -10; x <= 9; x++) {
for (let z = -4; z <= 15; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
// 8 мишеней на постаментах.
const targetSpots = [
{ x: -8, z: 6 }, { x: -5, z: 9 }, { x: -2, z: 7 }, { x: 1, z: 10 },
{ x: 4, z: 7 }, { x: 7, z: 9 }, { x: -6, z: 12 }, { x: 5, z: 12 },
];
const primitives = [];
let id = 0;
const targetIds = [];
for (const t of targetSpots) {
// постамент-кубик
id++;
primitives.push({
id, type: 'cube', name: остамент_' + id,
x: t.x, y: 1, z: t.z, sx: 1, sy: 2, sz: 1,
color: '#6b7280', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// мишень-сфера на постаменте
id++;
targetIds.push(id);
primitives.push({
id, type: 'sphere', name: 'Мишень_' + id,
x: t.x, y: 3, z: t.z, sx: 1.1, sy: 1.1, sz: 1.1,
color: '#ff3030', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
const scripts = [];
scripts.push({
id: 'g15_main',
name: 'Главный скрипт',
target: null,
code:
`// === ИГРА «ТИР» — главный скрипт ===
let score = 0;
const TOTAL = ${targetIds.length};
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 });
}
});`,
});
// Скрипт на каждую мишень.
for (const tid of targetIds) {
scripts.push({
id: 'g15_target_' + tid,
name: 'Мишень_' + tid,
target: { kind: 'primitive', id: tid },
code:
`// === Скрипт мишени ===
// Клик по 3D-объекту = выстрел в него.
game.self.onClick(() => {
// взрыв искр на месте мишени
game.scene.spawnParticles('explosion', game.self.position,
{ count: 1, color: '#ff6633' });
game.self.delete(); // мишень сбита
game.broadcast('hit'); // сообщаем главному скрипту о попадании
});`,
});
}
return wrap({
blocks,
primitives,
scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: -2 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 16 — «Лава-пол»
// Поле залито лавой — касание лавы жжёт (урон). Прыгай по безопасным
// каменным островкам до финиша.
// ══════════════════════════════════════════════════════════════════
function game16LavaFloor() {
// Пол из блоков лавы — большое озеро.
const blocks = [];
for (let x = -10; x <= 9; x++) {
for (let z = -2; z <= 30; z++) {
blocks.push({ x, y: 0, z, type: 'lava' });
}
}
// Стартовая безопасная площадка из камня.
for (let x = -3; x <= 2; x++) {
for (let z = -2; z <= 1; z++) {
blocks.push({ x, y: 1, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// Островки-платформы — каменные кубы над лавой, дорожкой.
const isleSpots = [
{ x: 0, z: 5 }, { x: 3, z: 9 }, { x: -2, z: 13 },
{ x: 2, z: 17 }, { x: -3, z: 21 }, { x: 1, z: 25 },
];
for (const s of isleSpots) {
id++;
primitives.push({
id, type: 'cube', name: 'Островок_' + id,
x: s.x, y: 1.2, z: s.z, sx: 2.4, sy: 0.6, sz: 2.4,
color: '#7a8088', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Невидимая зона-лава над озером — ловит касание игрока.
const LAVA_ID = ++id;
primitives.push({
id: LAVA_ID, type: 'cube', name: 'ЛаваЗона',
x: -0.5, y: 1, z: 14, sx: 20, sy: 0.6, sz: 34,
color: '#ff5522', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — каменная площадка в конце (твёрдая, на неё встают).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.2, z: 29, sx: 5, sy: 0.6, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх площадки — ловит игрока телом
// (он стоит сверху, тонкую плитку телом не пересекает).
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1.5 + 1.5, z: 29, sx: 5, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g16_main',
name: 'Главный скрипт',
target: null,
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 });
});`,
},
{
id: 'g16_lava',
name: 'ЛаваЗона',
target: { kind: 'primitive', id: LAVA_ID },
code:
`// === Скрипт лавы ===
// Игрок коснулся лавы — урон. У damage есть защита (i-frames),
// так что урон не каждый кадр, а раз в ~0.5 секунды.
game.self.onTouch(() => {
game.player.damage(20);
game.sound.play('hit');
});`,
},
{
id: 'g16_finish',
name: 'Финиш',
target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`,
},
];
return wrap({
blocks,
primitives,
scripts,
worldSize: 70,
spawnPoint: { x: 0, y: 2, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 17 — «Ключ и сундук»
// Найди ключ на уровне, подбери его, открой сундук клавишей E.
// ══════════════════════════════════════════════════════════════════
function game17KeyChest() {
const blocks = [];
for (let x = -10; x <= 9; x++) {
for (let z = -10; z <= 9; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Несколько кустов-кубов, среди которых спрятан ключ.
const primitives = [];
let id = 0;
const bushSpots = [
{ x: -7, z: -6 }, { x: 5, z: -7 }, { x: -4, z: 5 },
{ x: 7, z: 6 }, { x: 0, z: -8 }, { x: -8, z: 3 },
];
for (const b of bushSpots) {
id++;
primitives.push({
id, type: 'cube', name: 'Куст_' + id,
x: b.x, y: 0.8, z: b.z, sx: 1.6, sy: 1.6, sz: 1.6,
color: '#2f7d32', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Ключ — жёлтый тор, лежит в углу.
const KEY_ID = ++id;
primitives.push({
id: KEY_ID, type: 'torus', name: 'Ключ',
x: 7, y: 1, z: -7, sx: 0.9, sy: 0.4, sz: 0.9,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Сундук — коричневый куб в центре.
const CHEST_ID = ++id;
primitives.push({
id: CHEST_ID, type: 'cube', name: 'Сундук',
x: 0, y: 0.8, z: 4, sx: 2, sy: 1.6, sz: 1.4,
color: '#7a4a26', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g17_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
id: 'g17_key', name: 'Ключ', target: { kind: 'primitive', id: KEY_ID },
code:
`// === Скрипт ключа ===
game.self.onTouch(() => {
game.broadcast('takeKey'); // сообщаем главному скрипту: ключ найден
game.self.delete(); // ключ подобран
});`,
},
{
id: 'g17_chest', name: 'Сундук', target: { kind: 'primitive', id: CHEST_ID },
code:
`// === Скрипт сундука ===
game.self.onInteract(() => {
game.broadcast('openChest'); // сообщаем главному скрипту: открыть сундук
}, { text: 'Открыть сундук', distance: 4 });`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: -3 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 18 — «Качели»
// Качели на петле-констрейнте раскачиваются. Прокатись на качелях.
// ══════════════════════════════════════════════════════════════════
function game18Swing() {
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -8; z <= 7; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Стартовая возвышенность, с которой запрыгивают на качели.
for (let x = -2; x <= 1; x++) {
for (let z = -6; z <= -4; z++) {
blocks.push({ x, y: 1, z, type: 'greystone' });
blocks.push({ x, y: 2, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// Сиденье качелей — широкая плоская платформа.
const SWING_ID = ++id;
primitives.push({
id: SWING_ID, type: 'cube', name: 'Качели',
x: 0, y: 3, z: 0, sx: 4, sy: 0.5, sz: 3,
color: '#9b6b3e', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — площадка по другую сторону качелей (твёрдая, на неё встают).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1, z: 7, sx: 4, sy: 0.5, sz: 3,
color: '#22dd55', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх площадки — ловит игрока телом.
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 1.25 + 1.5, z: 7, sx: 4, sy: 3, sz: 3,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g18_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
id: 'g18_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 4, z: -5 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 19 — «Лифт»
// Платформа-лифт ездит между нижним и верхним этажом.
// Заедь на лифте наверх, на финиш.
// ══════════════════════════════════════════════════════════════════
function game19Elevator() {
const blocks = [];
// Нижний этаж.
for (let x = -7; x <= 6; x++) {
for (let z = -7; z <= 6; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
// Верхний этаж — площадка сбоку на высоте 12.
for (let x = -7; x <= 6; x++) {
for (let z = 8; z <= 14; z++) {
blocks.push({ x, y: 12, z, type: 'rock' });
}
}
const primitives = [];
let id = 0;
// Лифт — платформа.
const LIFT_ID = ++id;
primitives.push({
id: LIFT_ID, type: 'cube', name: 'Лифт',
x: 0, y: 1, z: 4, sx: 3.5, sy: 0.5, sz: 3.5,
color: '#3357ff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — на верхнем этаже. Блоки этажа rock на y=12 (верх y=13),
// коврик-финиш кладём НА этаж (y=13.1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 13.1, z: 11, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 14.25, z: 11, sx: 4, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g19_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
id: 'g19_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: -3 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 20 — «Имена над врагами»
// Несколько NPC-врагов, над каждым висит метка с именем и HP.
// Кликай по врагам — метка показывает урон. Победи всех.
// ══════════════════════════════════════════════════════════════════
function game20EnemyNames() {
const blocks = [];
for (let x = -10; x <= 9; x++) {
for (let z = -10; z <= 9; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const scripts = [{
id: 'g20_main', name: 'Главный скрипт', target: null,
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 });
}
});
});`,
}];
return wrap({
blocks, primitives: [], scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: -4 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 21 — «Преследователь»
// NPC гонится за игроком. Добеги до зелёного укрытия, не дав себя
// поймать. Поймал — на старт.
// ══════════════════════════════════════════════════════════════════
function game21Chaser() {
const blocks = [];
for (let x = -14; x <= 13; x++) {
for (let z = -6; z <= 28; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// несколько кубов-укрытий по полю, за которыми можно петлять
const primitives = [];
let id = 0;
const blockSpots = [
{ x: -6, z: 6 }, { x: 5, z: 10 }, { x: -3, z: 16 }, { x: 7, z: 20 },
];
for (const b of blockSpots) {
id++;
primitives.push({
id, type: 'cube', name: репятствие_' + id,
x: b.x, y: 1.5, z: b.z, sx: 3, sy: 3, sz: 3,
color: '#6b7280', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// финиш-укрытие — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 26, sx: 5, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 2.25, z: 26, sx: 5, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g21_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
id: 'g21_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 70,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 22 — «Зона опасности»
// На пути — невидимая зона. Внутри неё игрок теряет HP. Пробеги
// через зону к финишу, пока HP не кончилось.
// ══════════════════════════════════════════════════════════════════
function game22DangerZone() {
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -4; z <= 36; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// Зона опасности — большой красный полупрозрачный куб посередине.
const ZONE_ID = ++id;
primitives.push({
id: ZONE_ID, type: 'cube', name: 'ЗонаОпасности',
x: -0.5, y: 2, z: 16, sx: 16, sy: 5, sz: 14,
color: '#ff3333', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 33, sx: 5, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 2.25, z: 33, sx: 5, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Аптечка перед зоной — лечит.
const HEAL_ID = ++id;
primitives.push({
id: HEAL_ID, type: 'sphere', name: 'Аптечка',
x: 4, y: 1, z: 6, sx: 0.9, sy: 0.9, sz: 0.9,
color: '#33dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g22_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
id: 'g22_zone', name: 'ЗонаОпасности', target: { kind: 'primitive', id: ZONE_ID },
code:
`// === Скрипт зоны опасности ===
// onTouch — игрок вошёл, onUntouch — вышел.
game.self.onTouch(() => {
game.broadcast('zone-enter');
});
game.self.onUntouch(() => {
game.broadcast('zone-leave');
});`,
},
{
id: 'g22_heal', name: 'Аптечка', target: { kind: 'primitive', id: HEAL_ID },
code:
`// === Скрипт аптечки ===
game.self.onTouch(() => {
game.player.heal(60);
game.ui.showText('+60 HP', 1.5);
game.sound.play('pickup');
game.self.delete();
});`,
},
{
id: 'g22_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 70,
spawnPoint: { x: -0.5, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 23 — «Переключатели»
// Три рычага. Дёрни их (E) в правильном порядке — откроется дверь.
// Ошибся — все рычаги сбрасываются.
// ══════════════════════════════════════════════════════════════════
function game23Switches() {
const blocks = [];
for (let x = -9; x <= 8; x++) {
for (let z = -4; z <= 17; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// Стена с дверью.
primitives.push({
id: ++id, type: 'cube', name: 'Стенаево',
x: -5, y: 2.5, z: 10, sx: 8, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
primitives.push({
id: ++id, type: 'cube', name: 'Стена_право',
x: 5, y: 2.5, z: 10, sx: 8, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const DOOR_ID = ++id;
primitives.push({
id: DOOR_ID, type: 'cube', name: 'Дверь',
x: 0, y: 2.5, z: 10, sx: 3, sy: 6, sz: 0.8,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// 3 рычага-цилиндра.
const leverIds = [];
for (let n = 1; n <= 3; n++) {
const lid = ++id;
leverIds.push(lid);
primitives.push({
id: lid, type: 'cylinder', name: 'Рычаг_' + n,
x: -5 + (n - 1) * 5, y: 1.2, z: 4, sx: 1, sy: 2.4, sz: 1,
color: '#e23b3b', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Финиш за дверью — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 14, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 2.25, z: 14, sx: 4, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g23_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
leverIds.forEach((lid, idx) => {
const n = idx + 1;
scripts.push({
id: 'g23_lever_' + n, name: 'Рычаг_' + n,
target: { kind: 'primitive', id: lid },
code:
`// === Скрипт рычага ${n} ===
game.self.onInteract(() => {
game.broadcast('lever', { num: ${n} });
}, { text: 'Дёрнуть рычаг ${n}', distance: 3 });`,
});
});
scripts.push({
id: 'g23_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 24 — «Падающий мост»
// Мост из плиток над пропастью. Каждая плитка исчезает через секунду
// после касания. Перебеги мост, пока не рухнул.
// ══════════════════════════════════════════════════════════════════
function game24FallingBridge() {
// Старт и финиш — твёрдые площадки, между ними пропасть.
const blocks = [];
for (let x = -3; x <= 2; x++) {
for (let z = -3; z <= 0; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
for (let z = 40; z <= 44; z++) {
blocks.push({ x, y: 0, z, type: 'rock' });
}
}
// Мост — 18 плиток-примитивов в ряд над пропастью.
// По доскам ХОДЯТ: их верх кладём на уровень пола (y=1),
// доска sy=0.35 → центр y = 1 - 0.35/2 ≈ 0.83. Иначе доски ниже
// ног игрока и он сразу падает в пропасть.
const TILES = 18;
const primitives = [];
for (let i = 0; i < TILES; i++) {
primitives.push({
id: i + 1, type: 'cube', name: оска_' + (i + 1),
x: -0.5, y: 0.83, z: 3 + i * 2,
sx: 2.4, sy: 0.35, sz: 2,
color: '#9b6b3e', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Финиш — видимый коврик НА полу (финиш-блоки rock y=0, верх y=1).
const FINISH_ID = TILES + 1;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 42, sx: 5, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = TILES + 2;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 2.25, z: 42, sx: 5, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g24_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
for (let i = 1; i <= TILES; i++) {
scripts.push({
id: 'g24_plank_' + i, name: оска_' + i,
target: { kind: 'primitive', id: i },
code:
`// === Скрипт доски моста ===
let cracking = false;
game.self.onTouch(() => {
if (cracking) return;
cracking = true;
game.sound.play('click');
game.after(1, () => { game.self.delete(); });
});`,
});
}
scripts.push({
id: 'g24_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 70,
floorEnabled: false, // мост над пропастью — пола нет
spawnPoint: { x: -0.5, y: 1, z: -2 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 25 — «Камера-облёт»
// При старте камера красиво облетает уровень, показывая его. Потом
// управление возвращается игроку, и он идёт к финишу.
// ══════════════════════════════════════════════════════════════════
function game25FlybyCamera() {
const blocks = [];
for (let x = -12; x <= 11; x++) {
for (let z = -4; z <= 30; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
const primitives = [];
let id = 0;
// несколько украшающих столбов вдоль пути
for (let i = 0; i < 5; i++) {
id++;
primitives.push({
id, type: 'cylinder', name: 'Столб_' + id,
x: (i % 2 === 0) ? -6 : 6, y: 2.5, z: 4 + i * 5,
sx: 1.2, sy: 5, sz: 1.2,
color: '#a855f7', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// Финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 27, sx: 5, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 2.25, z: 27, sx: 5, sy: 3, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g25_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
id: 'g25_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 65,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 26 — «Магнит монет»
// Монеты разбросаны по полю. Когда игрок подходит близко — монета
// сама летит к нему (твин). Собери все.
// ══════════════════════════════════════════════════════════════════
function game26CoinMagnet() {
const blocks = [];
for (let x = -10; x <= 9; x++) {
for (let z = -10; z <= 9; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
const coinSpots = [
{ x: -7, z: -7 }, { x: 0, z: -7 }, { x: 7, z: -7 },
{ x: -7, z: 0 }, { x: 7, z: 0 },
{ x: -7, z: 7 }, { x: 0, z: 7 }, { x: 7, z: 7 },
];
const primitives = coinSpots.map((c, i) => ({
id: i + 1, type: 'sphere', name: 'Монетка_' + (i + 1),
x: c.x, y: 1, z: c.z, sx: 0.6, sy: 0.6, sz: 0.6,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
}));
const TOTAL = coinSpots.length;
const scripts = [];
scripts.push({
id: 'g26_main', name: 'Главный скрипт', target: null,
code:
`// === ИГРА «МАГНИТ МОНЕТ» — главный скрипт ===
let score = 0;
const TOTAL = ${TOTAL};
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 });
}
});`,
});
for (let i = 1; i <= TOTAL; i++) {
scripts.push({
id: 'g26_coin_' + i, name: 'Монетка_' + i,
target: { kind: 'primitive', id: i },
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' });
}
});`,
});
}
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 27 — «Двойной прыжок»
// Паркур, где платформы так далеко, что без двойного прыжка
// не допрыгнуть. Скрипт включает игроку двойной прыжок.
// ══════════════════════════════════════════════════════════════════
function game27DoubleJump() {
const blocks = [];
for (let x = -2; x <= 1; x++) {
for (let z = -2; z <= 1; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// платформы с большими разрывами
const plats = [
{ x: 0, y: 2, z: 7 },
{ x: 3, y: 3.5, z: 14 },
{ x: -3, y: 5, z: 21 },
{ x: 0, y: 6.5, z: 28 },
{ x: 3, y: 8, z: 35 },
];
const primitives = plats.map((p, i) => ({
id: i + 1, type: 'cube', name: 'Платформа_' + (i + 1),
x: p.x, y: p.y, z: p.z, sx: 2.5, sy: 0.5, sz: 2.5,
color: '#9b6b3e', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
}));
// Видимая финиш-площадка — куб, на неё встают (верх на y=8.75).
const FINISH_ID = plats.length + 1;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 3, y: 8.5, z: 40, sx: 4, sy: 0.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх площадки: onTouch тонкой плитки НЕ
// срабатывает, когда игрок просто СТОИТ на ней. Ловим телом игрока
// высокой невидимой зоной (canCollide:false — проходим сквозь).
const FINISH_ZONE_ID = plats.length + 2;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 3, y: 8.75 + 1.25, z: 40, sx: 4, sy: 2.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g27_main', name: 'Главный скрипт', target: null,
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 });
});`,
},
{
// скрипт вешаем на невидимую ЗОНУ, а не на плоскую плитку
id: 'g27_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиш-зоны ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 70,
floorEnabled: false, // паркур — между платформами пустота
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 28 — «Призрачные стены»
// Коридор перегорожен стенами. Некоторые стены — призрачные:
// кликни по стене, и она станет проходимой.
// ══════════════════════════════════════════════════════════════════
function game28GhostWalls() {
const blocks = [];
for (let x = -5; x <= 4; x++) {
for (let z = -4; z <= 36; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
// 4 стены поперёк коридора — все призрачные.
const primitives = [];
let id = 0;
const wallZ = [6, 14, 22, 30];
const wallIds = wallZ.map((z) => {
id++;
primitives.push({
id, type: 'cube', name: 'Стена_' + id,
x: -0.5, y: 2.5, z, sx: 10, sy: 5, sz: 1,
color: '#7c5cff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// Видимый коврик-финиш НА полу (пол greystone y=0, верх y=1 → y=1.1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 34, sx: 5, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока, когда он
// просто стоит на финише (onTouch плоской плитки не срабатывает).
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1 + 1.25, z: 34, sx: 5, sy: 2.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g28_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
for (const wid of wallIds) {
scripts.push({
id: 'g28_wall_' + wid, name: 'Стена_' + wid,
target: { kind: 'primitive', id: wid },
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);
});`,
});
}
scripts.push({
// скрипт вешаем на невидимую ЗОНУ, а не на плоский коврик
id: 'g28_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиш-зоны ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 60,
spawnPoint: { x: -0.5, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 29 — «Магазин»
// Собери монетки, потом купи у продавца ключ за 5 монет (E).
// С ключом открой дверь и дойди до финиша.
// ══════════════════════════════════════════════════════════════════
function game29Shop() {
const blocks = [];
for (let x = -9; x <= 8; x++) {
for (let z = -8; z <= 19; z++) {
blocks.push({ x, y: 0, z, type: 'wood' });
}
}
const primitives = [];
let id = 0;
// 7 монеток для сбора.
const coinSpots = [
{ x: -6, z: -5 }, { x: 6, z: -5 }, { x: 0, z: -6 },
{ x: -7, z: 1 }, { x: 7, z: 1 }, { x: -3, z: -2 }, { x: 4, z: -3 },
];
const coinIds = coinSpots.map((c) => {
id++;
primitives.push({
id, type: 'sphere', name: 'Монетка_' + id,
x: c.x, y: 1, z: c.z, sx: 0.6, sy: 0.6, sz: 0.6,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// Прилавок продавца.
const SHOP_ID = ++id;
primitives.push({
id: SHOP_ID, type: 'cube', name: 'Прилавок',
x: 0, y: 0.8, z: 5, sx: 5, sy: 1.6, sz: 1.5,
color: '#7a4a26', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Стена + дверь.
primitives.push({
id: ++id, type: 'cube', name: 'Стенаево',
x: -5, y: 2.5, z: 12, sx: 8, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
primitives.push({
id: ++id, type: 'cube', name: 'Стена_право',
x: 5, y: 2.5, z: 12, sx: 8, sy: 6, sz: 1,
color: '#8a8f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const DOOR_ID = ++id;
primitives.push({
id: DOOR_ID, type: 'cube', name: 'Дверь',
x: 0, y: 2.5, z: 12, sx: 3, sy: 6, sz: 0.8,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Видимый коврик-финиш НА полу (пол wood y=0, верх y=1 → y=1.1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 0, y: 1.1, z: 16, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока, когда он
// стоит на финише (onTouch плоской плитки сам по себе не срабатывает).
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 0, y: 1 + 1.25, z: 16, sx: 4, sy: 2.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g29_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
for (const cid of coinIds) {
scripts.push({
id: 'g29_coin_' + cid, name: 'Монетка_' + cid,
target: { kind: 'primitive', id: cid },
code:
`// === Скрипт монетки ===
game.self.onTouch(() => {
game.broadcast('coin');
game.self.delete();
});`,
});
}
scripts.push({
id: 'g29_shop', name: 'Прилавок', target: { kind: 'primitive', id: SHOP_ID },
code:
`// === Скрипт прилавка ===
game.self.onInteract(() => {
game.broadcast('buy');
}, { text: 'Купить ключ (5 монет)', distance: 4 });`,
});
scripts.push({
id: 'g29_door', name: 'Дверь', target: { kind: 'primitive', id: DOOR_ID },
code:
`// === Скрипт двери ===
game.self.onInteract(() => {
game.broadcast('open-door');
}, { text: 'Открыть дверь', distance: 4 });`,
});
scripts.push({
// скрипт вешаем на невидимую ЗОНУ, а не на плоский коврик
id: 'g29_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиш-зоны ===
game.self.onTouch(() => {
game.broadcast('win');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 60,
spawnPoint: { x: 0, y: 1, z: -3 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 30 — «Квест с заданиями»
// NPC даёт цепочку из 3 заданий: собери монетку, дойди до флага,
// вернись к NPC. Выполни всё — победа.
// ══════════════════════════════════════════════════════════════════
function game30QuestTasks() {
const blocks = [];
for (let x = -12; x <= 11; x++) {
for (let z = -8; z <= 23; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
const primitives = [];
let id = 0;
// Тумба-квестодатель (рядом стоит NPC).
const NPC_ID = ++id;
primitives.push({
id: NPC_ID, type: 'cube', name: 'Квестодатель',
x: 0, y: 0.8, z: 2, sx: 2, sy: 1.6, sz: 2,
color: '#7a4a26', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Монетка для задания 1.
const COIN_ID = ++id;
primitives.push({
id: COIN_ID, type: 'sphere', name: 'КвестМонетка',
x: -8, y: 1, z: 12, sx: 0.7, sy: 0.7, sz: 0.7,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Флаг для задания 2.
const FLAG_ID = ++id;
primitives.push({
id: FLAG_ID, type: 'cone', name: 'КвестФлаг',
x: 9, y: 1.2, z: 18, sx: 1.2, sy: 2.4, sz: 1.2,
color: '#3357ff', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g30_main', name: 'Главный скрипт', target: null,
code:
`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
// этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага,
// 3=вернуться к NPC, 4=готово
let stage = 0;
// Текущая цель квеста — ПОСТОЯННАЯ надпись вверху экрана (game.ui.set).
// Реплики npc.say исчезают через пару секунд, поэтому цель всегда
// держим на HUD отдельной меткой 'objective' — игрок не теряет нить.
function setObjective(text) {
game.ui.set('objective', 'ЦЕЛЬ: ' + text,
{ x: 50, y: 8, color: '#ffe066', size: 24 });
}
setObjective('подойди к квестодателю и нажми E');
// создаём 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);
setObjective('собери жёлтую монетку (слева)');
} else if (stage === 3) {
stage = 4;
npc.say('Молодец! Квест выполнен!', 4);
game.ui.set('objective', 'КВЕСТ ПРОЙДЕН!',
{ x: 50, y: 8, color: '#22dd55', size: 26 });
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 if (stage === 1) {
npc.say('Ты ещё не собрал монетку!', 3);
} else if (stage === 2) {
npc.say('Сначала дойди до синего флага!', 3);
}
});
game.onMessage('coin-done', () => {
if (stage !== 1) return;
stage = 2;
game.sound.play('coin');
npc.say('Отлично! Теперь дойди до синего флага.', 4);
game.ui.showText('Монетка собрана!', 2);
setObjective('дойди до синего флага (справа)');
});
game.onMessage('flag-done', () => {
if (stage !== 2) return;
stage = 3;
game.sound.play('pickup');
game.ui.showText('Флаг найден!', 2);
setObjective('вернись к квестодателю и нажми E');
});`,
});
scripts.push({
id: 'g30_npc', name: 'Квестодатель', target: { kind: 'primitive', id: NPC_ID },
code:
`// === Скрипт квестодателя ===
game.self.onInteract(() => {
game.broadcast('talk');
}, { text: 'Поговорить', distance: 4 });`,
});
scripts.push({
id: 'g30_coin', name: 'КвестМонетка', target: { kind: 'primitive', id: COIN_ID },
code:
`// === Скрипт квест-монетки ===
game.self.onTouch(() => {
game.broadcast('coin-done');
game.self.delete();
});`,
});
scripts.push({
id: 'g30_flag', name: 'КвестФлаг', target: { kind: 'primitive', id: FLAG_ID },
code:
`// === Скрипт квест-флага ===
game.self.onTouch(() => {
game.broadcast('flag-done');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 65,
spawnPoint: { x: 0, y: 1, z: -4 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 31 — «Защита базы»
// NPC-враги идут к твоей базе. Кликай по врагам, чтобы их убрать.
// Не пропусти 5 врагов до базы — иначе проигрыш.
// ══════════════════════════════════════════════════════════════════
function game31BaseDefense() {
const blocks = [];
for (let x = -10; x <= 9; x++) {
for (let z = -4; z <= 40; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// База — синий куб у игрока.
const BASE_ID = ++id;
primitives.push({
id: BASE_ID, type: 'cube', name: 'База',
x: 0, y: 1.5, z: 0, sx: 5, sy: 3, sz: 3,
color: '#3357ff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [{
id: 'g31_main', name: 'Главный скрипт', target: null,
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);
}
}
});
});`,
}];
return wrap({
blocks, primitives, scripts,
worldSize: 70,
spawnPoint: { x: 0, y: 1, z: 6 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 32 — «Гонка с кругами»
// Кольцевая трасса с чекпоинтами. Проедь 2 круга через чекпоинты
// по порядку. Засекается время.
// ══════════════════════════════════════════════════════════════════
function game32LapRace() {
// Квадратная кольцевая трасса.
const blocks = [];
const inside = (x, z) => (Math.abs(x) <= 6 && Math.abs(z) <= 6);
for (let x = -14; x <= 14; x++) {
for (let z = -14; z <= 14; z++) {
if (inside(x, z)) continue; // дырка в центре
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
// 4 чекпоинта по углам трассы.
const cpSpots = [
{ x: 0, z: -11 }, { x: 11, z: 0 },
{ x: 0, z: 11 }, { x: -11, z: 0 },
];
const cpIds = cpSpots.map((c, i) => {
primitives.push({
id: i + 1, type: 'cube', name: екпоинт_' + (i + 1),
x: c.x, y: 1.5, z: c.z, sx: 4, sy: 3, sz: 0.5,
color: i === 0 ? '#22dd55' : '#ffcc33', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return i + 1;
});
const scripts = [];
scripts.push({
id: 'g32_main', name: 'Главный скрипт', target: null,
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.ui.set).
// showText гаснет за пару секунд, а игрок должен всё время видеть,
// какой круг и какой чекпоинт ему нужен.
function updateProgress() {
game.ui.set('race',
'Круг ' + (lap + 1) + '/' + LAPS + ' • чекпоинт ' + (nextCp + 1) + '/' + CP_COUNT,
{ x: 50, y: 8, color: '#ffe066', size: 22 });
}
updateProgress();
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.set('race', 'ФИНИШ! ' + t + ' сек',
{ x: 50, y: 8, color: '#22dd55', size: 24 });
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);
updateProgress();
}
} else {
updateProgress();
}
});`,
});
cpIds.forEach((cid, idx) => {
scripts.push({
id: 'g32_cp_' + (idx + 1), name: екпоинт_' + (idx + 1),
target: { kind: 'primitive', id: cid },
code:
`// === Скрипт чекпоинта ${idx + 1} ===
game.self.onTouch(() => {
game.broadcast('checkpoint', { num: ${idx + 1} });
});`,
});
});
return wrap({
blocks, primitives, scripts,
worldSize: 70,
spawnPoint: { x: -3, y: 1, z: -11 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 33 — «Платформер с боссом»
// Пройди паркур до арены, там — NPC-босс. Кликай по боссу, пока
// у него не кончится HP.
// ══════════════════════════════════════════════════════════════════
function game33BossPlatformer() {
const blocks = [];
// Старт.
for (let x = -3; x <= 2; x++) {
for (let z = -3; z <= 0; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Арена босса.
for (let x = -8; x <= 7; x++) {
for (let z = 24; z <= 38; z++) {
blocks.push({ x, y: 6, z, type: 'rock' });
}
}
const primitives = [];
// Платформы паркура от старта (пол y=0, верх y=1) к арене (y=6, верх y=7).
// Подъём равномерный ~1.2 на платформу — допрыгивается обычным прыжком.
// Разрыв по z=4, лёгкий x-зигзаг.
const plats = [
{ x: 0, y: 2.0, z: 5 },
{ x: 1.5, y: 3.2, z: 9 },
{ x: -1.5, y: 4.4, z: 13 },
{ x: 0, y: 5.6, z: 17 },
];
plats.forEach((p, i) => {
primitives.push({
id: i + 1, type: 'cube', name: 'Платформа_' + (i + 1),
x: p.x, y: p.y, z: p.z, sx: 2.5, sy: 0.5, sz: 2.5,
color: '#9b6b3e', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
});
const scripts = [{
id: 'g33_main', name: 'Главный скрипт', target: null,
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');
}
});
}
});`,
}];
return wrap({
blocks, primitives, scripts,
worldSize: 70,
floorEnabled: false, // паркур над пустотой до арены босса
spawnPoint: { x: 0, y: 1, z: -2 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 34 — «Сбор урожая»
// Грядки. Растения растут (твин). Когда выросло — собирай (E).
// Собрал слишком рано или поздно — не считается. Собери 6 спелых.
// ══════════════════════════════════════════════════════════════════
function game34Harvest() {
const blocks = [];
for (let x = -9; x <= 8; x++) {
for (let z = -9; z <= 8; z++) {
blocks.push({ x, y: 0, z, type: 'dirt' });
}
}
const primitives = [];
// 6 растений — конусы, изначально маленькие.
const plantSpots = [
{ x: -6, z: -6 }, { x: 0, z: -6 }, { x: 6, z: -6 },
{ x: -6, z: 4 }, { x: 0, z: 4 }, { x: 6, z: 4 },
];
// Растения стоят НА земле (пол dirt y=0, верх y=1). Конус sy=0.5 →
// центр y=1.25, чтобы низ конуса был на земле y=1 (иначе утоплен).
const plantIds = plantSpots.map((p, i) => {
primitives.push({
id: i + 1, type: 'cone', name: 'Растение_' + (i + 1),
x: p.x, y: 1.25, z: p.z, sx: 0.4, sy: 0.5, sz: 0.4,
color: '#7cb342', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return i + 1;
});
const scripts = [];
scripts.push({
id: 'g34_main', name: 'Главный скрипт', target: null,
code:
`// === ИГРА «СБОР УРОЖАЯ» — главный скрипт ===
let harvested = 0;
const GOAL = ${plantIds.length};
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 });
}
});`,
});
plantIds.forEach((pid) => {
scripts.push({
id: 'g34_plant_' + pid, name: 'Растение_' + pid,
target: { kind: 'primitive', id: pid },
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 });`,
});
});
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 35 — «Прятки от NPC»
// NPC-искатель ходит по полю. Прячься за стенами, чтобы он тебя
// не «увидел» (не подошёл близко на открытом месте). Доживи до конца.
// ══════════════════════════════════════════════════════════════════
function game35HideFromNpc() {
const blocks = [];
for (let x = -13; x <= 12; x++) {
for (let z = -13; z <= 12; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Укрытия — стены-кубы по полю.
const primitives = [];
let id = 0;
const wallSpots = [
{ x: -7, z: -5 }, { x: 5, z: -7 }, { x: -3, z: 4 },
{ x: 8, z: 3 }, { x: 0, z: 9 }, { x: -9, z: 7 }, { x: 6, z: 9 },
];
for (const w of wallSpots) {
id++;
primitives.push({
id, type: 'cube', name: 'Укрытие_' + id,
x: w.x, y: 2, z: w.z, sx: 4, sy: 4, sz: 1.5,
color: '#6b7280', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
const scripts = [{
id: 'g35_main', name: 'Главный скрипт', target: null,
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 });
}
});`,
}];
return wrap({
blocks, primitives, scripts,
worldSize: 70,
spawnPoint: { x: -10, y: 1, z: -10 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 36 — «Головоломка с ящиками»
// 3 ящика и 3 плиты-цели. Нажимай E на ящике — он перепрыгивает
// на следующую клетку. Расставь все ящики по своим плитам.
// ══════════════════════════════════════════════════════════════════
function game36BoxPuzzle() {
const blocks = [];
for (let x = -9; x <= 8; x++) {
for (let z = -9; z <= 8; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// 3 плиты-цели — зелёные плоские круги-коврики НА полу
// (пол greystone y=0, верх y=1 → плита y=1.1, иначе утоплена).
const plateSpots = [
{ x: -6, z: 6 }, { x: 0, z: 6 }, { x: 6, z: 6 },
];
for (const p of plateSpots) {
id++;
primitives.push({
id, type: 'cylinder', name: 'Плита_' + id,
x: p.x, y: 1.1, z: p.z, sx: 2.4, sy: 0.2, sz: 2.4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// 3 ящика — куб 1.8, стоит НА полу: центр y = 1 + 0.9 = 1.9.
const boxIds = [];
for (let b = 0; b < 3; b++) {
id++;
boxIds.push(id);
primitives.push({
id, type: 'cube', name: 'Ящик_' + (b + 1),
x: -6 + b * 6, y: 1.9, z: -6, sx: 1.8, sy: 1.8, sz: 1.8,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
const scripts = [];
scripts.push({
id: 'g36_main', name: 'Главный скрипт', target: null,
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 });
}
});`,
});
boxIds.forEach((bid, idx) => {
// ящик idx ходит по ряду z = -6,-3,0,3,6; плита на z=6
scripts.push({
id: 'g36_box_' + (idx + 1), name: 'Ящик_' + (idx + 1),
target: { kind: 'primitive', id: bid },
code:
`// === Скрипт ящика ${idx + 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: ${idx}, on: z === PLATE_Z });
}, { text: 'Двинуть ящик', distance: 3 });`,
});
});
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: -8 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 37 — «Полоса препятствий»
// Длинная трасса: шипы (урон), движущаяся платформа, ямы. Доберись
// до финиша. Чекпоинт посередине.
// ══════════════════════════════════════════════════════════════════
function game37ObstacleCourse() {
const blocks = [];
// Трасса с ямами: ямы там, где z в диапазонах.
const pit = (z) => (z >= 14 && z <= 17) || (z >= 30 && z <= 33);
for (let x = -4; x <= 3; x++) {
for (let z = -3; z <= 48; z++) {
if (pit(z)) continue;
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// Шипы — конусы-урон в нескольких местах.
const spikeZ = [6, 9, 22, 25, 40, 43];
const spikeIds = spikeZ.map((z) => {
id++;
primitives.push({
id, type: 'cone', name: 'Шип_' + id,
x: (id % 2 === 0) ? -1 : 1, y: 0.8, z,
sx: 1.2, sy: 1.6, sz: 1.2,
color: '#ff3344', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// Движущаяся платформа над первой ямой — на уровне пола
// (пол-блоки y=0 верх y=1; платформа sy=0.5 → центр y=0.75,
// верх вровень с полом, иначе утоплена и игрок по ней не идёт).
const MOVER_ID = ++id;
primitives.push({
id: MOVER_ID, type: 'cube', name: 'ДвижПлатформа',
x: -0.5, y: 0.75, z: 15.5, sx: 3, sy: 0.5, sz: 3,
color: '#3357ff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Мост над второй ямой — узкая платформа на уровне пола.
primitives.push({
id: ++id, type: 'cube', name: 'Мостик',
x: -0.5, y: 0.75, z: 31.5, sx: 1.6, sy: 0.5, sz: 5,
color: '#9b6b3e', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Чекпоинт-флаг посередине.
const CP_ID = ++id;
primitives.push({
id: CP_ID, type: 'cone', name: 'Чекпоинт',
x: -0.5, y: 1.2, z: 24, sx: 1, sy: 2, sz: 1,
color: '#ffcc33', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш — видимый коврик НА полу (пол greystone y=0, верх y=1 → y=1.1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 46, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока, когда он
// стоит на финише (onTouch плоской плитки сам по себе не срабатывает).
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1 + 1.25, z: 46, sx: 4, sy: 2.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g37_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
for (const sid of spikeIds) {
scripts.push({
id: 'g37_spike_' + sid, name: 'Шип_' + sid,
target: { kind: 'primitive', id: sid },
code:
`// === Скрипт шипа ===
game.self.onTouch(() => {
game.player.damage(25);
game.sound.play('hit');
});`,
});
}
scripts.push({
id: 'g37_cp', name: 'Чекпоинт', target: { kind: 'primitive', id: CP_ID },
code:
`// === Скрипт чекпоинта ===
game.self.onTouch(() => {
game.broadcast('checkpoint');
});`,
});
scripts.push({
// скрипт вешаем на невидимую ЗОНУ, а не на плоский коврик
id: 'g37_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиш-зоны ===
game.self.onTouch(() => {
game.broadcast('finish');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 75,
floorEnabled: false, // трасса с ямами — в ямы нужно падать
spawnPoint: { x: -0.5, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 38 — «Музыкальная игра»
// Игра проигрывает последовательность звуков и подсвечивает плитки.
// Повтори последовательность, нажимая плитки (E) в том же порядке.
// ══════════════════════════════════════════════════════════════════
function game38MusicGame() {
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -6; z <= 6; z++) {
blocks.push({ x, y: 0, z, type: 'cotton-tan' });
}
}
const primitives = [];
// 4 цветные плитки.
const tiles = [
{ x: -5, color: '#e23b3b', snd: 'coin' },
{ x: -2, color: '#facc15', snd: 'jump' },
{ x: 2, color: '#22c55e', snd: 'click' },
{ x: 5, color: '#3b82f6', snd: 'hit' },
];
// Ноты-кнопки кладём НА пол (пол cotton-tan y=0, верх y=1 → y=1.15),
// иначе они утоплены в пол и не видны.
tiles.forEach((t, i) => {
primitives.push({
id: i + 1, type: 'cube', name: 'Нота_' + (i + 1),
x: t.x, y: 1.15, z: 0, sx: 2.4, sy: 0.4, sz: 2.4,
color: t.color, material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
});
const scripts = [];
scripts.push({
id: 'g38_main', name: 'Главный скрипт', target: null,
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');
}
});`,
});
tiles.forEach((t, i) => {
scripts.push({
id: 'g38_tile_' + (i + 1), name: 'Нота_' + (i + 1),
target: { kind: 'primitive', id: i + 1 },
code:
`// === Скрипт ноты-плитки ${i + 1} ===
game.self.onInteract(() => {
game.sound.play('${t.snd}');
game.scene.spawnParticles('sparks', game.self.position,
{ duration: 0.4, color: '${t.color}' });
game.broadcast('press', { n: ${i + 1} });
}, { text: 'Сыграть ноту', distance: 3 });`,
});
});
return wrap({
blocks, primitives, scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 1, z: 4 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 39 — «Башня — стройка»
// Скрипт говорит, где поставить блок. Подходи к подсвеченным местам
// и нажимай E — блок встаёт. Построй башню из 8 блоков.
// ══════════════════════════════════════════════════════════════════
function game39TowerBuild() {
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -8; z <= 7; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
const primitives = [];
// 8 «мест-призраков» — полупрозрачные кубы один над другим.
const STEPS = 8;
for (let i = 0; i < STEPS; i++) {
primitives.push({
id: i + 1, type: 'cube', name: 'Место_' + (i + 1),
x: 0, y: 1 + i * 2, z: 0, sx: 2, sy: 2, sz: 2,
color: '#88aaff', material: 'glass',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
const scripts = [];
scripts.push({
id: 'g39_main', name: 'Главный скрипт', target: null,
code:
`// === ИГРА «БАШНЯ — СТРОЙКА» — главный скрипт ===
const STEPS = ${STEPS};
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);
}
});`,
});
for (let i = 1; i <= STEPS; i++) {
scripts.push({
id: 'g39_spot_' + i, name: 'Место_' + i,
target: { kind: 'primitive', id: i },
code:
`// === Скрипт места под блок ${i} ===
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: ${i} });
}, { text: 'Поставить блок', distance: 4 });`,
});
}
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: 5 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 40 — «Выживание от волн»
// Волны NPC-врагов нападают одна за другой. Кликай по врагам.
// Продержись 3 волны — победа.
// ══════════════════════════════════════════════════════════════════
function game40WaveSurvival() {
const blocks = [];
for (let x = -12; x <= 11; x++) {
for (let z = -12; z <= 11; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
const scripts = [{
id: 'g40_main', name: 'Главный скрипт', target: null,
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 секунды`,
}];
return wrap({
blocks, primitives: [], scripts,
worldSize: 70,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 41 — «Платформер-приключение»
// Большой уровень: паркур, монетки, шипы, чекпоинт, финиш-сокровище.
// Собирает вместе всё из простых уроков.
// ══════════════════════════════════════════════════════════════════
function game41AdventurePlatformer() {
const blocks = [];
// Старт.
for (let x = -3; x <= 2; x++) {
for (let z = -3; z <= 0; z++) blocks.push({ x, y: 0, z, type: 'grass' });
}
// Площадка-чекпоинт посередине.
for (let x = -3; x <= 2; x++) {
for (let z = 26; z <= 30; z++) blocks.push({ x, y: 6, z, type: 'rock' });
}
// Финишная площадка.
for (let x = -3; x <= 2; x++) {
for (let z = 52; z <= 57; z++) blocks.push({ x, y: 11, z, type: 'rock' });
}
const primitives = [];
let id = 0;
// платформы паркура
const plats = [
{ x: 0, y: 1.5, z: 5 }, { x: 2, y: 2.5, z: 10 }, { x: -2, y: 3.5, z: 15 },
{ x: 0, y: 4.5, z: 20 }, { x: 2, y: 5.5, z: 24 },
{ x: 0, y: 7, z: 34 }, { x: -2, y: 8, z: 39 }, { x: 2, y: 9, z: 44 },
{ x: 0, y: 10, z: 49 },
];
for (const p of plats) {
id++;
primitives.push({
id, type: 'cube', name: 'Платформа_' + id,
x: p.x, y: p.y, z: p.z, sx: 2.5, sy: 0.5, sz: 2.5,
color: '#9b6b3e', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// монетки на платформах
const coinIds = [];
const coinPlats = [5, 15, 24, 39, 49];
for (const cz of coinPlats) {
id++;
coinIds.push(id);
const pl = plats.find((p) => p.z === cz);
primitives.push({
id, type: 'sphere', name: 'Монетка_' + id,
x: pl.x, y: pl.y + 1.5, z: pl.z, sx: 0.6, sy: 0.6, sz: 0.6,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
// чекпоинт-флаг
const CP_ID = ++id;
primitives.push({
id: CP_ID, type: 'cone', name: 'Чекпоинт',
x: -0.5, y: 7.2, z: 28, sx: 1, sy: 2, sz: 1,
color: '#ffcc33', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// финиш-сокровище
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Сокровище',
x: -0.5, y: 12, z: 54, sx: 2, sy: 2, sz: 2,
color: '#ffd700', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g41_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
for (const cid of coinIds) {
scripts.push({
id: 'g41_coin_' + cid, name: 'Монетка_' + cid,
target: { kind: 'primitive', id: cid },
code:
`// === Скрипт монетки ===
game.self.onTouch(() => {
game.broadcast('coin');
game.self.delete();
});`,
});
}
scripts.push({
id: 'g41_cp', name: 'Чекпоинт', target: { kind: 'primitive', id: CP_ID },
code:
`// === Скрипт чекпоинта ===
game.self.onTouch(() => {
game.broadcast('checkpoint');
});`,
});
scripts.push({
id: 'g41_finish', name: 'Сокровище', target: { kind: 'primitive', id: FINISH_ID },
code:
`// === Скрипт сокровища ===
game.self.onTouch(() => {
game.broadcast('treasure');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 90,
floorEnabled: false, // паркур с ямами — пола нет
spawnPoint: { x: 0, y: 1, z: -2 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 42 — «RPG-деревня»
// Деревня с двумя NPC и квестом: поговори со старостой → найди
// потерянный амулет → отнеси кузнецу → получи награду.
// ══════════════════════════════════════════════════════════════════
function game42RpgVillage() {
const blocks = [];
for (let x = -16; x <= 15; x++) {
for (let z = -10; z <= 21; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Два «домика» из блоков.
for (let x = -13; x <= -8; x++) {
for (let z = 5; z <= 10; z++) {
for (let h = 1; h <= 4; h++) blocks.push({ x, y: h, z, type: 'wood' });
}
}
for (let x = 9; x <= 14; x++) {
for (let z = 5; z <= 10; z++) {
for (let h = 1; h <= 4; h++) blocks.push({ x, y: h, z, type: 'brick-red' });
}
}
const primitives = [];
let id = 0;
// Тумба старосты.
const ELDER_ID = ++id;
primitives.push({
id: ELDER_ID, type: 'cube', name: 'Староста',
x: 0, y: 0.8, z: 2, sx: 1.8, sy: 1.6, sz: 1.8,
color: '#7a4a26', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Тумба кузнеца.
const SMITH_ID = ++id;
primitives.push({
id: SMITH_ID, type: 'cube', name: 'Кузнец',
x: 11, y: 0.8, z: 7, sx: 1.8, sy: 1.6, sz: 1.8,
color: '#4a4a4a', material: 'metal',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Амулет — спрятан за дальним домом.
const AMULET_ID = ++id;
primitives.push({
id: AMULET_ID, type: 'torus', name: 'Амулет',
x: -11, y: 1, z: 16, sx: 1, sy: 0.5, sz: 1,
color: '#a855f7', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g42_main', name: 'Главный скрипт', target: null,
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);
}
});`,
});
scripts.push({
id: 'g42_elder', name: 'Староста', target: { kind: 'primitive', id: ELDER_ID },
code:
`// === Скрипт старосты ===
game.self.onInteract(() => {
game.broadcast('elderTalk');
}, { text: 'Поговорить со старостой', distance: 4 });`,
});
scripts.push({
id: 'g42_smith', name: 'Кузнец', target: { kind: 'primitive', id: SMITH_ID },
code:
`// === Скрипт кузнеца ===
game.self.onInteract(() => {
game.broadcast('smithTalk');
}, { text: 'Поговорить с кузнецом', distance: 4 });`,
});
scripts.push({
id: 'g42_amulet', name: 'Амулет', target: { kind: 'primitive', id: AMULET_ID },
code:
`// === Скрипт амулета ===
game.self.onTouch(() => {
game.broadcast('takeAmulet');
game.self.delete();
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 90,
spawnPoint: { x: 0, y: 1, z: -4 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 43 — «Гонка с препятствиями»
// Трасса с бустами скорости и шипами-ловушками. Доедь до финиша
// на время. Буст ускоряет, шип — урон и замедление.
// ══════════════════════════════════════════════════════════════════
function game43ObstacleRace() {
const blocks = [];
for (let x = -4; x <= 3; x++) {
for (let z = 0; z <= 70; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
blocks.push({ x: -4, y: 1, z: 0, type: 'rock' });
}
for (let z = 0; z <= 70; z++) {
blocks.push({ x: -4, y: 1, z, type: 'rock' });
blocks.push({ x: 4, y: 1, z, type: 'rock' });
}
const primitives = [];
let id = 0;
// бусты — синие плитки-ускорители
const boostZ = [12, 30, 50];
const boostIds = boostZ.map((z) => {
id++;
primitives.push({
id, type: 'cube', name: уст_' + id,
x: -0.5, y: 1.15, z, sx: 4, sy: 0.3, sz: 2,
color: '#22aaff', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// шипы-ловушки
const spikeZ = [20, 22, 40, 42, 60];
const spikeIds = spikeZ.map((z) => {
id++;
primitives.push({
id, type: 'cone', name: 'Шип_' + id,
x: (id % 2 === 0) ? -2 : 2, y: 0.8, z,
sx: 1.2, sy: 1.6, sz: 1.2,
color: '#ff3344', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 68, sx: 8, sy: 0.2, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА в полный рост над ковриком — ловит игрока
// телом (тонкий коврик ноги «не касаются»).
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1 + 1.25, z: 68, sx: 8, sy: 2.5, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g43_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
for (const bid of boostIds) {
scripts.push({
id: 'g43_boost_' + bid, name: уст_' + bid,
target: { kind: 'primitive', id: bid },
code:
`// === Скрипт буста ===
game.self.onTouch(() => {
game.broadcast('boost');
});`,
});
}
for (const sid of spikeIds) {
scripts.push({
id: 'g43_spike_' + sid, name: 'Шип_' + sid,
target: { kind: 'primitive', id: sid },
code:
`// === Скрипт шипа-ловушки ===
game.self.onTouch(() => {
game.broadcast('spike');
});`,
});
}
scripts.push({
id: 'g43_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
game.self.onTouch(() => {
game.broadcast('finish');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 90,
spawnPoint: { x: -0.5, y: 1, z: 2 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 44 — «Tower Defense»
// Враги идут по дороге к базе. Ставь башни (E на площадках) — башни
// сами стреляют по врагам рядом. Не пропусти 8 врагов.
// ══════════════════════════════════════════════════════════════════
function game44TowerDefense() {
const blocks = [];
for (let x = -12; x <= 11; x++) {
for (let z = -4; z <= 44; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// Дорога врагов — полоса из камня по центру.
for (let x = -2; x <= 1; x++) {
for (let z = -4; z <= 44; z++) {
blocks.push({ x, y: 1, z, type: 'greystone' });
}
}
const primitives = [];
let id = 0;
// 4 площадки под башни — по бокам дороги.
const slotSpots = [
{ x: -6, z: 8 }, { x: 6, z: 14 }, { x: -6, z: 24 }, { x: 6, z: 32 },
];
const slotIds = slotSpots.map((s) => {
id++;
primitives.push({
id, type: 'cube', name: 'Площадка_' + id,
x: s.x, y: 1, z: s.z, sx: 3, sy: 2, sz: 3,
color: '#888f99', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// База в конце дороги.
const BASE_ID = ++id;
primitives.push({
id: BASE_ID, type: 'cube', name: 'База',
x: -0.5, y: 2, z: 42, sx: 5, sy: 4, sz: 3,
color: '#3357ff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g44_main', name: 'Главный скрипт', target: null,
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);
}
}
}
});`,
});
slotIds.forEach((sid) => {
scripts.push({
id: 'g44_slot_' + sid, name: 'Площадка_' + sid,
target: { kind: 'primitive', id: sid },
code:
`// === Скрипт площадки под башню ===
let built = false;
game.self.onInteract(() => {
if (built) return;
built = true;
const pos = game.self.position;
// === ВАРИАНТ 1: башня — жёлтый цилиндр (по умолчанию) ===
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',
});
// === ВАРИАНТ 2: башня — своя модель из редактора моделей ===
//
// Чтобы поставить вместо цилиндра свою воксельную модель:
// 1. В редакторе проекта открой панель «Мои модели» и создай
// башню в редакторе воксельных моделей.
// 2. Узнай её id — он показан рядом с названием модели в панели
// «Мои модели» (например 3).
// 3. ЗАКОММЕНТИРУЙ блок выше (Вариант 1) и используй такой код:
//
// game.scene.spawn('user:3', { // 3 — id твоей модели
// x: pos.x, y: pos.y + 2, z: pos.z,
// rotationY: 0, // поворот вокруг вертикали
// });
//
// У пользовательских моделей нет sx/sy/sz и color — размер задан
// самой моделью в редакторе, цвет — её вокселями. Только позиция
// и поворот по Y. Высоту y подбери под высоту своей модели.
game.broadcast('addTower', { x: pos.x, z: pos.z });
}, { text: 'Построить башню', distance: 4 });`,
});
});
return wrap({
blocks, primitives, scripts,
worldSize: 80,
spawnPoint: { x: -7, y: 1, z: 6 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 45 — «Стрелялка-арена»
// Арена с врагами. Кликай по врагам — они гибнут. Враги наносят
// урон при касании. Перебей 15 врагов, не потеряв всё HP.
// ══════════════════════════════════════════════════════════════════
function game45ArenaShooter() {
const blocks = [];
for (let x = -13; x <= 12; x++) {
for (let z = -13; z <= 12; z++) {
blocks.push({ x, y: 0, z, type: 'rock' });
}
}
// стены арены
for (let i = -13; i <= 12; i++) {
for (let h = 1; h <= 3; h++) {
blocks.push({ x: i, y: h, z: -13, type: 'greystone' });
blocks.push({ x: i, y: h, z: 12, type: 'greystone' });
blocks.push({ x: -13, y: h, z: i, type: 'greystone' });
blocks.push({ x: 12, y: h, z: i, type: 'greystone' });
}
}
const scripts = [{
id: 'g45_main', name: 'Главный скрипт', target: null,
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 });
}
}
});
});`,
}];
return wrap({
blocks, primitives: [], scripts,
worldSize: 75,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 46 — «Кликер»
// Кликай по большому кубу — копятся очки. На очки покупай улучшения
// (E на кнопках): +урон за клик и авто-доход. Накопи 200 очков.
// ══════════════════════════════════════════════════════════════════
function game46Clicker() {
const blocks = [];
for (let x = -8; x <= 7; x++) {
for (let z = -8; z <= 7; z++) {
blocks.push({ x, y: 0, z, type: 'cotton-green' });
}
}
const primitives = [];
let id = 0;
// Большой куб-кликер в центре.
const CUBE_ID = ++id;
primitives.push({
id: CUBE_ID, type: 'cube', name: 'Кликер',
x: 0, y: 2.5, z: 0, sx: 3, sy: 3, sz: 3,
color: '#ffcc33', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Кнопка апгрейда «сила клика».
const UP1_ID = ++id;
primitives.push({
id: UP1_ID, type: 'cube', name: 'УлучшениеСила',
x: -6, y: 1, z: 4, sx: 2, sy: 2, sz: 2,
color: '#e23b3b', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Кнопка апгрейда «авто-доход».
const UP2_ID = ++id;
primitives.push({
id: UP2_ID, type: 'cube', name: 'УлучшениеАвто',
x: 6, y: 1, z: 4, sx: 2, sy: 2, sz: 2,
color: '#3357ff', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g46_main', name: 'Главный скрипт', target: null,
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);
});`,
});
scripts.push({
id: 'g46_cube', name: 'Кликер', target: { kind: 'primitive', id: CUBE_ID },
code:
`// === Скрипт куба-кликера ===
game.self.onClick(() => {
game.broadcast('click');
// куб слегка вспыхивает
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
});`,
});
scripts.push({
id: 'g46_up1', name: 'УлучшениеСила', target: { kind: 'primitive', id: UP1_ID },
code:
`// === Скрипт улучшения «сила клика» (20 очков) ===
game.self.onInteract(() => {
game.broadcast('buyPower');
}, { text: 'Купить +силу клика (20)', distance: 3 });`,
});
scripts.push({
id: 'g46_up2', name: 'УлучшениеАвто', target: { kind: 'primitive', id: UP2_ID },
code:
`// === Скрипт улучшения «авто-доход» (40 очков) ===
game.self.onInteract(() => {
game.broadcast('buyAuto');
}, { text: 'Купить авто-доход (40)', distance: 3 });`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 50,
spawnPoint: { x: 0, y: 1, z: 6 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 47 — «Квест-побег»
// Запертая комната. Найди 3 кнопки (одна спрятана), нажми все —
// откроется дверь выхода. Комната-головоломка.
// ══════════════════════════════════════════════════════════════════
function game47EscapeQuest() {
const blocks = [];
// Пол + стены закрытой комнаты 20×20.
for (let x = -10; x <= 9; x++) {
for (let z = -10; z <= 9; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
for (let i = -10; i <= 9; i++) {
for (let h = 1; h <= 4; h++) {
blocks.push({ x: i, y: h, z: -10, type: 'rock' });
blocks.push({ x: i, y: h, z: 9, type: 'rock' });
blocks.push({ x: -10, y: h, z: i, type: 'rock' });
// правая стена с проёмом для двери (z от -1 до 1)
if (!(i >= -1 && i <= 1)) blocks.push({ x: 9, y: h, z: i, type: 'rock' });
}
}
const primitives = [];
let id = 0;
// Дверь выхода в правой стене.
const DOOR_ID = ++id;
primitives.push({
id: DOOR_ID, type: 'cube', name: 'Дверь',
x: 9, y: 2, z: 0, sx: 1, sy: 4, sz: 3,
color: '#b5651d', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// 3 кнопки: 2 на виду, 1 спрятана за ящиком.
const btnSpots = [
{ x: -7, z: -7 }, { x: 6, z: 6 }, { x: -6, z: 7 },
];
const btnIds = btnSpots.map((b) => {
id++;
primitives.push({
id, type: 'cylinder', name: 'Кнопка_' + id,
x: b.x, y: 0.6, z: b.z, sx: 1.2, sy: 1.2, sz: 1.2,
color: '#e23b3b', material: 'neon',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
return id;
});
// Ящик, прячущий третью кнопку.
primitives.push({
id: ++id, type: 'cube', name: 'Ящик',
x: -6, y: 1.5, z: 5, sx: 2.5, sy: 3, sz: 2.5,
color: '#7a4a26', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Финиш за дверью — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = ++id;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: 13, y: 1.1, z: 0, sx: 4, sy: 0.2, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА в полный рост над ковриком — ловит игрока
// телом (тонкий коврик ноги «не касаются»).
const FINISH_ZONE_ID = ++id;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: 13, y: 1 + 1.25, z: 0, sx: 4, sy: 2.5, sz: 4,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [];
scripts.push({
id: 'g47_main', name: 'Главный скрипт', target: null,
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 });
});`,
});
btnIds.forEach((bid, idx) => {
scripts.push({
id: 'g47_btn_' + (idx + 1), name: 'Кнопка_' + (idx + 1),
target: { kind: 'primitive', id: bid },
code:
`// === Скрипт кнопки ${idx + 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 });`,
});
});
scripts.push({
id: 'g47_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
game.self.onTouch(() => {
game.broadcast('escape');
});`,
});
return wrap({
blocks, primitives, scripts,
worldSize: 55,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 48 — «Мультиплеер: Салки»
// Догонялки на несколько игроков. Один водящий — он догоняет.
// В одиночку открывается как демо; настоящая игра — в комнате.
// ══════════════════════════════════════════════════════════════════
function game48MpTag() {
const blocks = [];
for (let x = -14; x <= 13; x++) {
for (let z = -14; z <= 13; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
// укрытия
const primitives = [];
let id = 0;
const wallSpots = [
{ x: -7, z: -6 }, { x: 6, z: -7 }, { x: -3, z: 5 },
{ x: 8, z: 4 }, { x: 0, z: 9 }, { x: -9, z: 8 },
];
for (const w of wallSpots) {
id++;
primitives.push({
id, type: 'cube', name: 'Укрытие_' + id,
x: w.x, y: 2, z: w.z, sx: 3.5, sy: 4, sz: 1.5,
color: '#6b7280', material: 'matte',
canCollide: true, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
}
const scripts = [{
id: 'g48_main', name: 'Главный скрипт', target: null,
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);
}
});`,
}];
return wrap({
blocks, primitives, scripts,
worldSize: 80,
spawnPoint: { x: 0, y: 1, z: 0 },
multiplayer: true,
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 49 — «Мультиплеер: Гонка»
// Соревнование игроков на трассе. Кто первым доедет до финиша —
// его имя в общем счёте комнаты.
// ══════════════════════════════════════════════════════════════════
function game49MpRace() {
const blocks = [];
for (let x = -5; x <= 4; x++) {
for (let z = 0; z <= 70; z++) {
blocks.push({ x, y: 0, z, type: 'greystone' });
}
}
for (let z = 0; z <= 70; z++) {
blocks.push({ x: -5, y: 1, z, type: 'rock' });
blocks.push({ x: 4, y: 1, z, type: 'rock' });
}
const primitives = [];
// Финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
const FINISH_ID = 1;
primitives.push({
id: FINISH_ID, type: 'cube', name: 'Финиш',
x: -0.5, y: 1.1, z: 68, sx: 10, sy: 0.2, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: true, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
// Невидимая ФИНИШ-ЗОНА в полный рост над ковриком — ловит игрока
// телом (тонкий коврик ноги «не касаются»).
const FINISH_ZONE_ID = 2;
primitives.push({
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
x: -0.5, y: 1 + 1.25, z: 68, sx: 10, sy: 2.5, sz: 2,
color: '#22dd55', material: 'neon',
canCollide: false, visible: false, anchored: true, mass: 1,
rotationX: 0, rotationY: 0, rotationZ: 0,
});
const scripts = [
{
id: 'g49_main', name: 'Главный скрипт', target: null,
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);
}
});`,
},
{
id: 'g49_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
code:
`// === Скрипт финиша ===
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
game.self.onTouch(() => {
game.broadcast('finish');
});`,
},
];
return wrap({
blocks, primitives, scripts,
worldSize: 90,
spawnPoint: { x: -0.5, y: 1, z: 2 },
multiplayer: true,
});
}
// ══════════════════════════════════════════════════════════════════
// ИГРА 50 — «Своя игра»
// Не готовая игра, а чистая песочница: ровная площадка, пара блоков
// для примера и скрипт-приветствие. Урок учит придумывать свою игру.
// ══════════════════════════════════════════════════════════════════
function game50MakeYourOwn() {
const blocks = [];
// большая ровная площадка для творчества
for (let x = -15; x <= 14; x++) {
for (let z = -15; z <= 14; z++) {
blocks.push({ x, y: 0, z, type: 'grass' });
}
}
const scripts = [{
id: 'g50_main', name: 'Главный скрипт', target: null,
code:
`// === «СВОЯ ИГРА» — твоя песочница ===
//
// Это пустая площадка. Здесь ты придумываешь и собираешь
// СВОЮ игру с нуля. Удали этот текст и пиши свой код.
//
// С чего начать:
// 1. Реши, КАКАЯ это игра (паркур / гонка / стрелялка / квест).
// 2. Построй сцену из блоков и примитивов.
// 3. Поставь точку спавна.
// 4. Добавь цель — финиш, счёт или врагов.
// 5. Напиши скрипты, оживляющие игру.
//
// Всё, что нужно, ты уже знаешь из уроков 1-49. Удачи!
game.ui.showText('Твоя песочница! Создай свою игру', 4);`,
}];
return wrap({
blocks, primitives: [], scripts,
worldSize: 80,
spawnPoint: { x: 0, y: 1, z: 0 },
});
}
// ══════════════════════════════════════════════════════════════════
// Реестр билдеров. Ключ = id игры из docsGames.js.
// ══════════════════════════════════════════════════════════════════
export const GAME_BUILDERS = {
'collect-coins': game1CollectCoins,
'platform-jump': game2PlatformJump,
'dont-fall': game3DontFall,
'button-door': game4ButtonDoor,
'maze': game5Maze,
'color-tiles': game6ColorTiles,
'catch-falling': game7CatchFalling,
'run-to-finish': game8RunToFinish,
'traffic-light': game9TrafficLight,
'spring-jump': game10SpringJump,
'echo-room': game11EchoRoom,
'code-door': game12CodeDoor,
'trader': game13Trader,
'collect-by-tag': game14CollectByTag,
'shooting-range': game15ShootingRange,
'lava-floor': game16LavaFloor,
'key-chest': game17KeyChest,
'swing': game18Swing,
'elevator': game19Elevator,
'enemy-names': game20EnemyNames,
'chaser': game21Chaser,
'danger-zone': game22DangerZone,
'switches': game23Switches,
'falling-bridge': game24FallingBridge,
'flyby-camera': game25FlybyCamera,
'coin-magnet': game26CoinMagnet,
'double-jump': game27DoubleJump,
'ghost-walls': game28GhostWalls,
'shop': game29Shop,
'quest-tasks': game30QuestTasks,
'base-defense': game31BaseDefense,
'lap-race': game32LapRace,
'boss-platformer': game33BossPlatformer,
'harvest': game34Harvest,
'hide-from-npc': game35HideFromNpc,
'box-puzzle': game36BoxPuzzle,
'obstacle-course': game37ObstacleCourse,
'music-game': game38MusicGame,
'tower-build': game39TowerBuild,
'wave-survival': game40WaveSurvival,
'adventure-platformer': game41AdventurePlatformer,
'rpg-village': game42RpgVillage,
'obstacle-race': game43ObstacleRace,
'tower-defense': game44TowerDefense,
'arena-shooter': game45ArenaShooter,
'clicker': game46Clicker,
'escape-quest': game47EscapeQuest,
'mp-tag': game48MpTag,
'mp-race': game49MpRace,
'make-your-own': game50MakeYourOwn,
};
/** Есть ли готовый билдер (играбельная версия) для игры с таким id. */
export function hasGameBuilder(id) {
return typeof GAME_BUILDERS[id] === 'function';
}
/** Построить project_data для игры-урока. Возвращает объект или null. */
export function buildGameProject(id) {
const fn = GAME_BUILDERS[id];
return fn ? fn() : null;
}