/**
 * 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,
                // Платформа grass: x от -6 до 5 (blocks=1unit, центры [-5.5..5.5]).
                // Сетка 6×6 плиток (центры через 2) центрируем на [-5..5].
                x: -5 + c * 2, y: 1.15, z: -5 + 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';
}

// ══════════════════════════════════════════════════════════════════
// LUA_OVERRIDES — реестр Lua-версий скриптов для уроков.
// Структура: { gameId: { scriptId: 'lua code' | (script) => 'lua code' } }
// Если скрипт описан здесь — при buildGameProject(id, {lang:'lua'}) его
// code будет заменён на Lua-версию.
// См. docsGamesBuildersLua.js для содержимого.
// ══════════════════════════════════════════════════════════════════
import { LUA_OVERRIDES } from './docsGamesBuildersLua';

/** Построить project_data для игры-урока. Возвращает объект или null.
 *  opts.lang: 'js' (default) | 'lua' — на каком языке скрипты в копии.
 */
/**
 * Генерирует минимальный рабочий Lua-каркас для скрипта когда явной
 * Lua-реализации в LUA_OVERRIDES нет. Анализирует target и name чтобы
 * сделать что-то осмысленное:
 * - target=null (главный скрипт): показывает подсказку, слушает событие
 *   FinishReached и при срабатывании — конфетти + Победа
 * - target=primitive с именем содержащим "Финиш"/"Final": Touched →
 *   шлёт FinishReached
 * - target=primitive с любым другим именем: Touched → красит примитив
 *   в случайный цвет (визуальный feedback что скрипт работает)
 */
function generateFallbackLua(s, gameTitle) {
    const target = s.target;
    const name = s.name || s.id || '';
    const title = gameTitle || 'игра';
    // Главный скрипт (target=null)
    if (!target || target === null) {
        return `-- === ${name} (Lua, авто-каркас) ===
-- Полная Lua-версия этой игры пока в разработке.
-- Этот каркас обеспечивает базовое поведение: подсказка + победа на финише.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local function getEvent(eventName)
    local ev = ReplicatedStorage:FindFirstChild(eventName)
    if not ev then
        ev = Instance.new("BindableEvent")
        ev.Name = eventName
        ev.Parent = ReplicatedStorage
    end
    return ev
end

__rbxl_show_text("${title.replace(/"/g, '\\"')}", 3)

local winSound = Instance.new("Sound", workspace)
winSound.SoundId = "win"; winSound.Volume = 1

local won = false
local winEvent = getEvent("FinishReached")
winEvent.Event:Connect(function()
    if won then return end
    won = true
    winSound:Play()
    __rbxl_show_text("Победа!", 4)
    local px = __rbxl_player_x()
    local py = __rbxl_player_y()
    local pz = __rbxl_player_z()
    __rbxl_spawn_particles("confetti", px, py + 3, pz, 3, 3)
end)`;
    }
    // Скрипт на примитиве с именем "Финиш" / "ФинишЗона" / "Final"
    const isFinish = /финиш|финал|final/i.test(name);
    if (isFinish) {
        return `-- === ${name} (Lua, авто-каркас) ===
-- При касании игроком шлём событие победы.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local part = script.Parent
local fired = false

part.Touched:Connect(function(hit)
    if fired then return end
    local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
    if not h then return end
    fired = true
    local ev = ReplicatedStorage:FindFirstChild("FinishReached")
    if not ev then
        ev = Instance.new("BindableEvent")
        ev.Name = "FinishReached"
        ev.Parent = ReplicatedStorage
    end
    ev:Fire()
end)`;
    }
    // Общий каркас для любого target-примитива — Touched красит в случайный цвет
    return `-- === ${name} (Lua, авто-каркас) ===
-- Полная Lua-версия этого скрипта пока в разработке.
-- Базовое поведение: при касании предмет реагирует визуально.
local part = script.Parent
local touched = false

part.Touched:Connect(function(hit)
    if touched then return end
    local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
    if not h then return end
    touched = true
    -- Меняем цвет на яркий зелёный — простой feedback
    part.Color = Color3.fromRGB(60, 230, 80)
end)`;
}

export function buildGameProject(id, opts = {}) {
    const fn = GAME_BUILDERS[id];
    if (!fn) return null;
    const project = fn();
    if (opts.lang === 'lua' && project) {
        const scene = project.scene || {};
        if (Array.isArray(scene.scripts)) {
            const overrides = LUA_OVERRIDES[id] || {};
            // Извлекаем название игры из любого скрипта (для подсказки в fallback)
            let gameTitle = '';
            const mainScript = scene.scripts.find(s => !s.target);
            if (mainScript) {
                const m = /===\s*ИГРА\s*[«"](.+?)[»"]/i.exec(mainScript.code || '');
                if (m) gameTitle = m[1];
            }
            scene.scripts = scene.scripts.map(s => {
                if (s.language === 'lua') return s;
                // Приоритет: явный code_lua → override из реестра → авто-fallback.
                let luaCode = s.code_lua;
                if (!luaCode) {
                    const ov = overrides[s.id];
                    if (typeof ov === 'function') luaCode = ov(s);
                    else if (typeof ov === 'string') luaCode = ov;
                }
                if (!luaCode || !luaCode.trim()) {
                    luaCode = generateFallbackLua(s, gameTitle);
                }
                return {
                    ...s,
                    language: 'lua',
                    code: luaCode,
                    code_js: s.code_js || s.code,
                    code_lua: luaCode,
                };
            });
        }
    }
    return project;
}
