/** * 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; }