Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026). Содержит изменения которые делались в процессе подготовки прод-окружения: Фиксы импортов после выноса из minecraftia: - Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/) - Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/ - API.js скопирован из минки целиком (было 8 экспортов, стало 312) - Добавлены PLAYER_URL, MyButton_1, недостающие компоненты - Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require) Структура ассетов: - public/kubikon-templates/ → public/assets/kubikon-templates/ - public/kubikon-learn/ → public/assets/kubikon-learn/ - (код искал в /assets/, файлы лежали без /assets/) Навигация роутов внутри студии: - /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced) - /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X UI: - Новый компонент StudioHeader (61px, как в минке) + копия favicon - WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера - SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке) - Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык) Документация: - docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR - docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API - API_USAGE.md — список эндпоинтов backend - README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/ .gitignore: - public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6166 lines
243 KiB
JavaScript
6166 lines
243 KiB
JavaScript
/**
|
||
* docsGamesBuilders.js — генераторы project_data для игр-уроков вики (раздел K).
|
||
*
|
||
* Каждая функция-билдер возвращает объект, совместимый с
|
||
* BabylonScene.serialize() — как в templates.js. Игра-урок НЕ хранится
|
||
* в БД отдельной записью: это чистый код-генератор. Когда ученик жмёт
|
||
* «Открыть игру в редакторе», вика вызывает createProject со свежим
|
||
* project_data из билдера → создаётся НОВАЯ запись на ученика. Так
|
||
* исходник урока физически нельзя испортить — он живёт в этом коде,
|
||
* а каждый получает свою редактируемую копию.
|
||
*
|
||
* Ключ билдера в объекте GAME_BUILDERS совпадает с id игры в docsGames.js.
|
||
*/
|
||
|
||
// Обёртка scene → полный project_data (как wrap() в templates.js).
|
||
function wrap(scene) {
|
||
return {
|
||
version: 1,
|
||
scene: {
|
||
blocks: [],
|
||
models: [],
|
||
primitives: [],
|
||
folders: [],
|
||
scripts: [],
|
||
spawnPoint: { x: 0, y: 5, z: 0 },
|
||
playerModelType: 'character-a',
|
||
worldSize: 100,
|
||
shadowQuality: 'soft',
|
||
environment: null,
|
||
audio: null,
|
||
...scene,
|
||
},
|
||
editorCamera: null,
|
||
settings: {},
|
||
};
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 1 — «Собери монетки»
|
||
// Травяная площадка, 8 жёлтых сфер-монеток. Касание монетки —
|
||
// она исчезает, +1 очко. Собрал все — победа.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game1CollectCoins() {
|
||
const blocks = [];
|
||
// Травяная площадка 14×14 (от -7 до 6 по X и Z), пол на y=0.
|
||
for (let x = -7; x <= 6; x++) {
|
||
for (let z = -7; z <= 6; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Бортик по краю площадки из камня — чтобы не упасть.
|
||
for (let i = -7; i <= 6; i++) {
|
||
blocks.push({ x: i, y: 1, z: -7, type: 'greystone' });
|
||
blocks.push({ x: i, y: 1, z: 6, type: 'greystone' });
|
||
blocks.push({ x: -7, y: 1, z: i, type: 'greystone' });
|
||
blocks.push({ x: 6, y: 1, z: i, type: 'greystone' });
|
||
}
|
||
|
||
// 8 монеток — жёлтые сферы, расставлены по площадке.
|
||
const coinSpots = [
|
||
{ x: -4, z: -4 }, { x: 0, z: -4 }, { x: 4, z: -4 },
|
||
{ x: -4, z: 0 }, { x: 4, z: 0 },
|
||
{ x: -4, z: 4 }, { x: 0, z: 4 }, { x: 4, z: 4 },
|
||
];
|
||
const primitives = coinSpots.map((c, i) => ({
|
||
id: i + 1,
|
||
type: 'sphere',
|
||
name: 'Монетка_' + (i + 1),
|
||
x: c.x, y: 1.2, z: c.z,
|
||
sx: 0.6, sy: 0.6, sz: 0.6,
|
||
color: '#ffd700',
|
||
material: 'neon',
|
||
canCollide: false, // сквозь монетку проходим — касание ловит скрипт
|
||
visible: true,
|
||
anchored: true,
|
||
mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
}));
|
||
|
||
const TOTAL = coinSpots.length;
|
||
|
||
const scripts = [];
|
||
|
||
// Главный (глобальный) скрипт — счётчик и проверка победы.
|
||
scripts.push({
|
||
id: 'g1_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт ===
|
||
// Этот скрипт глобальный: считает собранные монетки и проверяет победу.
|
||
|
||
let score = 0; // сколько монеток собрано
|
||
const TOTAL = ${TOTAL}; // всего монеток на уровне
|
||
|
||
game.ui.score = score; // показать счёт 0 в углу
|
||
game.ui.showText('Собери все монетки!', 2); // подсказка на старте
|
||
|
||
// Монетки сообщают сюда о сборе через game.broadcast('coin').
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('coin', () => {
|
||
score = score + 1; // +1 к счёту
|
||
game.ui.score = score; // обновить счётчик на экране
|
||
game.sound.play('coin'); // звон монетки
|
||
|
||
if (score >= TOTAL) {
|
||
// собраны все — победа!
|
||
game.ui.showText('Победа! Все монетки твои!', 4);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z },
|
||
{ duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую монетку — реагирует на касание игроком.
|
||
for (let i = 0; i < TOTAL; i++) {
|
||
scripts.push({
|
||
id: 'g1_coin_' + (i + 1),
|
||
name: 'Монетка_' + (i + 1),
|
||
target: { kind: 'primitive', id: i + 1 },
|
||
code:
|
||
`// === Скрипт монетки ===
|
||
// Висит на монетке. game.self — это сама монетка.
|
||
|
||
game.self.onTouch(() => {
|
||
// игрок коснулся монетки — сообщить главному скрипту
|
||
game.broadcast('coin');
|
||
game.self.delete(); // монетка исчезает
|
||
});`,
|
||
});
|
||
}
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: 0 }, // игрок появляется в центре площадки
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 2 — «Прыгай по платформам»
|
||
// Паркур: 8 платформ-кубов в воздухе с разрывами. Допрыгай до финиша.
|
||
// Упал вниз — респаун на старте.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game2PlatformJump() {
|
||
// Пола нет — игра целиком на платформах в воздухе. Упал — респаун.
|
||
const blocks = [];
|
||
|
||
// Платформы-примитивы: куб 2×0.5×2, ступеньками вперёд по +Z
|
||
// с подъёмом и разрывами. Прыгать с одной на другую.
|
||
const plats = [
|
||
{ x: 0, y: 1.5, z: 4 },
|
||
{ x: 0, y: 2.5, z: 8 },
|
||
{ x: 2, y: 3.5, z: 12 },
|
||
{ x: -2, y: 4.0, z: 16 },
|
||
{ x: 0, y: 4.5, z: 20 },
|
||
{ x: 2, y: 5.5, z: 24 },
|
||
{ x: 0, y: 6.0, z: 28 },
|
||
];
|
||
const primitives = plats.map((p, i) => ({
|
||
id: i + 1,
|
||
type: 'cube',
|
||
name: 'Платформа_' + (i + 1),
|
||
x: p.x, y: p.y, z: p.z,
|
||
sx: 2, sy: 0.5, sz: 2,
|
||
color: '#9b6b3e',
|
||
material: 'matte',
|
||
canCollide: true, // на платформу можно встать
|
||
visible: true,
|
||
anchored: true, // платформа не падает
|
||
mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
}));
|
||
|
||
// Финишная площадка-куб — большая, зелёная неоновая. На неё встают.
|
||
const FINISH_ID = plats.length + 1;
|
||
primitives.push({
|
||
id: FINISH_ID,
|
||
type: 'cube',
|
||
name: 'Финиш',
|
||
x: 0, y: 6.5, z: 33,
|
||
sx: 4, sy: 0.5, sz: 4,
|
||
color: '#22dd55',
|
||
material: 'neon',
|
||
canCollide: true,
|
||
visible: true,
|
||
anchored: true,
|
||
mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Невидимая ФИНИШ-ЗОНА поверх площадки.
|
||
// ВАЖНО: onTouch тонкой плитки НЕ срабатывает, когда игрок просто
|
||
// СТОИТ на ней — капсула касается верха плитки, но не пересекает
|
||
// её объём. Поэтому ловим игрока высокой невидимой зоной
|
||
// (canCollide:false — игрок проходит сквозь, тело внутри зоны).
|
||
const FINISH_ZONE_ID = plats.length + 2;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID,
|
||
type: 'cube',
|
||
name: 'ФинишЗона',
|
||
x: 0, y: 6.5 + 1.75, z: 33, // центр зоны на 1.75 над плиткой
|
||
sx: 4, sy: 3, sz: 4, // высота 3 — игрок попадает телом
|
||
color: '#22dd55',
|
||
material: 'neon',
|
||
canCollide: false, // сквозь зону проходим
|
||
visible: false, // зона невидима
|
||
anchored: true,
|
||
mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Стартовая платформа — игрок появляется на ней (пола в игре нет).
|
||
const START_ID = plats.length + 3;
|
||
primitives.push({
|
||
id: START_ID,
|
||
type: 'cube',
|
||
name: 'Старт',
|
||
x: 0, y: 1.0, z: 0,
|
||
sx: 3, sy: 0.5, sz: 3,
|
||
color: '#6b7280',
|
||
material: 'matte',
|
||
canCollide: true,
|
||
visible: true,
|
||
anchored: true,
|
||
mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт — следит, не упал ли игрок, и обрабатывает победу.
|
||
scripts.push({
|
||
id: 'g2_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ПРЫГАЙ ПО ПЛАТФОРМАМ» — главный скрипт ===
|
||
|
||
let won = false; // победа уже была?
|
||
|
||
game.ui.showText('Допрыгай до зелёной площадки!', 3);
|
||
|
||
// Каждый кадр проверяем высоту игрока.
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
// упал ниже всех платформ — вернуть на старт
|
||
if (p && p.y < -3) {
|
||
game.player.respawn();
|
||
game.ui.showText('Упал! Пробуй снова.', 1.5);
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// Финиш сообщает сюда о победе через broadcast/onMessage:
|
||
// скрипты живут в РАЗНЫХ песочницах, общая переменная между ними
|
||
// не видна — связь только сообщениями.
|
||
game.onMessage('finish', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты дошёл до финиша!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z },
|
||
{ duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на финиш-ЗОНУ — ловит касание (игрок стоит на площадке,
|
||
// тело внутри невидимой зоны).
|
||
scripts.push({
|
||
id: 'g2_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
// Висит на невидимой зоне над зелёной площадкой.
|
||
// Игрок встал на площадку — его тело внутри зоны — победа.
|
||
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 80,
|
||
floorEnabled: false, // пола нет — игра в воздухе
|
||
spawnPoint: { x: 0, y: 1.3, z: 0 }, // на стартовой платформе
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 3 — «Не упади»
|
||
// Дорожка из плиток. Встал на плитку — через 1.2с она исчезает.
|
||
// Нужно всё время бежать вперёд. В конце — финиш.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game3DontFall() {
|
||
// Пола нет — дорожка из плиток висит в воздухе. Упал — респаун.
|
||
const blocks = [];
|
||
|
||
// Дорожка из 14 плиток-примитивов (куб 2×0.4×2) вперёд по +Z.
|
||
// Все на y=1.0 — игрок ходит по ним как по полу.
|
||
const TILES = 14;
|
||
const TILE_Y = 1.0;
|
||
const primitives = [];
|
||
for (let i = 0; i < TILES; i++) {
|
||
// лёгкий зигзаг влево-вправо, чтобы было интереснее
|
||
const x = (i % 4 === 1) ? 2 : (i % 4 === 3) ? -2 : 0;
|
||
primitives.push({
|
||
id: i + 1,
|
||
type: 'cube',
|
||
name: 'Плитка_' + (i + 1),
|
||
x, y: TILE_Y, z: 3 + i * 3,
|
||
sx: 2, sy: 0.4, sz: 2,
|
||
color: '#d9a441',
|
||
material: 'matte',
|
||
canCollide: true,
|
||
visible: true,
|
||
anchored: true,
|
||
mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
// Финишная площадка после последней плитки — на уровне плиток.
|
||
const FINISH_ID = TILES + 1;
|
||
primitives.push({
|
||
id: FINISH_ID,
|
||
type: 'cube',
|
||
name: 'Финиш',
|
||
x: 0, y: TILE_Y, z: 3 + TILES * 3,
|
||
sx: 4, sy: 0.5, sz: 4,
|
||
color: '#22dd55',
|
||
material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх площадки — ловит игрока, стоящего
|
||
// на финише (тонкую плитку онтач не ловит, нужна объёмная зона).
|
||
const FINISH_ZONE_ID = TILES + 2;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID,
|
||
type: 'cube',
|
||
name: 'ФинишЗона',
|
||
x: 0, y: TILE_Y + 0.25 + 1.5, z: 3 + TILES * 3,
|
||
sx: 4, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Стартовая платформа — игрок появляется на ней (пола нет).
|
||
const START_ID = TILES + 3;
|
||
primitives.push({
|
||
id: START_ID,
|
||
type: 'cube',
|
||
name: 'Старт',
|
||
x: 0, y: TILE_Y, z: -1,
|
||
sx: 4, sy: 0.5, sz: 4,
|
||
color: '#6b7280',
|
||
material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт — падение/респаун и победа.
|
||
scripts.push({
|
||
id: 'g3_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «НЕ УПАДИ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Беги вперёд! Плитки исчезают!', 3);
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
// упал вниз — на старт
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) {
|
||
game.player.respawn();
|
||
game.ui.showText('Упал! Снова.', 1.5);
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// Финиш сообщает о победе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('finish', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты добежал!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую плитку — исчезает через 1.2с после касания.
|
||
for (let i = 0; i < TILES; i++) {
|
||
scripts.push({
|
||
id: 'g3_tile_' + (i + 1),
|
||
name: 'Плитка_' + (i + 1),
|
||
target: { kind: 'primitive', id: i + 1 },
|
||
code:
|
||
`// === Скрипт исчезающей плитки ===
|
||
|
||
let triggered = false; // плитка уже запущена на исчезновение?
|
||
|
||
game.self.onTouch(() => {
|
||
if (triggered) return;
|
||
triggered = true;
|
||
game.sound.play('click');
|
||
// через 1.2 секунды плитка пропадает
|
||
game.after(1.2, () => {
|
||
game.self.delete();
|
||
});
|
||
});`,
|
||
});
|
||
}
|
||
|
||
// Финиш — скрипт на невидимой зоне (игрок входит телом).
|
||
scripts.push({
|
||
id: 'g3_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 80,
|
||
floorEnabled: false, // пола нет — плитки в воздухе
|
||
spawnPoint: { x: 0, y: TILE_Y + 0.3, z: -1 }, // на стартовой платформе
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 4 — «Кнопка-открывашка»
|
||
// Комната перегорожена стеной с дверью. Подойди к кнопке, нажми E —
|
||
// дверь уедет вверх. За дверью — финиш.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game4ButtonDoor() {
|
||
// Пол комнаты 16×24 из камня.
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -4; z <= 19; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let pid = 0;
|
||
const next = () => ++pid;
|
||
|
||
// Стена-перегородка поперёк комнаты на z=8: два куба по краям,
|
||
// в центре проём шириной 3, который закрыт дверью.
|
||
primitives.push({
|
||
id: next(), type: 'cube', name: 'Стена_лево',
|
||
x: -4.5, y: 2, z: 8, sx: 7, sy: 5, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
primitives.push({
|
||
id: next(), type: 'cube', name: 'Стена_право',
|
||
x: 4.5, y: 2, z: 8, sx: 7, sy: 5, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Дверь — куб в проёме. Её id запомним для скрипта.
|
||
const DOOR_ID = next();
|
||
primitives.push({
|
||
id: DOOR_ID, type: 'cube', name: 'Дверь',
|
||
x: 0, y: 2, z: 8, sx: 3, sy: 5, sz: 0.8,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Кнопка — цилиндр-тумба перед дверью.
|
||
const BUTTON_ID = next();
|
||
primitives.push({
|
||
id: BUTTON_ID, type: 'cylinder', name: 'Кнопка',
|
||
x: 3, y: 0.6, z: 3, sx: 1.2, sy: 1.2, sz: 1.2,
|
||
color: '#e23b3b', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Финиш — за дверью.
|
||
const FINISH_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 0.3, z: 16, sx: 4, sy: 0.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт — победа.
|
||
scripts.push({
|
||
id: 'g4_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «КНОПКА-ОТКРЫВАШКА» — главный скрипт ===
|
||
|
||
game.ui.showText('Подойди к красной кнопке и нажми E', 4);
|
||
|
||
// Финиш сообщает о победе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Дверь открыта, ты прошёл!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт кнопки — взаимодействие по E открывает дверь.
|
||
scripts.push({
|
||
id: 'g4_button',
|
||
name: 'Кнопка',
|
||
target: { kind: 'primitive', id: BUTTON_ID },
|
||
code:
|
||
`// === Скрипт кнопки ===
|
||
// Висит на красной кнопке-цилиндре.
|
||
|
||
let opened = false;
|
||
|
||
game.self.onInteract(() => {
|
||
if (opened) return;
|
||
opened = true;
|
||
game.sound.play('click');
|
||
|
||
// находим дверь по имени и плавно поднимаем её вверх
|
||
const door = game.scene.findOne('Дверь');
|
||
game.tween(door, { y: 8 }, { duration: 1.2, easing: 'ease' });
|
||
|
||
game.ui.showText('Дверь открывается!', 2);
|
||
}, {
|
||
text: 'Открыть дверь',
|
||
distance: 4
|
||
});`,
|
||
});
|
||
|
||
// Скрипт финиша.
|
||
scripts.push({
|
||
id: 'g4_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 5 — «Лабиринт»
|
||
// Лабиринт из стен. Дойди от старта (S) до выхода (F).
|
||
// Карта рисуется текстом: # стена, пробел проход, S старт, F финиш.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game5Maze() {
|
||
// Карта лабиринта 15×15. Каждый символ = клетка 2×2 единицы.
|
||
// # — стена (3 блока в высоту)
|
||
// ' ' — проход
|
||
// S — старт игрока
|
||
// F — финиш
|
||
const MAP = [
|
||
'###############',
|
||
'#S # #',
|
||
'# ### # ##### #',
|
||
'# # # # #',
|
||
'# # ####### # #',
|
||
'# # # # #',
|
||
'# ####### # # #',
|
||
'# # # #',
|
||
'##### # # #####',
|
||
'# # # # #',
|
||
'# # # ##### # #',
|
||
'# # # # # #',
|
||
'# # ##### # # #',
|
||
'# # F#',
|
||
'###############',
|
||
];
|
||
const CELL = 2; // размер клетки в единицах мира
|
||
const blocks = [];
|
||
let spawn = { x: 0, y: 1, z: 0 };
|
||
let finish = { x: 0, z: 0 };
|
||
|
||
for (let r = 0; r < MAP.length; r++) {
|
||
for (let c = 0; c < MAP[r].length; c++) {
|
||
const ch = MAP[r][c];
|
||
const wx = (c - 7) * CELL; // центрируем карту вокруг 0
|
||
const wz = (r - 7) * CELL;
|
||
// пол под каждой клеткой
|
||
for (let dx = 0; dx < CELL; dx++) {
|
||
for (let dz = 0; dz < CELL; dz++) {
|
||
blocks.push({ x: wx + dx, y: 0, z: wz + dz, type: 'greystone' });
|
||
}
|
||
}
|
||
if (ch === '#') {
|
||
// стена — 3 блока в высоту над клеткой
|
||
for (let h = 1; h <= 3; h++) {
|
||
for (let dx = 0; dx < CELL; dx++) {
|
||
for (let dz = 0; dz < CELL; dz++) {
|
||
blocks.push({ x: wx + dx, y: h, z: wz + dz, type: 'rock' });
|
||
}
|
||
}
|
||
}
|
||
} else if (ch === 'S') {
|
||
spawn = { x: wx + 0.5, y: 1, z: wz + 0.5 };
|
||
} else if (ch === 'F') {
|
||
finish = { x: wx + 0.5, z: wz + 0.5 };
|
||
}
|
||
}
|
||
}
|
||
|
||
// Финиш — зелёный коврик НА полу клетки F (пол лабиринта = блоки
|
||
// y=0, их верх y=1, поэтому коврик кладём на y≈1.1).
|
||
const FINISH_ID = 1;
|
||
// Невидимая ФИНИШ-ЗОНА в полный рост игрока над ковриком.
|
||
// ВАЖНО: тонкий коврик игрок не «касается» телом — его ноги на
|
||
// полу, коврик под ногами. Ловим высокой невидимой зоной
|
||
// (от пола вверх на 2.5), сквозь которую игрок проходит телом.
|
||
const FINISH_ZONE_ID = 2;
|
||
const primitives = [
|
||
{
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: finish.x, y: 1.1, z: finish.z, sx: 1.6, sy: 0.2, sz: 1.6,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
},
|
||
{
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: finish.x, y: 1 + 1.25, z: finish.z, sx: 1.8, sy: 2.5, sz: 1.8,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
},
|
||
];
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g5_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ЛАБИРИНТ» — главный скрипт ===
|
||
|
||
game.ui.showText('Найди выход из лабиринта!', 3);
|
||
|
||
// Финиш сообщает о победе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Ты нашёл выход!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g5_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
spawnPoint: spawn,
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 6 — «Цветные плитки»
|
||
// Сетка серых плиток. Наступил на плитку — она стала яркой.
|
||
// Раскрась все плитки — победа.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game6ColorTiles() {
|
||
// Пол-рамка из травы, по которой стоят плитки.
|
||
const blocks = [];
|
||
for (let x = -6; x <= 5; x++) {
|
||
for (let z = -6; z <= 5; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
|
||
// Сетка 6×6 плиток-примитивов (плоские кубы), серые.
|
||
// Пол игры — блоки травы y=0 (их верх y=1). Плитки кладём НА пол
|
||
// (y=1.15), иначе игрок ходит выше плиток и не касается их —
|
||
// onTouch не срабатывает, плитки не красятся.
|
||
const GRID = 6;
|
||
const primitives = [];
|
||
let id = 0;
|
||
for (let r = 0; r < GRID; r++) {
|
||
for (let c = 0; c < GRID; c++) {
|
||
id++;
|
||
primitives.push({
|
||
id,
|
||
type: 'cube',
|
||
name: 'Плитка_' + id,
|
||
x: -4 + c * 2, y: 1.15, z: -4 + r * 2,
|
||
sx: 1.8, sy: 0.3, sz: 1.8,
|
||
color: '#9aa0aa', // серый — не раскрашена
|
||
material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
}
|
||
const TOTAL = id;
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт — счётчик раскрашенных плиток.
|
||
scripts.push({
|
||
id: 'g6_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ЦВЕТНЫЕ ПЛИТКИ» — главный скрипт ===
|
||
|
||
let painted = 0; // сколько плиток раскрашено
|
||
const TOTAL = ${TOTAL}; // всего плиток
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Наступи на все плитки!', 3);
|
||
|
||
// Плитки сообщают о покраске через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('paint', () => {
|
||
painted = painted + 1;
|
||
game.ui.score = painted;
|
||
game.sound.play('pickup');
|
||
if (painted >= TOTAL) {
|
||
game.ui.showText('Победа! Все плитки раскрашены!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую плитку — меняет цвет при касании.
|
||
for (let i = 1; i <= TOTAL; i++) {
|
||
scripts.push({
|
||
id: 'g6_tile_' + i,
|
||
name: 'Плитка_' + i,
|
||
target: { kind: 'primitive', id: i },
|
||
code:
|
||
`// === Скрипт цветной плитки ===
|
||
|
||
let painted = false; // плитка уже раскрашена?
|
||
|
||
game.self.onTouch(() => {
|
||
if (painted) return;
|
||
painted = true;
|
||
// меняем цвет плитки на ярко-зелёный
|
||
game.scene.setColor(game.self.ref, '#33dd55');
|
||
game.broadcast('paint'); // сообщаем главному скрипту о покраске
|
||
});`,
|
||
});
|
||
}
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 1, z: -5 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 7 — «Поймай падающее»
|
||
// С неба каждые 1.5с падает куб. Лови его (коснись) — +1 очко.
|
||
// Куб упал на землю — исчезает. Набери 15 очков.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game7CatchFalling() {
|
||
// Площадка 16×16 из травы с бортиком.
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -8; z <= 7; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
for (let i = -8; i <= 7; i++) {
|
||
blocks.push({ x: i, y: 1, z: -8, type: 'greystone' });
|
||
blocks.push({ x: i, y: 1, z: 7, type: 'greystone' });
|
||
blocks.push({ x: -8, y: 1, z: i, type: 'greystone' });
|
||
blocks.push({ x: 7, y: 1, z: i, type: 'greystone' });
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g7_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ПОЙМАЙ ПАДАЮЩЕЕ» — главный скрипт ===
|
||
|
||
let score = 0;
|
||
const GOAL = 15; // сколько кубов нужно поймать
|
||
let won = false;
|
||
|
||
// Пойманные кубы — чтобы не засчитать один куб дважды.
|
||
const caught = {};
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Лови падающие кубы! Нужно 15', 3);
|
||
|
||
// Каждые 1.5 секунды роняем с неба новый куб.
|
||
game.every(1.5, () => {
|
||
if (won) return;
|
||
const x = game.random(-6, 6); // случайная точка над площадкой
|
||
const z = game.random(-6, 6);
|
||
const cube = game.scene.spawn('primitive:cube', {
|
||
x: x, y: 14, z: z,
|
||
sx: 0.8, sy: 0.8, sz: 0.8,
|
||
color: '#ffcc33',
|
||
anchored: false, // anchored:false — куб ПАДАЕТ (физика)
|
||
});
|
||
// если за 6 секунд не поймали — куб исчезнет сам
|
||
game.scene.deleteAfter(cube, 6);
|
||
});
|
||
|
||
// Игрок коснулся падающего куба. onPlayerTouch шлёт e.target —
|
||
// ref ('primitive:N') только для заспавненных объектов (кубов),
|
||
// пол и стены сюда НЕ приходят.
|
||
game.onPlayerTouch((e) => {
|
||
if (won) return;
|
||
const ref = e && e.target;
|
||
if (!ref || caught[ref]) return; // нет ref или куб уже пойман
|
||
caught[ref] = true;
|
||
score = score + 1;
|
||
game.ui.score = score;
|
||
game.sound.play('coin');
|
||
game.scene.delete(ref); // пойманный куб исчезает
|
||
|
||
if (score >= GOAL) {
|
||
won = true;
|
||
game.ui.showText('Победа! Ты поймал 15 кубов!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives: [],
|
||
scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 8 — «Беги к финишу»
|
||
// Длинная дорожка. Со старта запускается секундомер. Добеги до финиша
|
||
// — секундомер остановится, покажет твоё время.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game8RunToFinish() {
|
||
// Дорожка-трасса: 6 в ширину, 60 в длину.
|
||
const blocks = [];
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = 0; z <= 60; z++) {
|
||
// чередуем оттенок дорожки полосами по 4 для наглядности скорости
|
||
const t = (Math.floor(z / 4) % 2 === 0) ? 'greystone' : 'rock';
|
||
blocks.push({ x, y: 0, z, type: t });
|
||
}
|
||
}
|
||
// Невысокие бортики вдоль трассы.
|
||
for (let z = 0; z <= 60; z++) {
|
||
blocks.push({ x: -3, y: 1, z, type: 'rock' });
|
||
blocks.push({ x: 2, y: 1, z, type: 'rock' });
|
||
}
|
||
|
||
// Финишная плитка-коврик в конце (на полу: пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = 1;
|
||
const FINISH_ZONE_ID = 2;
|
||
const primitives = [
|
||
{
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 58, sx: 6, sy: 0.2, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
},
|
||
{
|
||
// невидимая зона в полный рост — ловит игрока телом
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1 + 1.25, z: 58, sx: 6, sy: 2.5, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
},
|
||
];
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g8_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «БЕГИ К ФИНИШУ» — главный скрипт ===
|
||
|
||
let finished = false;
|
||
let time = 0; // прошло секунд
|
||
|
||
game.ui.timer = 0;
|
||
game.ui.showText('Беги к зелёному финишу — на время!', 3);
|
||
|
||
// Каждый кадр прибавляем к таймеру прошедшее время.
|
||
game.onTick((dt) => {
|
||
if (finished) return;
|
||
time = time + dt; // dt — секунды с прошлого кадра
|
||
game.ui.timer = time; // показываем секундомер
|
||
});
|
||
|
||
// Финиш сообщает о завершении забега через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('finish', () => {
|
||
if (finished) return;
|
||
finished = true;
|
||
// округляем время до десятых
|
||
const t = Math.round(time * 10) / 10;
|
||
game.ui.showText('Финиш! Твоё время: ' + t + ' сек', 6);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g8_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о финише
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 90,
|
||
spawnPoint: { x: -0.5, y: 1, z: 1 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 9 — «Светофор»
|
||
// Большой светофор меняет цвет. Зелёный — беги к финишу. Красный —
|
||
// замри! Двинулся на красный — возврат на старт.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game9TrafficLight() {
|
||
// Дорожка 8 в ширину, 50 в длину.
|
||
const blocks = [];
|
||
for (let x = -4; x <= 3; x++) {
|
||
for (let z = 0; z <= 50; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
// Светофор — большой куб в конце дорожки, за финишем, виден издалека.
|
||
const LIGHT_ID = 1;
|
||
primitives.push({
|
||
id: LIGHT_ID, type: 'sphere', name: 'Светофор',
|
||
x: -0.5, y: 6, z: 52, sx: 3, sy: 3, sz: 3,
|
||
color: '#e23b3b', material: 'neon', // старт — красный
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш — коврик на полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = 2;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 48, sx: 8, sy: 0.2, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая финиш-зона в полный рост — ловит игрока телом.
|
||
const FINISH_ZONE_ID = 3;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1 + 1.25, z: 48, sx: 8, sy: 2.5, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g9_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «СВЕТОФОР» — главный скрипт ===
|
||
|
||
let phase = 'green'; // 'green' (беги) или 'red' (замри)
|
||
let won = false;
|
||
let prev = null; // прошлая позиция игрока
|
||
|
||
game.ui.showText('Зелёный — беги! Красный — замри!', 3);
|
||
|
||
// findOne нельзя сразу в начале скрипта — снимок сцены приходит
|
||
// чуть позже. Светофор находим и запускаем цикл через game.after.
|
||
let light = null;
|
||
|
||
// Переключаем свет: зелёный 3с, красный 2.5с, по кругу.
|
||
function green() {
|
||
if (won || !light) return;
|
||
phase = 'green';
|
||
game.scene.setColor(light, '#22dd55');
|
||
game.ui.showText('ЗЕЛЁНЫЙ — беги!', 1.2);
|
||
game.after(3, red);
|
||
}
|
||
function red() {
|
||
if (won || !light) return;
|
||
phase = 'red';
|
||
game.scene.setColor(light, '#e23b3b');
|
||
game.ui.showText('КРАСНЫЙ — замри!', 1.2);
|
||
game.after(2.5, green);
|
||
}
|
||
game.after(0.2, () => {
|
||
light = game.scene.findOne('Светофор');
|
||
green(); // начинаем с зелёного
|
||
});
|
||
|
||
// Каждый кадр: если красный и игрок шевелится — на старт.
|
||
game.onTick((dt) => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (!p) return;
|
||
if (prev && phase === 'red') {
|
||
const moved = Math.hypot(p.x - prev.x, p.z - prev.z);
|
||
// двигался быстрее небольшого порога — пойман
|
||
if (moved / dt > 0.8) {
|
||
game.player.respawn();
|
||
game.ui.showText('Двинулся на красный! На старт.', 2);
|
||
game.sound.play('lose');
|
||
}
|
||
}
|
||
prev = { x: p.x, z: p.z };
|
||
});
|
||
|
||
// Финиш сообщает о победе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты дошёл до финиша!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g9_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 80,
|
||
spawnPoint: { x: -0.5, y: 1, z: 1 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 10 — «Прыжок-пружина»
|
||
// Башня из площадок на разной высоте. Между ними — батуты-пружины.
|
||
// Встал на батут — он подбрасывает вверх. Допрыгни до верха.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game10SpringJump() {
|
||
// Стартовая площадка.
|
||
const blocks = [];
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = -3; z <= 2; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
const next = () => ++id;
|
||
|
||
// Площадки-этажи на высотах 7, 14, 21 (промежуточные).
|
||
const floors = [
|
||
{ x: 0, y: 7, z: 6 },
|
||
{ x: 0, y: 14, z: 12 },
|
||
{ x: 0, y: 21, z: 18 },
|
||
];
|
||
const floorIds = floors.map((f) => {
|
||
const fid = next();
|
||
primitives.push({
|
||
id: fid, type: 'cube', name: 'Этаж_' + fid,
|
||
x: f.x, y: f.y, z: f.z, sx: 4, sy: 0.5, sz: 4,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return fid;
|
||
});
|
||
|
||
// Батуты — оранжевые низкие цилиндры (sy=0.4). Лежат НА поверхности,
|
||
// по которой ходит игрок: старт-пол верх y=1, этажи sy=0.5 → их верх
|
||
// y_этажа+0.25. Центр батута = поверхность + 0.2 (половина sy).
|
||
const trampSpots = [
|
||
{ x: 0, y: 1.2, z: 0 }, // на старте (пол верх y=1)
|
||
{ x: 0, y: 7.45, z: 6 }, // на 1 этаже (верх 7.25)
|
||
{ x: 0, y: 14.45, z: 12 }, // на 2 этаже (верх 14.25)
|
||
];
|
||
const trampIds = trampSpots.map((t) => {
|
||
const tid = next();
|
||
primitives.push({
|
||
id: tid, type: 'cylinder', name: 'Батут_' + tid,
|
||
x: t.x, y: t.y, z: t.z, sx: 2, sy: 0.4, sz: 2,
|
||
color: '#ff8c1a', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return tid;
|
||
});
|
||
|
||
// Финиш — на самом верху, на верхнем этаже (y=21, верх 21.25).
|
||
const FINISH_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 21.5, z: 18, sx: 3, sy: 0.5, sz: 3,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая финиш-зона в полный рост — ловит игрока телом.
|
||
const FINISH_ZONE_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 21.25 + 1.25, z: 18, sx: 3, sy: 2.5, sz: 3,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
scripts.push({
|
||
id: 'g10_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ПРЫЖОК-ПРУЖИНА» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Прыгай по батутам всё выше!', 3);
|
||
|
||
// упал вниз — на старт
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) {
|
||
game.player.respawn();
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// Финиш сообщает о победе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты допрыгал до верха!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждый батут — подбрасывает игрока.
|
||
for (const tid of trampIds) {
|
||
scripts.push({
|
||
id: 'g10_tramp_' + tid,
|
||
name: 'Батут_' + tid,
|
||
target: { kind: 'primitive', id: tid },
|
||
code:
|
||
`// === Скрипт батута ===
|
||
// Игрок встал на батут — мощный подброс вверх.
|
||
|
||
game.self.onTouch(() => {
|
||
game.player.boostJump(3.2); // 3.2 = в 3 раза выше обычного прыжка
|
||
game.sound.play('jump');
|
||
});`,
|
||
});
|
||
}
|
||
|
||
// Финиш — скрипт на невидимой зоне (игрок входит телом).
|
||
scripts.push({
|
||
id: 'g10_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
floorEnabled: false, // пола нет — батуты в воздухе
|
||
spawnPoint: { x: 0, y: 1, z: -2 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 11 — «Эхо-комната»
|
||
// Комната с 6 звуковыми плитками. Наступил — плитка вспыхивает
|
||
// и звучит. Пройди все 6 плиток, потом встань на финиш.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game11EchoRoom() {
|
||
// Пол комнаты 14×14.
|
||
const blocks = [];
|
||
for (let x = -7; x <= 6; x++) {
|
||
for (let z = -7; z <= 6; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'cotton-blue' });
|
||
}
|
||
}
|
||
// Стены комнаты — для «эха» (визуально замкнутое пространство).
|
||
for (let i = -7; i <= 6; i++) {
|
||
for (let h = 1; h <= 3; h++) {
|
||
blocks.push({ x: i, y: h, z: -7, type: 'greystone' });
|
||
blocks.push({ x: i, y: h, z: 6, type: 'greystone' });
|
||
blocks.push({ x: -7, y: h, z: i, type: 'greystone' });
|
||
blocks.push({ x: 6, y: h, z: i, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
// 6 звуковых плиток — разноцветные, каждая со своим звуком.
|
||
const tiles = [
|
||
{ x: -4, z: -3, color: '#e23b3b', sound: 'coin' },
|
||
{ x: 0, z: -3, color: '#f59e0b', sound: 'jump' },
|
||
{ x: 4, z: -3, color: '#facc15', sound: 'pickup' },
|
||
{ x: -4, z: 3, color: '#22c55e', sound: 'click' },
|
||
{ x: 0, z: 3, color: '#3b82f6', sound: 'hit' },
|
||
{ x: 4, z: 3, color: '#a855f7', sound: 'coin' },
|
||
];
|
||
// Плитки кладём НА пол (пол-блоки y=0, верх y=1 → плитка y=1.15),
|
||
// иначе игрок ходит выше плиток и не касается их.
|
||
const primitives = tiles.map((t, i) => ({
|
||
id: i + 1, type: 'cylinder', name: 'Плитка_' + (i + 1),
|
||
x: t.x, y: 1.15, z: t.z, sx: 2.2, sy: 0.3, sz: 2.2,
|
||
color: t.color, material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
}));
|
||
const TOTAL = tiles.length;
|
||
|
||
// Финиш — коврик на полу.
|
||
const FINISH_ID = TOTAL + 1;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 0, sx: 2.5, sy: 0.2, sz: 2.5,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая финиш-зона в полный рост — ловит игрока телом.
|
||
const FINISH_ZONE_ID = TOTAL + 2;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 1 + 1.25, z: 0, sx: 2.5, sy: 2.5, sz: 2.5,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
scripts.push({
|
||
id: 'g11_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ЭХО-КОМНАТА» — главный скрипт ===
|
||
|
||
let stepped = 0; // на сколько плиток наступили
|
||
const TOTAL = ${TOTAL};
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Наступи на все цветные плитки!', 3);
|
||
|
||
// Плитки и финиш сообщают сюда через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
|
||
// игрок впервые наступил на звуковую плитку
|
||
game.onMessage('step', () => {
|
||
stepped = stepped + 1;
|
||
game.ui.score = stepped;
|
||
if (stepped >= TOTAL) {
|
||
game.ui.showText('Все плитки звучали! Иди на финиш.', 3);
|
||
}
|
||
});
|
||
|
||
// игрок встал на финиш
|
||
game.onMessage('finish', () => {
|
||
if (stepped < TOTAL) {
|
||
game.ui.showText('Сначала пройди все ' + TOTAL + ' плиток!', 2);
|
||
return;
|
||
}
|
||
game.ui.showText('Победа! Эхо-комната пройдена!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую плитку — свой звук.
|
||
tiles.forEach((t, i) => {
|
||
scripts.push({
|
||
id: 'g11_tile_' + (i + 1),
|
||
name: 'Плитка_' + (i + 1),
|
||
target: { kind: 'primitive', id: i + 1 },
|
||
code:
|
||
`// === Скрипт звуковой плитки ===
|
||
|
||
let used = false; // на эту плитку уже наступали?
|
||
|
||
game.self.onTouch(() => {
|
||
// звук играет КАЖДЫЙ раз — это эхо-комната
|
||
game.sound.play('${t.sound}');
|
||
// вспышка частиц над плиткой
|
||
game.scene.spawnParticles('sparks', game.self.position,
|
||
{ duration: 0.6, color: '${t.color}' });
|
||
// засчитываем плитку только в первый раз
|
||
if (!used) {
|
||
used = true;
|
||
game.broadcast('step'); // сообщаем главному скрипту о новой плитке
|
||
}
|
||
});`,
|
||
});
|
||
});
|
||
|
||
// Финиш — скрипт на невидимой зоне (игрок входит телом).
|
||
scripts.push({
|
||
id: 'g11_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о финише
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 1, z: 5 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 12 — «Дверь по коду»
|
||
// Четыре кнопки-цифры. Нажимай их (клавишей E) в правильном порядке —
|
||
// дверь откроется. Ошибся — код сбрасывается.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game12CodeDoor() {
|
||
// Пол комнаты.
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -4; z <= 17; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
const next = () => ++id;
|
||
|
||
// Стена с проёмом на z=10.
|
||
primitives.push({
|
||
id: next(), type: 'cube', name: 'Стена_лево',
|
||
x: -4.5, y: 2.5, z: 10, sx: 7, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
primitives.push({
|
||
id: next(), type: 'cube', name: 'Стена_право',
|
||
x: 4.5, y: 2.5, z: 10, sx: 7, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Дверь.
|
||
const DOOR_ID = next();
|
||
primitives.push({
|
||
id: DOOR_ID, type: 'cube', name: 'Дверь',
|
||
x: 0, y: 2.5, z: 10, sx: 3, sy: 6, sz: 0.8,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// 4 кнопки-цифры — цилиндры в ряд перед дверью.
|
||
const buttonIds = [];
|
||
for (let n = 1; n <= 4; n++) {
|
||
const bid = next();
|
||
buttonIds.push(bid);
|
||
primitives.push({
|
||
id: bid, type: 'cylinder', name: 'Кнопка_' + n,
|
||
x: -4.5 + (n - 1) * 3, y: 1, z: 4, sx: 1.4, sy: 1.6, sz: 1.4,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
// Финиш за дверью — коврик на полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 14, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая финиш-зона в полный рост — ловит игрока телом.
|
||
const FINISH_ZONE_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 1 + 1.25, z: 14, sx: 4, sy: 2.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт — хранит код и проверяет последовательность.
|
||
scripts.push({
|
||
id: 'g12_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ДВЕРЬ ПО КОДУ» — главный скрипт ===
|
||
|
||
// СЕКРЕТНЫЙ КОД — порядок кнопок. Поменяй на свой!
|
||
const CODE = [3, 1, 4, 2];
|
||
|
||
let entered = []; // что игрок уже нажал
|
||
let opened = false;
|
||
|
||
game.ui.showText('Нажми кнопки в правильном порядке (E)', 4);
|
||
|
||
// Кнопки и финиш сообщают сюда через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
// Номер нажатой кнопки приходит в data: { num }.
|
||
|
||
// игрок нажал кнопку с номером d.num
|
||
game.onMessage('press', (d) => {
|
||
if (opened) return;
|
||
game.sound.play('click');
|
||
entered.push(d.num);
|
||
|
||
// проверяем — совпадает ли начало с кодом
|
||
const i = entered.length - 1;
|
||
if (entered[i] !== CODE[i]) {
|
||
// ошибка — сброс
|
||
entered = [];
|
||
game.ui.showText('Неверно! Код сброшен.', 1.5);
|
||
game.sound.play('lose');
|
||
return;
|
||
}
|
||
|
||
// весь код введён верно
|
||
if (entered.length === CODE.length) {
|
||
opened = true;
|
||
game.ui.showText('Код верный! Дверь открывается.', 3);
|
||
game.sound.play('win');
|
||
const door = game.scene.findOne('Дверь');
|
||
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
|
||
} else {
|
||
game.ui.showText('Верно! Дальше...', 1);
|
||
}
|
||
});
|
||
|
||
// игрок встал на финиш
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Ты разгадал код!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую кнопку.
|
||
buttonIds.forEach((bid, idx) => {
|
||
const num = idx + 1;
|
||
scripts.push({
|
||
id: 'g12_btn_' + num,
|
||
name: 'Кнопка_' + num,
|
||
target: { kind: 'primitive', id: bid },
|
||
code:
|
||
`// === Скрипт кнопки-цифры ${num} ===
|
||
|
||
game.self.onInteract(() => {
|
||
// сообщаем главному скрипту номер нажатой кнопки
|
||
game.broadcast('press', { num: ${num} });
|
||
}, {
|
||
text: 'Нажать кнопку ${num}',
|
||
distance: 3
|
||
});`,
|
||
});
|
||
});
|
||
|
||
// Финиш — скрипт на невидимой зоне (игрок входит телом).
|
||
scripts.push({
|
||
id: 'g12_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 13 — «Торговец»
|
||
// NPC-торговец стоит за прилавком. Подойди (E) — он поговорит
|
||
// и подарит ключ. С ключом открой дверь и дойди до финиша.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game13Trader() {
|
||
// Пол комнаты-лавки.
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -4; z <= 19; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'wood' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
const next = () => ++id;
|
||
|
||
// Прилавок — длинный куб.
|
||
const COUNTER_ID = next();
|
||
primitives.push({
|
||
id: COUNTER_ID, type: 'cube', name: 'Прилавок',
|
||
x: 0, y: 0.8, z: 3, sx: 5, sy: 1.6, sz: 1.5,
|
||
color: '#7a4a26', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Стена с дверью на z=11.
|
||
primitives.push({
|
||
id: next(), type: 'cube', name: 'Стена_лево',
|
||
x: -4.5, y: 2.5, z: 11, sx: 7, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
primitives.push({
|
||
id: next(), type: 'cube', name: 'Стена_право',
|
||
x: 4.5, y: 2.5, z: 11, sx: 7, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
const DOOR_ID = next();
|
||
primitives.push({
|
||
id: DOOR_ID, type: 'cube', name: 'Дверь',
|
||
x: 0, y: 2.5, z: 11, sx: 3, sy: 6, sz: 0.8,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш за дверью — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 15, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = next();
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 2.25, z: 15, sx: 4, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт — спавнит NPC-торговца и хранит состояние.
|
||
scripts.push({
|
||
id: 'g13_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ТОРГОВЕЦ» — главный скрипт ===
|
||
|
||
game.ui.showText('Поговори с торговцем — нажми E у прилавка', 4);
|
||
|
||
// создаём NPC-торговца за прилавком
|
||
const trader = game.scene.spawnNpc('character-a', {
|
||
x: 0, y: 1, z: 5, // y=1 — на верху блоков пола (иначе утоплен)
|
||
name: 'Торговец Боб',
|
||
hp: 100,
|
||
speed: 0, // торговец стоит на месте
|
||
});
|
||
|
||
let hasKey = false;
|
||
|
||
// Прилавок, дверь и финиш сообщают сюда через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
|
||
// игрок заговорил с торговцем
|
||
game.onMessage('talk', () => {
|
||
if (hasKey) {
|
||
trader.say('Иди к двери, ключ у тебя!', 3);
|
||
return;
|
||
}
|
||
hasKey = true;
|
||
trader.say('Привет! Вот тебе ключ от двери. Удачи!', 4);
|
||
game.inventory.add({ name: 'Ключ' });
|
||
game.ui.showText('Ты получил Ключ!', 2);
|
||
game.sound.play('pickup');
|
||
});
|
||
|
||
// игрок пытается открыть дверь
|
||
game.onMessage('openDoor', () => {
|
||
if (!game.inventory.has('Ключ')) {
|
||
game.ui.showText('Дверь заперта. Нужен ключ от торговца.', 2);
|
||
return;
|
||
}
|
||
game.ui.showText('Ключ подошёл! Дверь открыта.', 3);
|
||
game.sound.play('win');
|
||
const door = game.scene.findOne('Дверь');
|
||
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
|
||
});
|
||
|
||
// игрок встал на финиш
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Ты прошёл лавку торговца!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
|
||
// Скрипт прилавка — взаимодействие = разговор с торговцем.
|
||
scripts.push({
|
||
id: 'g13_counter',
|
||
name: 'Прилавок',
|
||
target: { kind: 'primitive', id: COUNTER_ID },
|
||
code:
|
||
`// === Скрипт прилавка ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('talk'); // сообщаем главному скрипту: говорим с торговцем
|
||
}, {
|
||
text: 'Поговорить с торговцем',
|
||
distance: 4
|
||
});`,
|
||
});
|
||
|
||
// Скрипт двери — взаимодействие = попытка открыть.
|
||
scripts.push({
|
||
id: 'g13_door',
|
||
name: 'Дверь',
|
||
target: { kind: 'primitive', id: DOOR_ID },
|
||
code:
|
||
`// === Скрипт двери ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('openDoor'); // сообщаем главному скрипту: открыть дверь
|
||
}, {
|
||
text: 'Открыть дверь',
|
||
distance: 4
|
||
});`,
|
||
});
|
||
|
||
// Финиш — скрипт висит на невидимой зоне над ковриком.
|
||
scripts.push({
|
||
id: 'g13_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 14 — «Собери по тегам»
|
||
// На площадке звёзды (с тегом «звезда») и кубы-обманки (без тега).
|
||
// Собери все звёзды. Тег помогает находить нужные объекты.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game14CollectByTag() {
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -8; z <= 7; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
|
||
// 7 звёзд — жёлтые конусы.
|
||
const starSpots = [
|
||
{ x: -5, z: -5 }, { x: 0, z: -5 }, { x: 5, z: -5 },
|
||
{ x: -5, z: 1 }, { x: 5, z: 1 },
|
||
{ x: -3, z: 5 }, { x: 4, z: 5 },
|
||
];
|
||
const starIds = starSpots.map((s) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cone', name: 'Звезда_' + id,
|
||
x: s.x, y: 1, z: s.z, sx: 1, sy: 1.4, sz: 1,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
|
||
// 5 кубов-обманок — синие, без тега. Их собирать не надо.
|
||
const decoySpots = [
|
||
{ x: -2, z: -2 }, { x: 3, z: -2 }, { x: 0, z: 3 },
|
||
{ x: -6, z: 4 }, { x: 6, z: -6 },
|
||
];
|
||
for (const d of decoySpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Куб_' + id,
|
||
x: d.x, y: 0.8, z: d.z, sx: 1.2, sy: 1.2, sz: 1.2,
|
||
color: '#3b82f6', material: 'matte',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
const scripts = [];
|
||
|
||
// Главный скрипт: помечает звёзды тегом и считает остаток.
|
||
scripts.push({
|
||
id: 'g14_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «СОБЕРИ ПО ТЕГАМ» — главный скрипт ===
|
||
|
||
game.ui.showText('Собери все ЖЁЛТЫЕ звёзды!', 3);
|
||
game.ui.score = 0;
|
||
|
||
// помечаем все звёзды тегом 'звезда'.
|
||
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже,
|
||
// поэтому простановку тегов делаем через game.after.
|
||
game.after(0.2, () => {
|
||
for (let i = 1; i <= ${starIds.length}; i++) {
|
||
const star = game.scene.findOne('Звезда_' + i);
|
||
if (star) game.scene.tag(star, 'звезда');
|
||
}
|
||
});
|
||
|
||
// Звёзды сообщают о сборе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('collected', () => {
|
||
// getTagged вернёт все ещё не собранные звёзды
|
||
const left = game.scene.getTagged('звезда').length;
|
||
game.ui.score = ${starIds.length} - left;
|
||
game.sound.play('coin');
|
||
if (left === 0) {
|
||
game.ui.showText('Победа! Все звёзды собраны!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую звезду.
|
||
for (const sid of starIds) {
|
||
scripts.push({
|
||
id: 'g14_star_' + sid,
|
||
name: 'Звезда_' + sid,
|
||
target: { kind: 'primitive', id: sid },
|
||
code:
|
||
`// === Скрипт звезды ===
|
||
game.self.onTouch(() => {
|
||
// снимаем тег и удаляем звезду
|
||
game.scene.untag(game.self.ref, 'звезда');
|
||
game.self.delete();
|
||
game.broadcast('collected'); // сообщаем главному скрипту о сборе
|
||
});`,
|
||
});
|
||
}
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 15 — «Тир»
|
||
// Мишени-сферы на постаментах. Кликай по мишеням — за каждое
|
||
// попадание очко. Выбей все 8 мишеней.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game15ShootingRange() {
|
||
// Площадка тира.
|
||
const blocks = [];
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -4; z <= 15; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
// 8 мишеней на постаментах.
|
||
const targetSpots = [
|
||
{ x: -8, z: 6 }, { x: -5, z: 9 }, { x: -2, z: 7 }, { x: 1, z: 10 },
|
||
{ x: 4, z: 7 }, { x: 7, z: 9 }, { x: -6, z: 12 }, { x: 5, z: 12 },
|
||
];
|
||
const primitives = [];
|
||
let id = 0;
|
||
const targetIds = [];
|
||
for (const t of targetSpots) {
|
||
// постамент-кубик
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Постамент_' + id,
|
||
x: t.x, y: 1, z: t.z, sx: 1, sy: 2, sz: 1,
|
||
color: '#6b7280', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// мишень-сфера на постаменте
|
||
id++;
|
||
targetIds.push(id);
|
||
primitives.push({
|
||
id, type: 'sphere', name: 'Мишень_' + id,
|
||
x: t.x, y: 3, z: t.z, sx: 1.1, sy: 1.1, sz: 1.1,
|
||
color: '#ff3030', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
const scripts = [];
|
||
|
||
scripts.push({
|
||
id: 'g15_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ТИР» — главный скрипт ===
|
||
|
||
let score = 0;
|
||
const TOTAL = ${targetIds.length};
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Кликай по красным мишеням!', 3);
|
||
|
||
// Мишени сообщают о попадании через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('hit', () => {
|
||
score = score + 1;
|
||
game.ui.score = score;
|
||
game.sound.play('hit');
|
||
if (score >= TOTAL) {
|
||
game.ui.showText('Победа! Все мишени выбиты!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
|
||
// Скрипт на каждую мишень.
|
||
for (const tid of targetIds) {
|
||
scripts.push({
|
||
id: 'g15_target_' + tid,
|
||
name: 'Мишень_' + tid,
|
||
target: { kind: 'primitive', id: tid },
|
||
code:
|
||
`// === Скрипт мишени ===
|
||
// Клик по 3D-объекту = выстрел в него.
|
||
|
||
game.self.onClick(() => {
|
||
// взрыв искр на месте мишени
|
||
game.scene.spawnParticles('explosion', game.self.position,
|
||
{ count: 1, color: '#ff6633' });
|
||
game.self.delete(); // мишень сбита
|
||
game.broadcast('hit'); // сообщаем главному скрипту о попадании
|
||
});`,
|
||
});
|
||
}
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: -2 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 16 — «Лава-пол»
|
||
// Поле залито лавой — касание лавы жжёт (урон). Прыгай по безопасным
|
||
// каменным островкам до финиша.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game16LavaFloor() {
|
||
// Пол из блоков лавы — большое озеро.
|
||
const blocks = [];
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -2; z <= 30; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'lava' });
|
||
}
|
||
}
|
||
// Стартовая безопасная площадка из камня.
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = -2; z <= 1; z++) {
|
||
blocks.push({ x, y: 1, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
|
||
// Островки-платформы — каменные кубы над лавой, дорожкой.
|
||
const isleSpots = [
|
||
{ x: 0, z: 5 }, { x: 3, z: 9 }, { x: -2, z: 13 },
|
||
{ x: 2, z: 17 }, { x: -3, z: 21 }, { x: 1, z: 25 },
|
||
];
|
||
for (const s of isleSpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Островок_' + id,
|
||
x: s.x, y: 1.2, z: s.z, sx: 2.4, sy: 0.6, sz: 2.4,
|
||
color: '#7a8088', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
// Невидимая зона-лава над озером — ловит касание игрока.
|
||
const LAVA_ID = ++id;
|
||
primitives.push({
|
||
id: LAVA_ID, type: 'cube', name: 'ЛаваЗона',
|
||
x: -0.5, y: 1, z: 14, sx: 20, sy: 0.6, sz: 34,
|
||
color: '#ff5522', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
// Финиш — каменная площадка в конце (твёрдая, на неё встают).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.2, z: 29, sx: 5, sy: 0.6, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх площадки — ловит игрока телом
|
||
// (он стоит сверху, тонкую плитку телом не пересекает).
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1.5 + 1.5, z: 29, sx: 5, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g16_main',
|
||
name: 'Главный скрипт',
|
||
target: null,
|
||
code:
|
||
`// === ИГРА «ЛАВА-ПОЛ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Прыгай по островкам! Лава жжёт!', 3);
|
||
|
||
// если HP кончилось — игрок воскреснет на старте сам.
|
||
// следим за падением в лаву ниже уровня.
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -2) {
|
||
game.player.respawn();
|
||
}
|
||
});
|
||
|
||
// Финиш сообщает о победе через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты перебрался через лаву!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g16_lava',
|
||
name: 'ЛаваЗона',
|
||
target: { kind: 'primitive', id: LAVA_ID },
|
||
code:
|
||
`// === Скрипт лавы ===
|
||
// Игрок коснулся лавы — урон. У damage есть защита (i-frames),
|
||
// так что урон не каждый кадр, а раз в ~0.5 секунды.
|
||
|
||
game.self.onTouch(() => {
|
||
game.player.damage(20);
|
||
game.sound.play('hit');
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g16_finish',
|
||
name: 'Финиш',
|
||
target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks,
|
||
primitives,
|
||
scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: 0, y: 2, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 17 — «Ключ и сундук»
|
||
// Найди ключ на уровне, подбери его, открой сундук клавишей E.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game17KeyChest() {
|
||
const blocks = [];
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -10; z <= 9; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Несколько кустов-кубов, среди которых спрятан ключ.
|
||
const primitives = [];
|
||
let id = 0;
|
||
const bushSpots = [
|
||
{ x: -7, z: -6 }, { x: 5, z: -7 }, { x: -4, z: 5 },
|
||
{ x: 7, z: 6 }, { x: 0, z: -8 }, { x: -8, z: 3 },
|
||
];
|
||
for (const b of bushSpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Куст_' + id,
|
||
x: b.x, y: 0.8, z: b.z, sx: 1.6, sy: 1.6, sz: 1.6,
|
||
color: '#2f7d32', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// Ключ — жёлтый тор, лежит в углу.
|
||
const KEY_ID = ++id;
|
||
primitives.push({
|
||
id: KEY_ID, type: 'torus', name: 'Ключ',
|
||
x: 7, y: 1, z: -7, sx: 0.9, sy: 0.4, sz: 0.9,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Сундук — коричневый куб в центре.
|
||
const CHEST_ID = ++id;
|
||
primitives.push({
|
||
id: CHEST_ID, type: 'cube', name: 'Сундук',
|
||
x: 0, y: 0.8, z: 4, sx: 2, sy: 1.6, sz: 1.4,
|
||
color: '#7a4a26', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g17_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «КЛЮЧ И СУНДУК» — главный скрипт ===
|
||
|
||
game.ui.showText('Найди ключ и открой сундук!', 3);
|
||
|
||
// Ключ и сундук сообщают сюда через broadcast/onMessage —
|
||
// скрипты в разных песочницах, связь только сообщениями.
|
||
|
||
// игрок подобрал ключ
|
||
game.onMessage('takeKey', () => {
|
||
game.inventory.add({ name: 'Ключ' });
|
||
game.ui.showText('Ты нашёл Ключ!', 2);
|
||
game.sound.play('pickup');
|
||
});
|
||
|
||
// игрок пытается открыть сундук
|
||
game.onMessage('openChest', () => {
|
||
if (!game.inventory.has('Ключ')) {
|
||
game.ui.showText('Сундук заперт. Сначала найди ключ.', 2);
|
||
return;
|
||
}
|
||
game.ui.showText('Победа! Сундук открыт — там сокровище!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g17_key', name: 'Ключ', target: { kind: 'primitive', id: KEY_ID },
|
||
code:
|
||
`// === Скрипт ключа ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('takeKey'); // сообщаем главному скрипту: ключ найден
|
||
game.self.delete(); // ключ подобран
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g17_chest', name: 'Сундук', target: { kind: 'primitive', id: CHEST_ID },
|
||
code:
|
||
`// === Скрипт сундука ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('openChest'); // сообщаем главному скрипту: открыть сундук
|
||
}, { text: 'Открыть сундук', distance: 4 });`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: -3 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 18 — «Качели»
|
||
// Качели на петле-констрейнте раскачиваются. Прокатись на качелях.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game18Swing() {
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -8; z <= 7; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Стартовая возвышенность, с которой запрыгивают на качели.
|
||
for (let x = -2; x <= 1; x++) {
|
||
for (let z = -6; z <= -4; z++) {
|
||
blocks.push({ x, y: 1, z, type: 'greystone' });
|
||
blocks.push({ x, y: 2, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Сиденье качелей — широкая плоская платформа.
|
||
const SWING_ID = ++id;
|
||
primitives.push({
|
||
id: SWING_ID, type: 'cube', name: 'Качели',
|
||
x: 0, y: 3, z: 0, sx: 4, sy: 0.5, sz: 3,
|
||
color: '#9b6b3e', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш — площадка по другую сторону качелей (твёрдая, на неё встают).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1, z: 7, sx: 4, sy: 0.5, sz: 3,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх площадки — ловит игрока телом.
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 1.25 + 1.5, z: 7, sx: 4, sy: 3, sz: 3,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g18_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «КАЧЕЛИ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Запрыгни на качели и прокатись!', 3);
|
||
|
||
// делаем качели на петле и раскачиваем их.
|
||
// ВАЖНО: game.scene.findOne нельзя звать сразу в начале скрипта —
|
||
// снимок сцены приходит чуть позже. Ждём 0.2с через game.after.
|
||
let hinge = null;
|
||
let dir = -35;
|
||
game.after(0.2, () => {
|
||
const swing = game.scene.findOne('Качели');
|
||
hinge = game.constraints.hinge(swing, {
|
||
pivotX: 0, pivotZ: 0, // ось — посередине качелей
|
||
angle: 35
|
||
});
|
||
// раскачиваем туда-сюда каждые 1.4 секунды
|
||
game.every(1.4, () => {
|
||
if (won || !hinge) return;
|
||
hinge.setAngle(dir);
|
||
dir = -dir;
|
||
});
|
||
});
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) game.player.respawn();
|
||
});
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты перебрался на качелях!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g18_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 4, z: -5 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 19 — «Лифт»
|
||
// Платформа-лифт ездит между нижним и верхним этажом.
|
||
// Заедь на лифте наверх, на финиш.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game19Elevator() {
|
||
const blocks = [];
|
||
// Нижний этаж.
|
||
for (let x = -7; x <= 6; x++) {
|
||
for (let z = -7; z <= 6; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
// Верхний этаж — площадка сбоку на высоте 12.
|
||
for (let x = -7; x <= 6; x++) {
|
||
for (let z = 8; z <= 14; z++) {
|
||
blocks.push({ x, y: 12, z, type: 'rock' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Лифт — платформа.
|
||
const LIFT_ID = ++id;
|
||
primitives.push({
|
||
id: LIFT_ID, type: 'cube', name: 'Лифт',
|
||
x: 0, y: 1, z: 4, sx: 3.5, sy: 0.5, sz: 3.5,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш — на верхнем этаже. Блоки этажа rock на y=12 (верх y=13),
|
||
// коврик-финиш кладём НА этаж (y=13.1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 13.1, z: 11, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 14.25, z: 11, sx: 4, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g19_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ЛИФТ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Встань на синий лифт — он повезёт наверх', 3);
|
||
|
||
// лифт вечно ездит: вниз (y=1) ↔ верх (y=12.3).
|
||
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже.
|
||
game.after(0.2, () => {
|
||
const lift = game.scene.findOne('Лифт');
|
||
game.tween(lift, { y: 12.3 }, {
|
||
duration: 3.5,
|
||
yoyo: true, // обратно вниз
|
||
repeat: 999, // почти бесконечно
|
||
easing: 'ease'
|
||
});
|
||
});
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) game.player.respawn();
|
||
});
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты поднялся на лифте!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g19_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: -3 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 20 — «Имена над врагами»
|
||
// Несколько NPC-врагов, над каждым висит метка с именем и HP.
|
||
// Кликай по врагам — метка показывает урон. Победи всех.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game20EnemyNames() {
|
||
const blocks = [];
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -10; z <= 9; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g20_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт ===
|
||
|
||
game.ui.showText('Победи всех врагов! Кликай по ним', 3);
|
||
|
||
// данные врагов: имя и позиция
|
||
const enemyData = [
|
||
{ name: 'Гоблин', x: -5, z: 3, hp: 60 },
|
||
{ name: 'Скелет', x: 4, z: 5, hp: 80 },
|
||
{ name: 'Орк', x: 0, z: 8, hp: 100 },
|
||
];
|
||
|
||
let alive = enemyData.length;
|
||
|
||
enemyData.forEach((d) => {
|
||
// создаём NPC-врага
|
||
const npc = game.scene.spawnNpc('character-b', {
|
||
x: d.x, y: 1, z: d.z, // y=1 — на верху блоков пола
|
||
name: d.name, hp: d.hp, speed: 0,
|
||
});
|
||
|
||
// вешаем над врагом метку-billboard с именем и HP
|
||
let hp = d.hp;
|
||
function updateLabel() {
|
||
game.scene.setLabel(npc.ref, d.name + ' HP: ' + hp, {
|
||
color: '#ff5555', height: 3
|
||
});
|
||
}
|
||
updateLabel();
|
||
|
||
// когда враг гибнет — убираем метку
|
||
npc.onDeath(() => {
|
||
game.scene.clearLabel(npc.ref);
|
||
alive = alive - 1;
|
||
game.sound.play('hit');
|
||
if (alive <= 0) {
|
||
game.ui.showText('Победа! Все враги повержены!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});
|
||
|
||
// урон врагу по клику игрока рядом — упрощённо бьём раз в клик
|
||
// (в этой игре кликаем по сцене возле врага)
|
||
game.onClick(() => {
|
||
if (hp <= 0) return;
|
||
// бьём только если игрок близко к этому врагу
|
||
const p = game.player.position;
|
||
if (!p || !npc.position) return;
|
||
const dist = Math.hypot(p.x - npc.position.x, p.z - npc.position.z);
|
||
if (dist < 4) {
|
||
hp = hp - 30;
|
||
if (hp < 0) hp = 0;
|
||
npc.damage(30);
|
||
updateLabel();
|
||
game.scene.spawnParticles('sparks', npc.position, { duration: 0.4 });
|
||
}
|
||
});
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives: [], scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: -4 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 21 — «Преследователь»
|
||
// NPC гонится за игроком. Добеги до зелёного укрытия, не дав себя
|
||
// поймать. Поймал — на старт.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game21Chaser() {
|
||
const blocks = [];
|
||
for (let x = -14; x <= 13; x++) {
|
||
for (let z = -6; z <= 28; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// несколько кубов-укрытий по полю, за которыми можно петлять
|
||
const primitives = [];
|
||
let id = 0;
|
||
const blockSpots = [
|
||
{ x: -6, z: 6 }, { x: 5, z: 10 }, { x: -3, z: 16 }, { x: 7, z: 20 },
|
||
];
|
||
for (const b of blockSpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Препятствие_' + id,
|
||
x: b.x, y: 1.5, z: b.z, sx: 3, sy: 3, sz: 3,
|
||
color: '#6b7280', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// финиш-укрытие — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 26, sx: 5, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 2.25, z: 26, sx: 5, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g21_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПРЕСЛЕДОВАТЕЛЬ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Убегай от врага! Добеги до укрытия!', 3);
|
||
|
||
// создаём NPC-преследователя
|
||
const enemy = game.scene.spawnNpc('character-b', {
|
||
x: 0, y: 1, z: -3, // y=1 — на верху блоков пола (иначе утоплен)
|
||
name: 'Охотник', hp: 100, speed: 4,
|
||
});
|
||
// враг постоянно гонится за игроком
|
||
enemy.follow('player');
|
||
|
||
// каждый кадр проверяем — не догнал ли враг.
|
||
// enemy.position наполняется не сразу после spawnNpc (NPC появляется
|
||
// через кадр) — пока позиции нет, пропускаем кадр.
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
const e = enemy.position;
|
||
if (!p || !e) return;
|
||
const dist = Math.hypot(p.x - e.x, p.z - e.z);
|
||
if (dist < 1.6) {
|
||
// враг поймал — на старт
|
||
game.player.respawn();
|
||
game.ui.showText('Пойман! Беги снова!', 2);
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
enemy.stop();
|
||
game.ui.showText('Победа! Ты убежал от врага!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g21_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 22 — «Зона опасности»
|
||
// На пути — невидимая зона. Внутри неё игрок теряет HP. Пробеги
|
||
// через зону к финишу, пока HP не кончилось.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game22DangerZone() {
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -4; z <= 36; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Зона опасности — большой красный полупрозрачный куб посередине.
|
||
const ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: ZONE_ID, type: 'cube', name: 'ЗонаОпасности',
|
||
x: -0.5, y: 2, z: 16, sx: 16, sy: 5, sz: 14,
|
||
color: '#ff3333', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 33, sx: 5, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 2.25, z: 33, sx: 5, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Аптечка перед зоной — лечит.
|
||
const HEAL_ID = ++id;
|
||
primitives.push({
|
||
id: HEAL_ID, type: 'sphere', name: 'Аптечка',
|
||
x: 4, y: 1, z: 6, sx: 0.9, sy: 0.9, sz: 0.9,
|
||
color: '#33dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g22_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ЗОНА ОПАСНОСТИ» — главный скрипт ===
|
||
|
||
let inZone = false; // игрок сейчас в красной зоне?
|
||
let won = false;
|
||
|
||
game.ui.showText('Пробеги через красную зону к финишу!', 3);
|
||
|
||
// пока игрок в зоне — каждые 0.6с снимаем HP
|
||
game.every(0.6, () => {
|
||
if (won) return;
|
||
if (inZone) {
|
||
game.player.damage(12);
|
||
game.sound.play('hit');
|
||
}
|
||
});
|
||
|
||
// зона и финиш шлют сюда сообщения через broadcast — скрипты
|
||
// в разных песочницах, общие переменные между ними не видны.
|
||
game.onMessage('zone-enter', () => {
|
||
inZone = true;
|
||
game.ui.showText('Опасно! Беги быстрее!', 1.5);
|
||
});
|
||
game.onMessage('zone-leave', () => { inZone = false; });
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты прошёл зону опасности!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g22_zone', name: 'ЗонаОпасности', target: { kind: 'primitive', id: ZONE_ID },
|
||
code:
|
||
`// === Скрипт зоны опасности ===
|
||
// onTouch — игрок вошёл, onUntouch — вышел.
|
||
|
||
game.self.onTouch(() => {
|
||
game.broadcast('zone-enter');
|
||
});
|
||
game.self.onUntouch(() => {
|
||
game.broadcast('zone-leave');
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g22_heal', name: 'Аптечка', target: { kind: 'primitive', id: HEAL_ID },
|
||
code:
|
||
`// === Скрипт аптечки ===
|
||
game.self.onTouch(() => {
|
||
game.player.heal(60);
|
||
game.ui.showText('+60 HP', 1.5);
|
||
game.sound.play('pickup');
|
||
game.self.delete();
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g22_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: -0.5, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 23 — «Переключатели»
|
||
// Три рычага. Дёрни их (E) в правильном порядке — откроется дверь.
|
||
// Ошибся — все рычаги сбрасываются.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game23Switches() {
|
||
const blocks = [];
|
||
for (let x = -9; x <= 8; x++) {
|
||
for (let z = -4; z <= 17; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Стена с дверью.
|
||
primitives.push({
|
||
id: ++id, type: 'cube', name: 'Стена_лево',
|
||
x: -5, y: 2.5, z: 10, sx: 8, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
primitives.push({
|
||
id: ++id, type: 'cube', name: 'Стена_право',
|
||
x: 5, y: 2.5, z: 10, sx: 8, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
const DOOR_ID = ++id;
|
||
primitives.push({
|
||
id: DOOR_ID, type: 'cube', name: 'Дверь',
|
||
x: 0, y: 2.5, z: 10, sx: 3, sy: 6, sz: 0.8,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// 3 рычага-цилиндра.
|
||
const leverIds = [];
|
||
for (let n = 1; n <= 3; n++) {
|
||
const lid = ++id;
|
||
leverIds.push(lid);
|
||
primitives.push({
|
||
id: lid, type: 'cylinder', name: 'Рычаг_' + n,
|
||
x: -5 + (n - 1) * 5, y: 1.2, z: 4, sx: 1, sy: 2.4, sz: 1,
|
||
color: '#e23b3b', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// Финиш за дверью — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 14, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 2.25, z: 14, sx: 4, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g23_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПЕРЕКЛЮЧАТЕЛИ» — главный скрипт ===
|
||
|
||
// правильный порядок рычагов
|
||
const ORDER = [2, 3, 1];
|
||
let pressed = [];
|
||
let opened = false;
|
||
|
||
game.ui.showText('Дёрни рычаги в нужном порядке (E)', 4);
|
||
|
||
// рычаги дёргаются через broadcast('lever', { num }) — скрипты
|
||
// в разных песочницах, состояние pressed[] живёт только здесь.
|
||
game.onMessage('lever', (d) => {
|
||
const n = d.num;
|
||
if (opened) return;
|
||
game.sound.play('click');
|
||
pressed.push(n);
|
||
const i = pressed.length - 1;
|
||
if (pressed[i] !== ORDER[i]) {
|
||
pressed = [];
|
||
game.ui.showText('Неверно! Рычаги сброшены.', 1.5);
|
||
game.sound.play('lose');
|
||
return;
|
||
}
|
||
if (pressed.length === ORDER.length) {
|
||
opened = true;
|
||
game.ui.showText('Верно! Дверь открыта.', 3);
|
||
game.sound.play('win');
|
||
const door = game.scene.findOne('Дверь');
|
||
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
|
||
} else {
|
||
game.ui.showText('Так держать!', 1);
|
||
}
|
||
});
|
||
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Ты разгадал порядок!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
leverIds.forEach((lid, idx) => {
|
||
const n = idx + 1;
|
||
scripts.push({
|
||
id: 'g23_lever_' + n, name: 'Рычаг_' + n,
|
||
target: { kind: 'primitive', id: lid },
|
||
code:
|
||
`// === Скрипт рычага ${n} ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('lever', { num: ${n} });
|
||
}, { text: 'Дёрнуть рычаг ${n}', distance: 3 });`,
|
||
});
|
||
});
|
||
scripts.push({
|
||
id: 'g23_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 24 — «Падающий мост»
|
||
// Мост из плиток над пропастью. Каждая плитка исчезает через секунду
|
||
// после касания. Перебеги мост, пока не рухнул.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game24FallingBridge() {
|
||
// Старт и финиш — твёрдые площадки, между ними пропасть.
|
||
const blocks = [];
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = -3; z <= 0; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
for (let z = 40; z <= 44; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'rock' });
|
||
}
|
||
}
|
||
|
||
// Мост — 18 плиток-примитивов в ряд над пропастью.
|
||
// По доскам ХОДЯТ: их верх кладём на уровень пола (y=1),
|
||
// доска sy=0.35 → центр y = 1 - 0.35/2 ≈ 0.83. Иначе доски ниже
|
||
// ног игрока и он сразу падает в пропасть.
|
||
const TILES = 18;
|
||
const primitives = [];
|
||
for (let i = 0; i < TILES; i++) {
|
||
primitives.push({
|
||
id: i + 1, type: 'cube', name: 'Доска_' + (i + 1),
|
||
x: -0.5, y: 0.83, z: 3 + i * 2,
|
||
sx: 2.4, sy: 0.35, sz: 2,
|
||
color: '#9b6b3e', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// Финиш — видимый коврик НА полу (финиш-блоки rock y=0, верх y=1).
|
||
const FINISH_ID = TILES + 1;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 42, sx: 5, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = TILES + 2;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 2.25, z: 42, sx: 5, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g24_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПАДАЮЩИЙ МОСТ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Беги по мосту — доски рушатся!', 3);
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) {
|
||
game.player.respawn();
|
||
game.ui.showText('Упал в пропасть! Снова.', 1.5);
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Ты перебежал мост!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
for (let i = 1; i <= TILES; i++) {
|
||
scripts.push({
|
||
id: 'g24_plank_' + i, name: 'Доска_' + i,
|
||
target: { kind: 'primitive', id: i },
|
||
code:
|
||
`// === Скрипт доски моста ===
|
||
let cracking = false;
|
||
game.self.onTouch(() => {
|
||
if (cracking) return;
|
||
cracking = true;
|
||
game.sound.play('click');
|
||
game.after(1, () => { game.self.delete(); });
|
||
});`,
|
||
});
|
||
}
|
||
scripts.push({
|
||
id: 'g24_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
floorEnabled: false, // мост над пропастью — пола нет
|
||
spawnPoint: { x: -0.5, y: 1, z: -2 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 25 — «Камера-облёт»
|
||
// При старте камера красиво облетает уровень, показывая его. Потом
|
||
// управление возвращается игроку, и он идёт к финишу.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game25FlybyCamera() {
|
||
const blocks = [];
|
||
for (let x = -12; x <= 11; x++) {
|
||
for (let z = -4; z <= 30; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// несколько украшающих столбов вдоль пути
|
||
for (let i = 0; i < 5; i++) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cylinder', name: 'Столб_' + id,
|
||
x: (i % 2 === 0) ? -6 : 6, y: 2.5, z: 4 + i * 5,
|
||
sx: 1.2, sy: 5, sz: 1.2,
|
||
color: '#a855f7', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// Финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 27, sx: 5, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока телом.
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 2.25, z: 27, sx: 5, sy: 3, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g25_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «КАМЕРА-ОБЛЁТ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
// при старте — облёт уровня камерой по точкам
|
||
game.camera.cutscene([
|
||
{ x: 0, y: 18, z: -10 },
|
||
{ x: 12, y: 12, z: 8 },
|
||
{ x: -12, y: 12, z: 18 },
|
||
{ x: 0, y: 10, z: 28 },
|
||
], { segDuration: 1.8 });
|
||
|
||
// когда облёт закончился — отдаём камеру игроку
|
||
game.onCutsceneDone(() => {
|
||
game.ui.showText('Вперёд, к зелёному финишу!', 3);
|
||
});
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Уровень пройден!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g25_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 65,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 26 — «Магнит монет»
|
||
// Монеты разбросаны по полю. Когда игрок подходит близко — монета
|
||
// сама летит к нему (твин). Собери все.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game26CoinMagnet() {
|
||
const blocks = [];
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -10; z <= 9; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
const coinSpots = [
|
||
{ x: -7, z: -7 }, { x: 0, z: -7 }, { x: 7, z: -7 },
|
||
{ x: -7, z: 0 }, { x: 7, z: 0 },
|
||
{ x: -7, z: 7 }, { x: 0, z: 7 }, { x: 7, z: 7 },
|
||
];
|
||
const primitives = coinSpots.map((c, i) => ({
|
||
id: i + 1, type: 'sphere', name: 'Монетка_' + (i + 1),
|
||
x: c.x, y: 1, z: c.z, sx: 0.6, sy: 0.6, sz: 0.6,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
}));
|
||
const TOTAL = coinSpots.length;
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g26_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «МАГНИТ МОНЕТ» — главный скрипт ===
|
||
|
||
let score = 0;
|
||
const TOTAL = ${TOTAL};
|
||
game.ui.score = 0;
|
||
game.ui.showText('Подходи к монеткам — они притянутся!', 3);
|
||
|
||
// монетки сообщают о сборе через broadcast('coin') — скрипты
|
||
// в разных песочницах, счётчик score живёт только здесь.
|
||
game.onMessage('coin', () => {
|
||
score = score + 1;
|
||
game.ui.score = score;
|
||
game.sound.play('coin');
|
||
if (score >= TOTAL) {
|
||
game.ui.showText('Победа! Все монетки собраны!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
for (let i = 1; i <= TOTAL; i++) {
|
||
scripts.push({
|
||
id: 'g26_coin_' + i, name: 'Монетка_' + i,
|
||
target: { kind: 'primitive', id: i },
|
||
code:
|
||
`// === Скрипт магнитной монетки ===
|
||
|
||
let flying = false; // монетка уже летит к игроку?
|
||
let taken = false;
|
||
|
||
game.onTick(() => {
|
||
if (taken) return;
|
||
const c = game.self.position;
|
||
const p = game.player.position;
|
||
// позиции могут быть не готовы первые кадры — ждём
|
||
if (!c || !p) return;
|
||
const dist = Math.hypot(p.x - c.x, p.z - c.z);
|
||
|
||
// игрок коснулся монетки — собрана
|
||
if (dist < 1.2) {
|
||
taken = true;
|
||
game.self.delete();
|
||
game.broadcast('coin'); // сообщить главному скрипту
|
||
return;
|
||
}
|
||
// подошёл близко — монетка летит к игроку
|
||
if (!flying && dist < 6) {
|
||
flying = true;
|
||
game.tween(game.self.ref,
|
||
{ x: p.x, y: p.y + 1, z: p.z },
|
||
{ duration: 0.5, easing: 'ease' });
|
||
}
|
||
});`,
|
||
});
|
||
}
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 27 — «Двойной прыжок»
|
||
// Паркур, где платформы так далеко, что без двойного прыжка
|
||
// не допрыгнуть. Скрипт включает игроку двойной прыжок.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game27DoubleJump() {
|
||
const blocks = [];
|
||
for (let x = -2; x <= 1; x++) {
|
||
for (let z = -2; z <= 1; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// платформы с большими разрывами
|
||
const plats = [
|
||
{ x: 0, y: 2, z: 7 },
|
||
{ x: 3, y: 3.5, z: 14 },
|
||
{ x: -3, y: 5, z: 21 },
|
||
{ x: 0, y: 6.5, z: 28 },
|
||
{ x: 3, y: 8, z: 35 },
|
||
];
|
||
const primitives = plats.map((p, i) => ({
|
||
id: i + 1, type: 'cube', name: 'Платформа_' + (i + 1),
|
||
x: p.x, y: p.y, z: p.z, sx: 2.5, sy: 0.5, sz: 2.5,
|
||
color: '#9b6b3e', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
}));
|
||
// Видимая финиш-площадка — куб, на неё встают (верх на y=8.75).
|
||
const FINISH_ID = plats.length + 1;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 3, y: 8.5, z: 40, sx: 4, sy: 0.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх площадки: onTouch тонкой плитки НЕ
|
||
// срабатывает, когда игрок просто СТОИТ на ней. Ловим телом игрока
|
||
// высокой невидимой зоной (canCollide:false — проходим сквозь).
|
||
const FINISH_ZONE_ID = plats.length + 2;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 3, y: 8.75 + 1.25, z: 40, sx: 4, sy: 2.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g27_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ДВОЙНОЙ ПРЫЖОК» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
// включаем игроку двойной прыжок — теперь можно прыгнуть
|
||
// ещё раз прямо в воздухе
|
||
game.player.setDoubleJump(true);
|
||
game.ui.showText('Жми Space ДВАЖДЫ — двойной прыжок!', 4);
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) {
|
||
game.player.respawn();
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Двойной прыжок освоен!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
},
|
||
{
|
||
// скрипт вешаем на невидимую ЗОНУ, а не на плоскую плитку
|
||
id: 'g27_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиш-зоны ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
floorEnabled: false, // паркур — между платформами пустота
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 28 — «Призрачные стены»
|
||
// Коридор перегорожен стенами. Некоторые стены — призрачные:
|
||
// кликни по стене, и она станет проходимой.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game28GhostWalls() {
|
||
const blocks = [];
|
||
for (let x = -5; x <= 4; x++) {
|
||
for (let z = -4; z <= 36; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
// 4 стены поперёк коридора — все призрачные.
|
||
const primitives = [];
|
||
let id = 0;
|
||
const wallZ = [6, 14, 22, 30];
|
||
const wallIds = wallZ.map((z) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Стена_' + id,
|
||
x: -0.5, y: 2.5, z, sx: 10, sy: 5, sz: 1,
|
||
color: '#7c5cff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// Видимый коврик-финиш НА полу (пол greystone y=0, верх y=1 → y=1.1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 34, sx: 5, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока, когда он
|
||
// просто стоит на финише (onTouch плоской плитки не срабатывает).
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1 + 1.25, z: 34, sx: 5, sy: 2.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g28_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПРИЗРАЧНЫЕ СТЕНЫ» — главный скрипт ===
|
||
|
||
game.ui.showText('Кликай по фиолетовым стенам — пройди сквозь!', 4);
|
||
|
||
// финиш сообщает о победе через broadcast — скрипты в разных
|
||
// песочницах, общая переменная между ними не видна.
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Ты прошёл сквозь все стены!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
for (const wid of wallIds) {
|
||
scripts.push({
|
||
id: 'g28_wall_' + wid, name: 'Стена_' + wid,
|
||
target: { kind: 'primitive', id: wid },
|
||
code:
|
||
`// === Скрипт призрачной стены ===
|
||
|
||
let ghost = false;
|
||
game.self.onClick(() => {
|
||
if (ghost) return;
|
||
ghost = true;
|
||
// стена становится проходимой и полупрозрачной
|
||
game.physics.passThrough(game.self.ref, true);
|
||
game.scene.setOpacity(game.self.ref, 0.25);
|
||
game.sound.play('click');
|
||
game.ui.showText('Стена стала призрачной!', 1.5);
|
||
});`,
|
||
});
|
||
}
|
||
scripts.push({
|
||
// скрипт вешаем на невидимую ЗОНУ, а не на плоский коврик
|
||
id: 'g28_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиш-зоны ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: -0.5, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 29 — «Магазин»
|
||
// Собери монетки, потом купи у продавца ключ за 5 монет (E).
|
||
// С ключом открой дверь и дойди до финиша.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game29Shop() {
|
||
const blocks = [];
|
||
for (let x = -9; x <= 8; x++) {
|
||
for (let z = -8; z <= 19; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'wood' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// 7 монеток для сбора.
|
||
const coinSpots = [
|
||
{ x: -6, z: -5 }, { x: 6, z: -5 }, { x: 0, z: -6 },
|
||
{ x: -7, z: 1 }, { x: 7, z: 1 }, { x: -3, z: -2 }, { x: 4, z: -3 },
|
||
];
|
||
const coinIds = coinSpots.map((c) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'sphere', name: 'Монетка_' + id,
|
||
x: c.x, y: 1, z: c.z, sx: 0.6, sy: 0.6, sz: 0.6,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// Прилавок продавца.
|
||
const SHOP_ID = ++id;
|
||
primitives.push({
|
||
id: SHOP_ID, type: 'cube', name: 'Прилавок',
|
||
x: 0, y: 0.8, z: 5, sx: 5, sy: 1.6, sz: 1.5,
|
||
color: '#7a4a26', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Стена + дверь.
|
||
primitives.push({
|
||
id: ++id, type: 'cube', name: 'Стена_лево',
|
||
x: -5, y: 2.5, z: 12, sx: 8, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
primitives.push({
|
||
id: ++id, type: 'cube', name: 'Стена_право',
|
||
x: 5, y: 2.5, z: 12, sx: 8, sy: 6, sz: 1,
|
||
color: '#8a8f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
const DOOR_ID = ++id;
|
||
primitives.push({
|
||
id: DOOR_ID, type: 'cube', name: 'Дверь',
|
||
x: 0, y: 2.5, z: 12, sx: 3, sy: 6, sz: 0.8,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Видимый коврик-финиш НА полу (пол wood y=0, верх y=1 → y=1.1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 0, y: 1.1, z: 16, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока, когда он
|
||
// стоит на финише (onTouch плоской плитки сам по себе не срабатывает).
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 0, y: 1 + 1.25, z: 16, sx: 4, sy: 2.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g29_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «МАГАЗИН» — главный скрипт ===
|
||
|
||
let coins = 0;
|
||
const PRICE = 5; // ключ стоит 5 монет
|
||
let bought = false;
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Собери монетки и купи ключ у продавца!', 4);
|
||
|
||
// монетки/прилавок/дверь/финиш шлют сюда сообщения через broadcast —
|
||
// скрипты в разных песочницах, состояние coins/bought живёт только здесь.
|
||
game.onMessage('coin', () => {
|
||
coins = coins + 1;
|
||
game.ui.score = coins;
|
||
game.sound.play('coin');
|
||
});
|
||
|
||
game.onMessage('buy', () => {
|
||
if (bought) {
|
||
game.ui.showText('Ключ уже куплен, иди к двери!', 2);
|
||
return;
|
||
}
|
||
if (coins < PRICE) {
|
||
game.ui.showText('Мало монет! Нужно ' + PRICE + ', есть ' + coins, 2);
|
||
game.sound.play('lose');
|
||
return;
|
||
}
|
||
bought = true;
|
||
coins = coins - PRICE;
|
||
game.ui.score = coins;
|
||
game.inventory.add({ name: 'Ключ' });
|
||
game.ui.showText('Куплен Ключ! Открой дверь.', 3);
|
||
game.sound.play('win');
|
||
});
|
||
|
||
game.onMessage('open-door', () => {
|
||
if (!game.inventory.has('Ключ')) {
|
||
game.ui.showText('Дверь заперта. Купи ключ в магазине.', 2);
|
||
return;
|
||
}
|
||
const door = game.scene.findOne('Дверь');
|
||
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
|
||
game.ui.showText('Дверь открыта!', 2);
|
||
});
|
||
|
||
game.onMessage('win', () => {
|
||
game.ui.showText('Победа! Ты прошёл магазин!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
for (const cid of coinIds) {
|
||
scripts.push({
|
||
id: 'g29_coin_' + cid, name: 'Монетка_' + cid,
|
||
target: { kind: 'primitive', id: cid },
|
||
code:
|
||
`// === Скрипт монетки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('coin');
|
||
game.self.delete();
|
||
});`,
|
||
});
|
||
}
|
||
scripts.push({
|
||
id: 'g29_shop', name: 'Прилавок', target: { kind: 'primitive', id: SHOP_ID },
|
||
code:
|
||
`// === Скрипт прилавка ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('buy');
|
||
}, { text: 'Купить ключ (5 монет)', distance: 4 });`,
|
||
});
|
||
scripts.push({
|
||
id: 'g29_door', name: 'Дверь', target: { kind: 'primitive', id: DOOR_ID },
|
||
code:
|
||
`// === Скрипт двери ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('open-door');
|
||
}, { text: 'Открыть дверь', distance: 4 });`,
|
||
});
|
||
scripts.push({
|
||
// скрипт вешаем на невидимую ЗОНУ, а не на плоский коврик
|
||
id: 'g29_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиш-зоны ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 60,
|
||
spawnPoint: { x: 0, y: 1, z: -3 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 30 — «Квест с заданиями»
|
||
// NPC даёт цепочку из 3 заданий: собери монетку, дойди до флага,
|
||
// вернись к NPC. Выполни всё — победа.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game30QuestTasks() {
|
||
const blocks = [];
|
||
for (let x = -12; x <= 11; x++) {
|
||
for (let z = -8; z <= 23; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Тумба-квестодатель (рядом стоит NPC).
|
||
const NPC_ID = ++id;
|
||
primitives.push({
|
||
id: NPC_ID, type: 'cube', name: 'Квестодатель',
|
||
x: 0, y: 0.8, z: 2, sx: 2, sy: 1.6, sz: 2,
|
||
color: '#7a4a26', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Монетка для задания 1.
|
||
const COIN_ID = ++id;
|
||
primitives.push({
|
||
id: COIN_ID, type: 'sphere', name: 'КвестМонетка',
|
||
x: -8, y: 1, z: 12, sx: 0.7, sy: 0.7, sz: 0.7,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Флаг для задания 2.
|
||
const FLAG_ID = ++id;
|
||
primitives.push({
|
||
id: FLAG_ID, type: 'cone', name: 'КвестФлаг',
|
||
x: 9, y: 1.2, z: 18, sx: 1.2, sy: 2.4, sz: 1.2,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g30_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
|
||
|
||
// этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага,
|
||
// 3=вернуться к NPC, 4=готово
|
||
let stage = 0;
|
||
|
||
// Текущая цель квеста — ПОСТОЯННАЯ надпись вверху экрана (game.ui.set).
|
||
// Реплики npc.say исчезают через пару секунд, поэтому цель всегда
|
||
// держим на HUD отдельной меткой 'objective' — игрок не теряет нить.
|
||
function setObjective(text) {
|
||
game.ui.set('objective', 'ЦЕЛЬ: ' + text,
|
||
{ x: 50, y: 8, color: '#ffe066', size: 24 });
|
||
}
|
||
setObjective('подойди к квестодателю и нажми E');
|
||
|
||
// создаём NPC рядом с тумбой
|
||
const npc = game.scene.spawnNpc('character-a', {
|
||
x: 1.5, y: 1, z: 2, name: 'Старейшина', hp: 100, speed: 0, // y=1 — на полу
|
||
});
|
||
|
||
// квестодатель/монетка/флаг шлют сюда сообщения через broadcast —
|
||
// скрипты в разных песочницах, этап квеста stage живёт только здесь.
|
||
game.onMessage('talk', () => {
|
||
if (stage === 0) {
|
||
stage = 1;
|
||
npc.say('Задание 1: найди жёлтую монетку!', 4);
|
||
setObjective('собери жёлтую монетку (слева)');
|
||
} else if (stage === 3) {
|
||
stage = 4;
|
||
npc.say('Молодец! Квест выполнен!', 4);
|
||
game.ui.set('objective', 'КВЕСТ ПРОЙДЕН!',
|
||
{ x: 50, y: 8, color: '#22dd55', size: 26 });
|
||
game.ui.showText('Победа! Квест пройден!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
} else if (stage === 4) {
|
||
npc.say('Спасибо, герой!', 3);
|
||
} else if (stage === 1) {
|
||
npc.say('Ты ещё не собрал монетку!', 3);
|
||
} else if (stage === 2) {
|
||
npc.say('Сначала дойди до синего флага!', 3);
|
||
}
|
||
});
|
||
|
||
game.onMessage('coin-done', () => {
|
||
if (stage !== 1) return;
|
||
stage = 2;
|
||
game.sound.play('coin');
|
||
npc.say('Отлично! Теперь дойди до синего флага.', 4);
|
||
game.ui.showText('Монетка собрана!', 2);
|
||
setObjective('дойди до синего флага (справа)');
|
||
});
|
||
|
||
game.onMessage('flag-done', () => {
|
||
if (stage !== 2) return;
|
||
stage = 3;
|
||
game.sound.play('pickup');
|
||
game.ui.showText('Флаг найден!', 2);
|
||
setObjective('вернись к квестодателю и нажми E');
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
id: 'g30_npc', name: 'Квестодатель', target: { kind: 'primitive', id: NPC_ID },
|
||
code:
|
||
`// === Скрипт квестодателя ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('talk');
|
||
}, { text: 'Поговорить', distance: 4 });`,
|
||
});
|
||
scripts.push({
|
||
id: 'g30_coin', name: 'КвестМонетка', target: { kind: 'primitive', id: COIN_ID },
|
||
code:
|
||
`// === Скрипт квест-монетки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('coin-done');
|
||
game.self.delete();
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
id: 'g30_flag', name: 'КвестФлаг', target: { kind: 'primitive', id: FLAG_ID },
|
||
code:
|
||
`// === Скрипт квест-флага ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('flag-done');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 65,
|
||
spawnPoint: { x: 0, y: 1, z: -4 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 31 — «Защита базы»
|
||
// NPC-враги идут к твоей базе. Кликай по врагам, чтобы их убрать.
|
||
// Не пропусти 5 врагов до базы — иначе проигрыш.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game31BaseDefense() {
|
||
const blocks = [];
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -4; z <= 40; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
const primitives = [];
|
||
let id = 0;
|
||
// База — синий куб у игрока.
|
||
const BASE_ID = ++id;
|
||
primitives.push({
|
||
id: BASE_ID, type: 'cube', name: 'База',
|
||
x: 0, y: 1.5, z: 0, sx: 5, sy: 3, sz: 3,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [{
|
||
id: 'g31_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ЗАЩИТА БАЗЫ» — главный скрипт ===
|
||
|
||
let killed = 0; // сколько врагов уничтожено
|
||
let leaked = 0; // сколько врагов дошло до базы
|
||
const GOAL = 12; // победа — уничтожить 12 врагов
|
||
const MAX_LEAK = 5; // проигрыш — 5 врагов прорвались
|
||
let over = false;
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Защити базу! Кликай по врагам', 3);
|
||
|
||
// каждые 2 секунды появляется новый враг далеко от базы
|
||
let total = 0;
|
||
game.every(2, () => {
|
||
if (over || total >= GOAL + MAX_LEAK) return;
|
||
total = total + 1;
|
||
const x = game.random(-8, 8);
|
||
const enemy = game.scene.spawnNpc('character-b', {
|
||
x: x, y: 1, z: 38, name: 'Враг', hp: 30, speed: 2.5, // y=1 — на полу
|
||
});
|
||
// враг идёт к базе (точка 0,0,0)
|
||
enemy.moveTo(0, 2);
|
||
|
||
let dead = false;
|
||
// клик по врагу — уничтожить
|
||
game.onClick(() => {
|
||
if (dead || over) return;
|
||
const p = game.player.position;
|
||
const e = enemy.position;
|
||
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 5) {
|
||
dead = true;
|
||
enemy.remove();
|
||
game.scene.spawnParticles('explosion', e, { count: 1 });
|
||
game.sound.play('hit');
|
||
killed = killed + 1;
|
||
game.ui.score = killed;
|
||
if (killed >= GOAL && !over) {
|
||
over = true;
|
||
game.ui.showText('Победа! База защищена!', 5);
|
||
game.sound.play('win');
|
||
}
|
||
}
|
||
});
|
||
|
||
// если враг дошёл до базы — пропуск
|
||
const watch = game.every(0.4, () => {
|
||
if (dead || over) { game.cancel(watch); return; }
|
||
// enemy.position готов не сразу после spawnNpc — ждём
|
||
const ep = enemy.position;
|
||
if (ep && ep.z < 4) {
|
||
dead = true;
|
||
game.cancel(watch);
|
||
enemy.remove();
|
||
leaked = leaked + 1;
|
||
game.sound.play('lose');
|
||
game.ui.showText('Враг прорвался! (' + leaked + '/' + MAX_LEAK + ')', 2);
|
||
if (leaked >= MAX_LEAK && !over) {
|
||
over = true;
|
||
game.ui.showText('База разрушена! Поражение.', 5);
|
||
}
|
||
}
|
||
});
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: 0, y: 1, z: 6 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 32 — «Гонка с кругами»
|
||
// Кольцевая трасса с чекпоинтами. Проедь 2 круга через чекпоинты
|
||
// по порядку. Засекается время.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game32LapRace() {
|
||
// Квадратная кольцевая трасса.
|
||
const blocks = [];
|
||
const inside = (x, z) => (Math.abs(x) <= 6 && Math.abs(z) <= 6);
|
||
for (let x = -14; x <= 14; x++) {
|
||
for (let z = -14; z <= 14; z++) {
|
||
if (inside(x, z)) continue; // дырка в центре
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
// 4 чекпоинта по углам трассы.
|
||
const cpSpots = [
|
||
{ x: 0, z: -11 }, { x: 11, z: 0 },
|
||
{ x: 0, z: 11 }, { x: -11, z: 0 },
|
||
];
|
||
const cpIds = cpSpots.map((c, i) => {
|
||
primitives.push({
|
||
id: i + 1, type: 'cube', name: 'Чекпоинт_' + (i + 1),
|
||
x: c.x, y: 1.5, z: c.z, sx: 4, sy: 3, sz: 0.5,
|
||
color: i === 0 ? '#22dd55' : '#ffcc33', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return i + 1;
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g32_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ГОНКА С КРУГАМИ» — главный скрипт ===
|
||
|
||
const LAPS = 2; // сколько кругов проехать
|
||
const CP_COUNT = 4; // чекпоинтов на круге
|
||
let nextCp = 0; // какой чекпоинт ждём (0..3)
|
||
let lap = 0; // текущий круг
|
||
let time = 0;
|
||
let won = false;
|
||
|
||
game.ui.timer = 0;
|
||
game.ui.showText('Проедь 2 круга через чекпоинты!', 3);
|
||
|
||
// Прогресс гонки — ПОСТОЯННАЯ надпись вверху по центру (game.ui.set).
|
||
// showText гаснет за пару секунд, а игрок должен всё время видеть,
|
||
// какой круг и какой чекпоинт ему нужен.
|
||
function updateProgress() {
|
||
game.ui.set('race',
|
||
'Круг ' + (lap + 1) + '/' + LAPS + ' • чекпоинт ' + (nextCp + 1) + '/' + CP_COUNT,
|
||
{ x: 50, y: 8, color: '#ffe066', size: 22 });
|
||
}
|
||
updateProgress();
|
||
|
||
game.onTick((dt) => {
|
||
if (won) return;
|
||
time = time + dt;
|
||
game.ui.timer = time;
|
||
});
|
||
|
||
// чекпоинты шлют сюда broadcast('checkpoint', { num }) — скрипты
|
||
// в разных песочницах, прогресс гонки nextCp/lap живёт только здесь.
|
||
game.onMessage('checkpoint', (d) => {
|
||
// игрок пересёк чекпоинт с номером n (1..4)
|
||
const n = d.num;
|
||
if (won) return;
|
||
// ждём именно следующий по порядку чекпоинт
|
||
if (n - 1 !== nextCp) return;
|
||
game.sound.play('click');
|
||
nextCp = nextCp + 1;
|
||
if (nextCp >= CP_COUNT) {
|
||
nextCp = 0;
|
||
lap = lap + 1;
|
||
if (lap >= LAPS) {
|
||
won = true;
|
||
const t = Math.round(time * 10) / 10;
|
||
game.ui.set('race', 'ФИНИШ! ' + t + ' сек',
|
||
{ x: 50, y: 8, color: '#22dd55', size: 24 });
|
||
game.ui.showText('Финиш! Круги пройдены за ' + t + ' сек', 6);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
} else {
|
||
game.ui.showText('Круг ' + lap + ' из ' + LAPS + '!', 2);
|
||
updateProgress();
|
||
}
|
||
} else {
|
||
updateProgress();
|
||
}
|
||
});`,
|
||
});
|
||
cpIds.forEach((cid, idx) => {
|
||
scripts.push({
|
||
id: 'g32_cp_' + (idx + 1), name: 'Чекпоинт_' + (idx + 1),
|
||
target: { kind: 'primitive', id: cid },
|
||
code:
|
||
`// === Скрипт чекпоинта ${idx + 1} ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('checkpoint', { num: ${idx + 1} });
|
||
});`,
|
||
});
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: -3, y: 1, z: -11 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 33 — «Платформер с боссом»
|
||
// Пройди паркур до арены, там — NPC-босс. Кликай по боссу, пока
|
||
// у него не кончится HP.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game33BossPlatformer() {
|
||
const blocks = [];
|
||
// Старт.
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = -3; z <= 0; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Арена босса.
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = 24; z <= 38; z++) {
|
||
blocks.push({ x, y: 6, z, type: 'rock' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
// Платформы паркура от старта (пол y=0, верх y=1) к арене (y=6, верх y=7).
|
||
// Подъём равномерный ~1.2 на платформу — допрыгивается обычным прыжком.
|
||
// Разрыв по z=4, лёгкий x-зигзаг.
|
||
const plats = [
|
||
{ x: 0, y: 2.0, z: 5 },
|
||
{ x: 1.5, y: 3.2, z: 9 },
|
||
{ x: -1.5, y: 4.4, z: 13 },
|
||
{ x: 0, y: 5.6, z: 17 },
|
||
];
|
||
plats.forEach((p, i) => {
|
||
primitives.push({
|
||
id: i + 1, type: 'cube', name: 'Платформа_' + (i + 1),
|
||
x: p.x, y: p.y, z: p.z, sx: 2.5, sy: 0.5, sz: 2.5,
|
||
color: '#9b6b3e', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
});
|
||
|
||
const scripts = [{
|
||
id: 'g33_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПЛАТФОРМЕР С БОССОМ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
let bossSpawned = false;
|
||
let bossHp = 120;
|
||
|
||
game.ui.showText('Пройди паркур до арены босса!', 3);
|
||
|
||
// упал — на старт
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (!p) return; // позиция игрока может быть не готова первые кадры
|
||
if (p.y < -3) {
|
||
game.player.respawn();
|
||
game.sound.play('lose');
|
||
}
|
||
// дошёл до арены — спавним босса один раз
|
||
if (!bossSpawned && p.z > 24 && p.y > 5) {
|
||
bossSpawned = true;
|
||
const boss = game.scene.spawnNpc('character-b', {
|
||
x: 0, y: 7, z: 32, name: 'БОСС', hp: bossHp, speed: 2, // y=7 — на полу арены (блоки y=6)
|
||
});
|
||
boss.follow('player');
|
||
game.scene.setLabel(boss.ref, 'БОСС HP: ' + bossHp, {
|
||
color: '#ff3333', height: 3.5
|
||
});
|
||
game.ui.showText('БОСС! Кликай по нему!', 3);
|
||
|
||
boss.onDeath(() => {
|
||
won = true;
|
||
game.scene.clearLabel(boss.ref);
|
||
game.ui.showText('Победа! Босс повержен!', 5);
|
||
game.sound.play('win');
|
||
const p2 = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p2.x, y: p2.y + 3, z: p2.z }, { duration: 3, count: 3 });
|
||
});
|
||
|
||
// удар по боссу кликом, если игрок близко
|
||
game.onClick(() => {
|
||
if (won) return;
|
||
const pp = game.player.position;
|
||
const bp = boss.position;
|
||
if (pp && bp && Math.hypot(pp.x - bp.x, pp.z - bp.z) < 5) {
|
||
bossHp = bossHp - 20;
|
||
if (bossHp < 0) bossHp = 0;
|
||
boss.damage(20);
|
||
game.scene.setLabel(boss.ref, 'БОСС HP: ' + bossHp, {
|
||
color: '#ff3333', height: 3.5
|
||
});
|
||
game.scene.spawnParticles('sparks', bp, { duration: 0.4 });
|
||
game.sound.play('hit');
|
||
}
|
||
});
|
||
}
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
floorEnabled: false, // паркур над пустотой до арены босса
|
||
spawnPoint: { x: 0, y: 1, z: -2 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 34 — «Сбор урожая»
|
||
// Грядки. Растения растут (твин). Когда выросло — собирай (E).
|
||
// Собрал слишком рано или поздно — не считается. Собери 6 спелых.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game34Harvest() {
|
||
const blocks = [];
|
||
for (let x = -9; x <= 8; x++) {
|
||
for (let z = -9; z <= 8; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'dirt' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
// 6 растений — конусы, изначально маленькие.
|
||
const plantSpots = [
|
||
{ x: -6, z: -6 }, { x: 0, z: -6 }, { x: 6, z: -6 },
|
||
{ x: -6, z: 4 }, { x: 0, z: 4 }, { x: 6, z: 4 },
|
||
];
|
||
// Растения стоят НА земле (пол dirt y=0, верх y=1). Конус sy=0.5 →
|
||
// центр y=1.25, чтобы низ конуса был на земле y=1 (иначе утоплен).
|
||
const plantIds = plantSpots.map((p, i) => {
|
||
primitives.push({
|
||
id: i + 1, type: 'cone', name: 'Растение_' + (i + 1),
|
||
x: p.x, y: 1.25, z: p.z, sx: 0.4, sy: 0.5, sz: 0.4,
|
||
color: '#7cb342', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return i + 1;
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g34_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «СБОР УРОЖАЯ» — главный скрипт ===
|
||
|
||
let harvested = 0;
|
||
const GOAL = ${plantIds.length};
|
||
game.ui.score = 0;
|
||
game.ui.showText('Дождись, пока растения вырастут, и собери!', 4);
|
||
|
||
// растения сообщают о сборе через broadcast('harvested') — скрипты
|
||
// в разных песочницах, счётчик harvested живёт только здесь.
|
||
game.onMessage('harvested', () => {
|
||
harvested = harvested + 1;
|
||
game.ui.score = harvested;
|
||
game.sound.play('coin');
|
||
if (harvested >= GOAL) {
|
||
game.ui.showText('Победа! Весь урожай собран!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
plantIds.forEach((pid) => {
|
||
scripts.push({
|
||
id: 'g34_plant_' + pid, name: 'Растение_' + pid,
|
||
target: { kind: 'primitive', id: pid },
|
||
code:
|
||
`// === Скрипт растения ===
|
||
|
||
let ripe = false; // растение выросло (спелое)?
|
||
let picked = false;
|
||
|
||
// растение медленно растёт за 5 секунд. Вместе с sy растёт и y —
|
||
// чтобы низ конуса оставался на земле (растение «растёт из земли»).
|
||
game.tween(game.self.ref,
|
||
{ sx: 1.3, sy: 2.6, sz: 1.3, y: 2.3 },
|
||
{ duration: 5, onDone: () => {
|
||
ripe = true;
|
||
game.scene.setColor(game.self.ref, '#ffcc33'); // спелое — жёлтое
|
||
}
|
||
});
|
||
|
||
// собрать растение (E)
|
||
game.self.onInteract(() => {
|
||
if (picked) return;
|
||
if (!ripe) {
|
||
game.ui.showText('Ещё не выросло! Подожди.', 1.5);
|
||
return;
|
||
}
|
||
picked = true;
|
||
game.self.delete();
|
||
game.broadcast('harvested');
|
||
}, { text: 'Собрать', distance: 3 });`,
|
||
});
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 35 — «Прятки от NPC»
|
||
// NPC-искатель ходит по полю. Прячься за стенами, чтобы он тебя
|
||
// не «увидел» (не подошёл близко на открытом месте). Доживи до конца.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game35HideFromNpc() {
|
||
const blocks = [];
|
||
for (let x = -13; x <= 12; x++) {
|
||
for (let z = -13; z <= 12; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Укрытия — стены-кубы по полю.
|
||
const primitives = [];
|
||
let id = 0;
|
||
const wallSpots = [
|
||
{ x: -7, z: -5 }, { x: 5, z: -7 }, { x: -3, z: 4 },
|
||
{ x: 8, z: 3 }, { x: 0, z: 9 }, { x: -9, z: 7 }, { x: 6, z: 9 },
|
||
];
|
||
for (const w of wallSpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Укрытие_' + id,
|
||
x: w.x, y: 2, z: w.z, sx: 4, sy: 4, sz: 1.5,
|
||
color: '#6b7280', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g35_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПРЯТКИ ОТ NPC» — главный скрипт ===
|
||
|
||
let time = 0;
|
||
const SURVIVE = 40; // продержись 40 секунд
|
||
let won = false;
|
||
|
||
game.ui.timer = 0;
|
||
game.ui.showText('Прячься за стенами 40 секунд!', 4);
|
||
|
||
// NPC-искатель ходит за игроком
|
||
const seeker = game.scene.spawnNpc('character-b', {
|
||
x: 0, y: 1, z: 10, name: 'Искатель', hp: 100, speed: 3, // y=1 — на полу
|
||
});
|
||
seeker.follow('player');
|
||
|
||
game.onTick((dt) => {
|
||
if (won) return;
|
||
time = time + dt;
|
||
game.ui.timer = time;
|
||
|
||
// искатель поймал — на старт, время продолжается.
|
||
// seeker.position появляется через кадр после spawnNpc — ждём.
|
||
const p = game.player.position;
|
||
const e = seeker.position;
|
||
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 1.7) {
|
||
game.player.respawn();
|
||
game.ui.showText('Найден! Прячься снова!', 1.5);
|
||
game.sound.play('lose');
|
||
}
|
||
|
||
// продержался — победа
|
||
if (time >= SURVIVE) {
|
||
won = true;
|
||
seeker.stop();
|
||
game.ui.showText('Победа! Ты прятался 40 секунд!', 5);
|
||
game.sound.play('win');
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: -10, y: 1, z: -10 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 36 — «Головоломка с ящиками»
|
||
// 3 ящика и 3 плиты-цели. Нажимай E на ящике — он перепрыгивает
|
||
// на следующую клетку. Расставь все ящики по своим плитам.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game36BoxPuzzle() {
|
||
const blocks = [];
|
||
for (let x = -9; x <= 8; x++) {
|
||
for (let z = -9; z <= 8; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// 3 плиты-цели — зелёные плоские круги-коврики НА полу
|
||
// (пол greystone y=0, верх y=1 → плита y=1.1, иначе утоплена).
|
||
const plateSpots = [
|
||
{ x: -6, z: 6 }, { x: 0, z: 6 }, { x: 6, z: 6 },
|
||
];
|
||
for (const p of plateSpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cylinder', name: 'Плита_' + id,
|
||
x: p.x, y: 1.1, z: p.z, sx: 2.4, sy: 0.2, sz: 2.4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// 3 ящика — куб 1.8, стоит НА полу: центр y = 1 + 0.9 = 1.9.
|
||
const boxIds = [];
|
||
for (let b = 0; b < 3; b++) {
|
||
id++;
|
||
boxIds.push(id);
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Ящик_' + (b + 1),
|
||
x: -6 + b * 6, y: 1.9, z: -6, sx: 1.8, sy: 1.8, sz: 1.8,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g36_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ГОЛОВОЛОМКА С ЯЩИКАМИ» — главный скрипт ===
|
||
|
||
// для каждого ящика — на какой плите он сейчас (true/false)
|
||
const onPlate = [false, false, false];
|
||
let won = false;
|
||
|
||
game.ui.showText('Поставь все 3 ящика на зелёные плиты!', 4);
|
||
|
||
// Ящики сообщают сюда через game.broadcast('box', { i, on }).
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('box', (d) => {
|
||
// ящик с номером d.i (0..2) встал/сошёл с плиты
|
||
onPlate[d.i] = d.on;
|
||
if (d.on) game.sound.play('click');
|
||
// все три ящика на плитах?
|
||
if (!won && onPlate[0] && onPlate[1] && onPlate[2]) {
|
||
won = true;
|
||
game.ui.showText('Победа! Все ящики на местах!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
});`,
|
||
});
|
||
boxIds.forEach((bid, idx) => {
|
||
// ящик idx ходит по ряду z = -6,-3,0,3,6; плита на z=6
|
||
scripts.push({
|
||
id: 'g36_box_' + (idx + 1), name: 'Ящик_' + (idx + 1),
|
||
target: { kind: 'primitive', id: bid },
|
||
code:
|
||
`// === Скрипт ящика ${idx + 1} ===
|
||
|
||
// ряд позиций по Z, по которым прыгает ящик
|
||
const ROW = [-6, -3, 0, 3, 6];
|
||
let cell = 0; // на какой клетке ряда стоит ящик
|
||
const PLATE_Z = 6; // плита в конце ряда
|
||
|
||
game.self.onInteract(() => {
|
||
// перепрыгиваем на следующую клетку (по кругу)
|
||
cell = (cell + 1) % ROW.length;
|
||
const z = ROW[cell];
|
||
game.tween(game.self.ref, { z: z }, { duration: 0.4, easing: 'ease' });
|
||
// сообщаем главному скрипту — стоит ли ящик на плите
|
||
game.broadcast('box', { i: ${idx}, on: z === PLATE_Z });
|
||
}, { text: 'Двинуть ящик', distance: 3 });`,
|
||
});
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: -8 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 37 — «Полоса препятствий»
|
||
// Длинная трасса: шипы (урон), движущаяся платформа, ямы. Доберись
|
||
// до финиша. Чекпоинт посередине.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game37ObstacleCourse() {
|
||
const blocks = [];
|
||
// Трасса с ямами: ямы там, где z в диапазонах.
|
||
const pit = (z) => (z >= 14 && z <= 17) || (z >= 30 && z <= 33);
|
||
for (let x = -4; x <= 3; x++) {
|
||
for (let z = -3; z <= 48; z++) {
|
||
if (pit(z)) continue;
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Шипы — конусы-урон в нескольких местах.
|
||
const spikeZ = [6, 9, 22, 25, 40, 43];
|
||
const spikeIds = spikeZ.map((z) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cone', name: 'Шип_' + id,
|
||
x: (id % 2 === 0) ? -1 : 1, y: 0.8, z,
|
||
sx: 1.2, sy: 1.6, sz: 1.2,
|
||
color: '#ff3344', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// Движущаяся платформа над первой ямой — на уровне пола
|
||
// (пол-блоки y=0 верх y=1; платформа sy=0.5 → центр y=0.75,
|
||
// верх вровень с полом, иначе утоплена и игрок по ней не идёт).
|
||
const MOVER_ID = ++id;
|
||
primitives.push({
|
||
id: MOVER_ID, type: 'cube', name: 'ДвижПлатформа',
|
||
x: -0.5, y: 0.75, z: 15.5, sx: 3, sy: 0.5, sz: 3,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Мост над второй ямой — узкая платформа на уровне пола.
|
||
primitives.push({
|
||
id: ++id, type: 'cube', name: 'Мостик',
|
||
x: -0.5, y: 0.75, z: 31.5, sx: 1.6, sy: 0.5, sz: 5,
|
||
color: '#9b6b3e', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Чекпоинт-флаг посередине.
|
||
const CP_ID = ++id;
|
||
primitives.push({
|
||
id: CP_ID, type: 'cone', name: 'Чекпоинт',
|
||
x: -0.5, y: 1.2, z: 24, sx: 1, sy: 2, sz: 1,
|
||
color: '#ffcc33', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш — видимый коврик НА полу (пол greystone y=0, верх y=1 → y=1.1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 46, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА поверх коврика — ловит игрока, когда он
|
||
// стоит на финише (onTouch плоской плитки сам по себе не срабатывает).
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1 + 1.25, z: 46, sx: 4, sy: 2.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g37_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПОЛОСА ПРЕПЯТСТВИЙ» — главный скрипт ===
|
||
|
||
let won = false;
|
||
|
||
game.ui.showText('Пройди полосу: шипы, ямы, платформа!', 4);
|
||
|
||
// движущаяся платформа ездит над ямой туда-сюда.
|
||
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже.
|
||
game.after(0.2, () => {
|
||
const mover = game.scene.findOne('ДвижПлатформа');
|
||
game.tween(mover, { x: 3 }, {
|
||
duration: 2, yoyo: true, repeat: 999, easing: 'ease'
|
||
});
|
||
});
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) {
|
||
game.player.respawn(); // упал в яму — на чекпоинт/старт
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// Чекпоинт и финиш сообщают сюда через game.broadcast.
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('checkpoint', () => {
|
||
// ставим точку возрождения на чекпоинт
|
||
game.player.setSpawn({ x: -0.5, y: 1, z: 24 });
|
||
game.ui.showText('Чекпоинт сохранён!', 2);
|
||
game.sound.play('pickup');
|
||
});
|
||
|
||
game.onMessage('finish', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Полоса пройдена!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
for (const sid of spikeIds) {
|
||
scripts.push({
|
||
id: 'g37_spike_' + sid, name: 'Шип_' + sid,
|
||
target: { kind: 'primitive', id: sid },
|
||
code:
|
||
`// === Скрипт шипа ===
|
||
game.self.onTouch(() => {
|
||
game.player.damage(25);
|
||
game.sound.play('hit');
|
||
});`,
|
||
});
|
||
}
|
||
scripts.push({
|
||
id: 'g37_cp', name: 'Чекпоинт', target: { kind: 'primitive', id: CP_ID },
|
||
code:
|
||
`// === Скрипт чекпоинта ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('checkpoint');
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
// скрипт вешаем на невидимую ЗОНУ, а не на плоский коврик
|
||
id: 'g37_finish', name: 'ФинишЗона', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиш-зоны ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 75,
|
||
floorEnabled: false, // трасса с ямами — в ямы нужно падать
|
||
spawnPoint: { x: -0.5, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 38 — «Музыкальная игра»
|
||
// Игра проигрывает последовательность звуков и подсвечивает плитки.
|
||
// Повтори последовательность, нажимая плитки (E) в том же порядке.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game38MusicGame() {
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -6; z <= 6; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'cotton-tan' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
// 4 цветные плитки.
|
||
const tiles = [
|
||
{ x: -5, color: '#e23b3b', snd: 'coin' },
|
||
{ x: -2, color: '#facc15', snd: 'jump' },
|
||
{ x: 2, color: '#22c55e', snd: 'click' },
|
||
{ x: 5, color: '#3b82f6', snd: 'hit' },
|
||
];
|
||
// Ноты-кнопки кладём НА пол (пол cotton-tan y=0, верх y=1 → y=1.15),
|
||
// иначе они утоплены в пол и не видны.
|
||
tiles.forEach((t, i) => {
|
||
primitives.push({
|
||
id: i + 1, type: 'cube', name: 'Нота_' + (i + 1),
|
||
x: t.x, y: 1.15, z: 0, sx: 2.4, sy: 0.4, sz: 2.4,
|
||
color: t.color, material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g38_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «МУЗЫКАЛЬНАЯ ИГРА» — главный скрипт ===
|
||
|
||
const SOUNDS = ['coin', 'jump', 'click', 'hit']; // плитки 1..4
|
||
// загаданная последовательность из 5 нот
|
||
const SEQ = [1, 3, 2, 4, 1];
|
||
let playerStep = 0; // на каком шаге игрок
|
||
let won = false;
|
||
let canPress = false;
|
||
|
||
game.ui.showText('Слушай мелодию, потом повтори!', 3);
|
||
|
||
// проигрываем мелодию: нота за нотой каждые 0.8 сек
|
||
SEQ.forEach((note, i) => {
|
||
game.after(1 + i * 0.8, () => {
|
||
game.sound.play(SOUNDS[note - 1]);
|
||
game.ui.showText('Нота ' + (i + 1) + ' из ' + SEQ.length, 0.7);
|
||
});
|
||
});
|
||
// после мелодии разрешаем игроку повторять
|
||
game.after(1 + SEQ.length * 0.8 + 0.5, () => {
|
||
canPress = true;
|
||
game.ui.showText('Теперь повтори мелодию!', 3);
|
||
});
|
||
|
||
// Плитки сообщают сюда нажатую ноту через game.broadcast('press', { n }).
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('press', (d) => {
|
||
if (won || !canPress) return;
|
||
// правильная нота?
|
||
if (d.n === SEQ[playerStep]) {
|
||
playerStep = playerStep + 1;
|
||
if (playerStep >= SEQ.length) {
|
||
won = true;
|
||
game.ui.showText('Победа! Мелодия повторена верно!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
} else {
|
||
// ошибка — сброс
|
||
playerStep = 0;
|
||
game.ui.showText('Ошибка! Слушай и пробуй снова.', 2);
|
||
game.sound.play('lose');
|
||
}
|
||
});`,
|
||
});
|
||
tiles.forEach((t, i) => {
|
||
scripts.push({
|
||
id: 'g38_tile_' + (i + 1), name: 'Нота_' + (i + 1),
|
||
target: { kind: 'primitive', id: i + 1 },
|
||
code:
|
||
`// === Скрипт ноты-плитки ${i + 1} ===
|
||
game.self.onInteract(() => {
|
||
game.sound.play('${t.snd}');
|
||
game.scene.spawnParticles('sparks', game.self.position,
|
||
{ duration: 0.4, color: '${t.color}' });
|
||
game.broadcast('press', { n: ${i + 1} });
|
||
}, { text: 'Сыграть ноту', distance: 3 });`,
|
||
});
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 1, z: 4 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 39 — «Башня — стройка»
|
||
// Скрипт говорит, где поставить блок. Подходи к подсвеченным местам
|
||
// и нажимай E — блок встаёт. Построй башню из 8 блоков.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game39TowerBuild() {
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -8; z <= 7; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
// 8 «мест-призраков» — полупрозрачные кубы один над другим.
|
||
const STEPS = 8;
|
||
for (let i = 0; i < STEPS; i++) {
|
||
primitives.push({
|
||
id: i + 1, type: 'cube', name: 'Место_' + (i + 1),
|
||
x: 0, y: 1 + i * 2, z: 0, sx: 2, sy: 2, sz: 2,
|
||
color: '#88aaff', material: 'glass',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g39_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «БАШНЯ — СТРОЙКА» — главный скрипт ===
|
||
|
||
const STEPS = ${STEPS};
|
||
let placed = 0; // сколько блоков поставлено
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Подходи к местам и ставь блоки (E) снизу вверх', 4);
|
||
|
||
// Места-призраки сообщают сюда через game.broadcast('place', { n }).
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('place', (d) => {
|
||
// блок поставлен в место номер d.n (1..STEPS);
|
||
// ставить можно только следующее по порядку место
|
||
if (d.n !== placed + 1) {
|
||
game.ui.showText('Сначала поставь блок ниже!', 1.5);
|
||
return;
|
||
}
|
||
placed = placed + 1;
|
||
game.ui.score = placed;
|
||
game.sound.play('click');
|
||
if (placed >= STEPS) {
|
||
game.ui.showText('Победа! Башня построена!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
} else {
|
||
game.ui.showText('Блок ' + placed + ' из ' + STEPS, 1.5);
|
||
}
|
||
});`,
|
||
});
|
||
for (let i = 1; i <= STEPS; i++) {
|
||
scripts.push({
|
||
id: 'g39_spot_' + i, name: 'Место_' + i,
|
||
target: { kind: 'primitive', id: i },
|
||
code:
|
||
`// === Скрипт места под блок ${i} ===
|
||
let built = false;
|
||
game.self.onInteract(() => {
|
||
if (built) return;
|
||
// ставим настоящий блок — делаем призрак твёрдым и непрозрачным
|
||
game.physics.passThrough(game.self.ref, false);
|
||
game.scene.setOpacity(game.self.ref, 1);
|
||
game.scene.setColor(game.self.ref, '#b5651d');
|
||
game.scene.setCollide(game.self.ref, true);
|
||
built = true;
|
||
game.broadcast('place', { n: ${i} });
|
||
}, { text: 'Поставить блок', distance: 4 });`,
|
||
});
|
||
}
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: 5 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 40 — «Выживание от волн»
|
||
// Волны NPC-врагов нападают одна за другой. Кликай по врагам.
|
||
// Продержись 3 волны — победа.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game40WaveSurvival() {
|
||
const blocks = [];
|
||
for (let x = -12; x <= 11; x++) {
|
||
for (let z = -12; z <= 11; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g40_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ВЫЖИВАНИЕ ОТ ВОЛН» — главный скрипт ===
|
||
|
||
const WAVES = 3; // всего волн
|
||
let wave = 0;
|
||
let won = false;
|
||
|
||
game.ui.showText('Отбей 3 волны врагов! Кликай по ним', 3);
|
||
|
||
// запускаем очередную волну
|
||
function startWave() {
|
||
if (won) return;
|
||
wave = wave + 1;
|
||
game.ui.showText('Волна ' + wave + ' из ' + WAVES + '!', 3);
|
||
game.sound.play('hit');
|
||
|
||
const count = wave + 2; // врагов всё больше: 3, 4, 5
|
||
let aliveInWave = count;
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
// враг появляется по краю поля
|
||
const angle = (i / count) * 6.28;
|
||
const ex = Math.cos(angle) * 10;
|
||
const ez = Math.sin(angle) * 10;
|
||
const enemy = game.scene.spawnNpc('character-b', {
|
||
x: ex, y: 1, z: ez, name: 'Враг', hp: 40, speed: 2, // y=1 — на полу
|
||
});
|
||
enemy.follow('player');
|
||
|
||
let dead = false;
|
||
game.onClick(() => {
|
||
if (dead || won) return;
|
||
const p = game.player.position;
|
||
const e = enemy.position;
|
||
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 5) {
|
||
dead = true;
|
||
enemy.remove();
|
||
game.scene.spawnParticles('explosion', e, { count: 1 });
|
||
game.sound.play('hit');
|
||
aliveInWave = aliveInWave - 1;
|
||
// вся волна перебита — следующая
|
||
if (aliveInWave <= 0) {
|
||
if (wave >= WAVES) {
|
||
won = true;
|
||
game.ui.showText('Победа! Все волны отбиты!', 5);
|
||
game.sound.play('win');
|
||
const pp = game.player.position;
|
||
if (pp) game.scene.spawnParticles('confetti',
|
||
{ x: pp.x, y: pp.y + 3, z: pp.z }, { duration: 3, count: 3 });
|
||
} else {
|
||
game.after(2, startWave);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
game.after(2, startWave); // первая волна через 2 секунды`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives: [], scripts,
|
||
worldSize: 70,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 41 — «Платформер-приключение»
|
||
// Большой уровень: паркур, монетки, шипы, чекпоинт, финиш-сокровище.
|
||
// Собирает вместе всё из простых уроков.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game41AdventurePlatformer() {
|
||
const blocks = [];
|
||
// Старт.
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = -3; z <= 0; z++) blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
// Площадка-чекпоинт посередине.
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = 26; z <= 30; z++) blocks.push({ x, y: 6, z, type: 'rock' });
|
||
}
|
||
// Финишная площадка.
|
||
for (let x = -3; x <= 2; x++) {
|
||
for (let z = 52; z <= 57; z++) blocks.push({ x, y: 11, z, type: 'rock' });
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// платформы паркура
|
||
const plats = [
|
||
{ x: 0, y: 1.5, z: 5 }, { x: 2, y: 2.5, z: 10 }, { x: -2, y: 3.5, z: 15 },
|
||
{ x: 0, y: 4.5, z: 20 }, { x: 2, y: 5.5, z: 24 },
|
||
{ x: 0, y: 7, z: 34 }, { x: -2, y: 8, z: 39 }, { x: 2, y: 9, z: 44 },
|
||
{ x: 0, y: 10, z: 49 },
|
||
];
|
||
for (const p of plats) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Платформа_' + id,
|
||
x: p.x, y: p.y, z: p.z, sx: 2.5, sy: 0.5, sz: 2.5,
|
||
color: '#9b6b3e', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// монетки на платформах
|
||
const coinIds = [];
|
||
const coinPlats = [5, 15, 24, 39, 49];
|
||
for (const cz of coinPlats) {
|
||
id++;
|
||
coinIds.push(id);
|
||
const pl = plats.find((p) => p.z === cz);
|
||
primitives.push({
|
||
id, type: 'sphere', name: 'Монетка_' + id,
|
||
x: pl.x, y: pl.y + 1.5, z: pl.z, sx: 0.6, sy: 0.6, sz: 0.6,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
// чекпоинт-флаг
|
||
const CP_ID = ++id;
|
||
primitives.push({
|
||
id: CP_ID, type: 'cone', name: 'Чекпоинт',
|
||
x: -0.5, y: 7.2, z: 28, sx: 1, sy: 2, sz: 1,
|
||
color: '#ffcc33', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// финиш-сокровище
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Сокровище',
|
||
x: -0.5, y: 12, z: 54, sx: 2, sy: 2, sz: 2,
|
||
color: '#ffd700', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g41_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ПЛАТФОРМЕР-ПРИКЛЮЧЕНИЕ» — главный скрипт ===
|
||
|
||
let coins = 0;
|
||
let won = false;
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Доберись до сокровища! Собирай монетки', 4);
|
||
|
||
game.onTick(() => {
|
||
if (won) return;
|
||
// позиция игрока может быть ещё не готова первые кадры —
|
||
// проверяем p перед обращением к p.y.
|
||
const p = game.player.position;
|
||
if (p && p.y < -3) {
|
||
game.player.respawn();
|
||
game.sound.play('lose');
|
||
}
|
||
});
|
||
|
||
// Монетки, чекпоинт и сокровище сообщают сюда через game.broadcast.
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('coin', () => {
|
||
coins = coins + 1;
|
||
game.ui.score = coins;
|
||
game.sound.play('coin');
|
||
});
|
||
|
||
game.onMessage('checkpoint', () => {
|
||
game.player.setSpawn({ x: -0.5, y: 7, z: 28 });
|
||
game.ui.showText('Чекпоинт! Дальше — отсюда.', 2);
|
||
game.sound.play('pickup');
|
||
});
|
||
|
||
game.onMessage('treasure', () => {
|
||
if (won) return;
|
||
won = true;
|
||
game.ui.showText('Победа! Сокровище и ' + coins + ' монет!', 6);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
for (const cid of coinIds) {
|
||
scripts.push({
|
||
id: 'g41_coin_' + cid, name: 'Монетка_' + cid,
|
||
target: { kind: 'primitive', id: cid },
|
||
code:
|
||
`// === Скрипт монетки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('coin');
|
||
game.self.delete();
|
||
});`,
|
||
});
|
||
}
|
||
scripts.push({
|
||
id: 'g41_cp', name: 'Чекпоинт', target: { kind: 'primitive', id: CP_ID },
|
||
code:
|
||
`// === Скрипт чекпоинта ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('checkpoint');
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
id: 'g41_finish', name: 'Сокровище', target: { kind: 'primitive', id: FINISH_ID },
|
||
code:
|
||
`// === Скрипт сокровища ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('treasure');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 90,
|
||
floorEnabled: false, // паркур с ямами — пола нет
|
||
spawnPoint: { x: 0, y: 1, z: -2 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 42 — «RPG-деревня»
|
||
// Деревня с двумя NPC и квестом: поговори со старостой → найди
|
||
// потерянный амулет → отнеси кузнецу → получи награду.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game42RpgVillage() {
|
||
const blocks = [];
|
||
for (let x = -16; x <= 15; x++) {
|
||
for (let z = -10; z <= 21; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Два «домика» из блоков.
|
||
for (let x = -13; x <= -8; x++) {
|
||
for (let z = 5; z <= 10; z++) {
|
||
for (let h = 1; h <= 4; h++) blocks.push({ x, y: h, z, type: 'wood' });
|
||
}
|
||
}
|
||
for (let x = 9; x <= 14; x++) {
|
||
for (let z = 5; z <= 10; z++) {
|
||
for (let h = 1; h <= 4; h++) blocks.push({ x, y: h, z, type: 'brick-red' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Тумба старосты.
|
||
const ELDER_ID = ++id;
|
||
primitives.push({
|
||
id: ELDER_ID, type: 'cube', name: 'Староста',
|
||
x: 0, y: 0.8, z: 2, sx: 1.8, sy: 1.6, sz: 1.8,
|
||
color: '#7a4a26', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Тумба кузнеца.
|
||
const SMITH_ID = ++id;
|
||
primitives.push({
|
||
id: SMITH_ID, type: 'cube', name: 'Кузнец',
|
||
x: 11, y: 0.8, z: 7, sx: 1.8, sy: 1.6, sz: 1.8,
|
||
color: '#4a4a4a', material: 'metal',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Амулет — спрятан за дальним домом.
|
||
const AMULET_ID = ++id;
|
||
primitives.push({
|
||
id: AMULET_ID, type: 'torus', name: 'Амулет',
|
||
x: -11, y: 1, z: 16, sx: 1, sy: 0.5, sz: 1,
|
||
color: '#a855f7', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g42_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «RPG-ДЕРЕВНЯ» — главный скрипт ===
|
||
|
||
// этап: 0=начало, 1=ищем амулет, 2=несём кузнецу, 3=готово
|
||
let stage = 0;
|
||
|
||
game.ui.showText('Деревня. Поговори со старостой (E)', 4);
|
||
|
||
const elder = game.scene.spawnNpc('character-a', {
|
||
x: 1.6, y: 1, z: 2, name: 'Староста', hp: 100, speed: 0, // y=1 — на полу
|
||
});
|
||
const smith = game.scene.spawnNpc('character-b', {
|
||
x: 12.6, y: 1, z: 7, name: 'Кузнец', hp: 100, speed: 0, // y=1 — на полу
|
||
});
|
||
|
||
// Староста, амулет и кузнец сообщают сюда через game.broadcast.
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('elderTalk', () => {
|
||
if (stage === 0) {
|
||
stage = 1;
|
||
elder.say('Найди потерянный амулет за домом!', 4);
|
||
game.ui.showText('Квест: найди фиолетовый амулет', 3);
|
||
} else if (stage === 1) {
|
||
elder.say('Амулет всё ещё не у тебя...', 3);
|
||
} else {
|
||
elder.say('Спасибо за помощь деревне!', 3);
|
||
}
|
||
});
|
||
|
||
game.onMessage('takeAmulet', () => {
|
||
if (stage !== 1) return;
|
||
stage = 2;
|
||
game.inventory.add({ name: 'Амулет' });
|
||
game.sound.play('pickup');
|
||
game.ui.showText('Амулет найден! Отнеси кузнецу.', 3);
|
||
});
|
||
|
||
game.onMessage('smithTalk', () => {
|
||
if (stage === 2 && game.inventory.has('Амулет')) {
|
||
stage = 3;
|
||
game.inventory.remove('Амулет');
|
||
smith.say('Отличный амулет! Вот награда, герой!', 4);
|
||
game.ui.showText('Победа! Квест RPG-деревни выполнен!', 6);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
} else if (stage === 3) {
|
||
smith.say('Доброго пути!', 3);
|
||
} else {
|
||
smith.say('Принеси мне амулет — поговори со старостой.', 4);
|
||
}
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
id: 'g42_elder', name: 'Староста', target: { kind: 'primitive', id: ELDER_ID },
|
||
code:
|
||
`// === Скрипт старосты ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('elderTalk');
|
||
}, { text: 'Поговорить со старостой', distance: 4 });`,
|
||
});
|
||
scripts.push({
|
||
id: 'g42_smith', name: 'Кузнец', target: { kind: 'primitive', id: SMITH_ID },
|
||
code:
|
||
`// === Скрипт кузнеца ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('smithTalk');
|
||
}, { text: 'Поговорить с кузнецом', distance: 4 });`,
|
||
});
|
||
scripts.push({
|
||
id: 'g42_amulet', name: 'Амулет', target: { kind: 'primitive', id: AMULET_ID },
|
||
code:
|
||
`// === Скрипт амулета ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('takeAmulet');
|
||
game.self.delete();
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 90,
|
||
spawnPoint: { x: 0, y: 1, z: -4 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 43 — «Гонка с препятствиями»
|
||
// Трасса с бустами скорости и шипами-ловушками. Доедь до финиша
|
||
// на время. Буст ускоряет, шип — урон и замедление.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game43ObstacleRace() {
|
||
const blocks = [];
|
||
for (let x = -4; x <= 3; x++) {
|
||
for (let z = 0; z <= 70; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
blocks.push({ x: -4, y: 1, z: 0, type: 'rock' });
|
||
}
|
||
for (let z = 0; z <= 70; z++) {
|
||
blocks.push({ x: -4, y: 1, z, type: 'rock' });
|
||
blocks.push({ x: 4, y: 1, z, type: 'rock' });
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// бусты — синие плитки-ускорители
|
||
const boostZ = [12, 30, 50];
|
||
const boostIds = boostZ.map((z) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Буст_' + id,
|
||
x: -0.5, y: 1.15, z, sx: 4, sy: 0.3, sz: 2,
|
||
color: '#22aaff', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// шипы-ловушки
|
||
const spikeZ = [20, 22, 40, 42, 60];
|
||
const spikeIds = spikeZ.map((z) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cone', name: 'Шип_' + id,
|
||
x: (id % 2 === 0) ? -2 : 2, y: 0.8, z,
|
||
sx: 1.2, sy: 1.6, sz: 1.2,
|
||
color: '#ff3344', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 68, sx: 8, sy: 0.2, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА в полный рост над ковриком — ловит игрока
|
||
// телом (тонкий коврик ноги «не касаются»).
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1 + 1.25, z: 68, sx: 8, sy: 2.5, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g43_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «ГОНКА С ПРЕПЯТСТВИЯМИ» — главный скрипт ===
|
||
|
||
let time = 0;
|
||
let won = false;
|
||
|
||
game.ui.timer = 0;
|
||
game.ui.showText('Гонка! Синее ускоряет, шипы мешают', 4);
|
||
|
||
game.onTick((dt) => {
|
||
if (won) return;
|
||
time = time + dt;
|
||
game.ui.timer = time;
|
||
});
|
||
|
||
// Бусты, шипы и финиш сообщают сюда через game.broadcast.
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('boost', () => {
|
||
// ускоряем игрока на 3 секунды
|
||
game.player.setSpeed(1.8);
|
||
game.sound.play('pickup');
|
||
game.ui.showText('УСКОРЕНИЕ!', 1);
|
||
game.after(3, () => game.player.setSpeed(1));
|
||
});
|
||
|
||
game.onMessage('spike', () => {
|
||
game.player.damage(15);
|
||
game.player.setSpeed(0.5); // шип замедляет
|
||
game.sound.play('hit');
|
||
game.after(1.5, () => game.player.setSpeed(1));
|
||
});
|
||
|
||
game.onMessage('finish', () => {
|
||
if (won) return;
|
||
won = true;
|
||
const t = Math.round(time * 10) / 10;
|
||
game.ui.showText('Финиш! Время: ' + t + ' сек', 6);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
for (const bid of boostIds) {
|
||
scripts.push({
|
||
id: 'g43_boost_' + bid, name: 'Буст_' + bid,
|
||
target: { kind: 'primitive', id: bid },
|
||
code:
|
||
`// === Скрипт буста ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('boost');
|
||
});`,
|
||
});
|
||
}
|
||
for (const sid of spikeIds) {
|
||
scripts.push({
|
||
id: 'g43_spike_' + sid, name: 'Шип_' + sid,
|
||
target: { kind: 'primitive', id: sid },
|
||
code:
|
||
`// === Скрипт шипа-ловушки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('spike');
|
||
});`,
|
||
});
|
||
}
|
||
scripts.push({
|
||
id: 'g43_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 90,
|
||
spawnPoint: { x: -0.5, y: 1, z: 2 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 44 — «Tower Defense»
|
||
// Враги идут по дороге к базе. Ставь башни (E на площадках) — башни
|
||
// сами стреляют по врагам рядом. Не пропусти 8 врагов.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game44TowerDefense() {
|
||
const blocks = [];
|
||
for (let x = -12; x <= 11; x++) {
|
||
for (let z = -4; z <= 44; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// Дорога врагов — полоса из камня по центру.
|
||
for (let x = -2; x <= 1; x++) {
|
||
for (let z = -4; z <= 44; z++) {
|
||
blocks.push({ x, y: 1, z, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// 4 площадки под башни — по бокам дороги.
|
||
const slotSpots = [
|
||
{ x: -6, z: 8 }, { x: 6, z: 14 }, { x: -6, z: 24 }, { x: 6, z: 32 },
|
||
];
|
||
const slotIds = slotSpots.map((s) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Площадка_' + id,
|
||
x: s.x, y: 1, z: s.z, sx: 3, sy: 2, sz: 3,
|
||
color: '#888f99', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// База в конце дороги.
|
||
const BASE_ID = ++id;
|
||
primitives.push({
|
||
id: BASE_ID, type: 'cube', name: 'База',
|
||
x: -0.5, y: 2, z: 42, sx: 5, sy: 4, sz: 3,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g44_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «TOWER DEFENSE» — главный скрипт ===
|
||
|
||
let leaked = 0; // врагов прошло до базы
|
||
const MAX_LEAK = 8;
|
||
let killed = 0;
|
||
const GOAL = 14; // победа — уничтожить 14 врагов
|
||
let over = false;
|
||
|
||
// список башен: {x, z}
|
||
const towers = [];
|
||
// список живых врагов: {npc, alive}
|
||
const enemies = [];
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Ставь башни (E)! Не пропусти врагов', 4);
|
||
|
||
// Площадки сообщают сюда о постройке башни через
|
||
// game.broadcast('addTower', { x, z }).
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('addTower', (d) => {
|
||
towers.push({ x: d.x, z: d.z });
|
||
game.sound.play('click');
|
||
game.ui.showText('Башня построена!', 1.5);
|
||
});
|
||
|
||
// спавн врагов
|
||
let total = 0;
|
||
game.every(2.2, () => {
|
||
if (over || total >= GOAL + MAX_LEAK) return;
|
||
total = total + 1;
|
||
const npc = game.scene.spawnNpc('character-b', {
|
||
x: -0.5, y: 1, z: -3, name: 'Враг', hp: 50, speed: 2,
|
||
});
|
||
npc.moveTo(-0.5, 42);
|
||
const rec = { npc: npc, alive: true };
|
||
enemies.push(rec);
|
||
npc.onDeath(() => {
|
||
rec.alive = false;
|
||
killed = killed + 1;
|
||
game.ui.score = killed;
|
||
if (killed >= GOAL && !over) {
|
||
over = true;
|
||
game.ui.showText('Победа! База защищена!', 5);
|
||
game.sound.play('win');
|
||
}
|
||
});
|
||
});
|
||
|
||
// башни стреляют: каждые 0.8с бьём врага рядом с любой башней
|
||
game.every(0.8, () => {
|
||
if (over) return;
|
||
for (const t of towers) {
|
||
for (const e of enemies) {
|
||
if (!e.alive) continue;
|
||
const p = e.npc.position;
|
||
// позиция NPC появляется через кадр после spawn — пропускаем
|
||
if (!p) continue;
|
||
if (Math.hypot(p.x - t.x, p.z - t.z) < 7) {
|
||
e.npc.damage(25);
|
||
game.scene.spawnParticles('sparks', p, { duration: 0.3 });
|
||
break; // одна башня — один выстрел за тик
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// проверка прорыва к базе
|
||
game.every(0.5, () => {
|
||
if (over) return;
|
||
for (const e of enemies) {
|
||
if (e.alive && e.npc.position && e.npc.position.z > 40) {
|
||
e.alive = false;
|
||
e.npc.remove();
|
||
leaked = leaked + 1;
|
||
game.sound.play('lose');
|
||
game.ui.showText('Враг прорвался! (' + leaked + '/' + MAX_LEAK + ')', 2);
|
||
if (leaked >= MAX_LEAK && !over) {
|
||
over = true;
|
||
game.ui.showText('База разрушена! Поражение.', 5);
|
||
}
|
||
}
|
||
}
|
||
});`,
|
||
});
|
||
slotIds.forEach((sid) => {
|
||
scripts.push({
|
||
id: 'g44_slot_' + sid, name: 'Площадка_' + sid,
|
||
target: { kind: 'primitive', id: sid },
|
||
code:
|
||
`// === Скрипт площадки под башню ===
|
||
let built = false;
|
||
game.self.onInteract(() => {
|
||
if (built) return;
|
||
built = true;
|
||
const pos = game.self.position;
|
||
|
||
// === ВАРИАНТ 1: башня — жёлтый цилиндр (по умолчанию) ===
|
||
game.scene.spawn('primitive:cylinder', {
|
||
x: pos.x, y: pos.y + 2.5, z: pos.z,
|
||
sx: 1.5, sy: 3, sz: 1.5,
|
||
color: '#ffcc33',
|
||
});
|
||
|
||
// === ВАРИАНТ 2: башня — своя модель из редактора моделей ===
|
||
//
|
||
// Чтобы поставить вместо цилиндра свою воксельную модель:
|
||
// 1. В редакторе проекта открой панель «Мои модели» и создай
|
||
// башню в редакторе воксельных моделей.
|
||
// 2. Узнай её id — он показан рядом с названием модели в панели
|
||
// «Мои модели» (например 3).
|
||
// 3. ЗАКОММЕНТИРУЙ блок выше (Вариант 1) и используй такой код:
|
||
//
|
||
// game.scene.spawn('user:3', { // 3 — id твоей модели
|
||
// x: pos.x, y: pos.y + 2, z: pos.z,
|
||
// rotationY: 0, // поворот вокруг вертикали
|
||
// });
|
||
//
|
||
// У пользовательских моделей нет sx/sy/sz и color — размер задан
|
||
// самой моделью в редакторе, цвет — её вокселями. Только позиция
|
||
// и поворот по Y. Высоту y подбери под высоту своей модели.
|
||
|
||
game.broadcast('addTower', { x: pos.x, z: pos.z });
|
||
}, { text: 'Построить башню', distance: 4 });`,
|
||
});
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 80,
|
||
spawnPoint: { x: -7, y: 1, z: 6 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 45 — «Стрелялка-арена»
|
||
// Арена с врагами. Кликай по врагам — они гибнут. Враги наносят
|
||
// урон при касании. Перебей 15 врагов, не потеряв всё HP.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game45ArenaShooter() {
|
||
const blocks = [];
|
||
for (let x = -13; x <= 12; x++) {
|
||
for (let z = -13; z <= 12; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'rock' });
|
||
}
|
||
}
|
||
// стены арены
|
||
for (let i = -13; i <= 12; i++) {
|
||
for (let h = 1; h <= 3; h++) {
|
||
blocks.push({ x: i, y: h, z: -13, type: 'greystone' });
|
||
blocks.push({ x: i, y: h, z: 12, type: 'greystone' });
|
||
blocks.push({ x: -13, y: h, z: i, type: 'greystone' });
|
||
blocks.push({ x: 12, y: h, z: i, type: 'greystone' });
|
||
}
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g45_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
|
||
|
||
let score = 0;
|
||
const GOAL = 15;
|
||
let over = false;
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Перебей 15 врагов! Кликай по ним', 3);
|
||
|
||
// проигрыш, когда HP игрока кончилось
|
||
game.onHpChange((e) => {
|
||
if (!over && e.hp <= 0) {
|
||
over = true;
|
||
game.ui.showText('Поражение! Тебя одолели враги.', 5);
|
||
}
|
||
});
|
||
|
||
// спавним нового врага каждые 1.8 сек
|
||
game.every(1.8, () => {
|
||
if (over || score >= GOAL) return;
|
||
const angle = game.random(0, 6.28);
|
||
const ex = Math.cos(angle) * 11;
|
||
const ez = Math.sin(angle) * 11;
|
||
const enemy = game.scene.spawnNpc('character-b', {
|
||
x: ex, y: 1, z: ez, name: 'Враг', hp: 30, speed: 2.2, // y=1 — на полу
|
||
});
|
||
enemy.follow('player');
|
||
|
||
let dead = false;
|
||
// враг бьёт игрока, если подошёл вплотную
|
||
const dmgTimer = game.every(0.7, () => {
|
||
if (dead || over) { game.cancel(dmgTimer); return; }
|
||
const p = game.player.position;
|
||
const e = enemy.position;
|
||
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 1.8) {
|
||
game.player.damage(10);
|
||
game.sound.play('hit');
|
||
}
|
||
});
|
||
|
||
// клик по врагу — убить
|
||
game.onClick(() => {
|
||
if (dead || over) return;
|
||
const p = game.player.position;
|
||
const e = enemy.position;
|
||
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 6) {
|
||
dead = true;
|
||
game.cancel(dmgTimer);
|
||
enemy.remove();
|
||
game.scene.spawnParticles('explosion', e, { count: 1 });
|
||
game.sound.play('hit');
|
||
score = score + 1;
|
||
game.ui.score = score;
|
||
if (score >= GOAL && !over) {
|
||
over = true;
|
||
game.ui.showText('Победа! Арена зачищена!', 5);
|
||
game.sound.play('win');
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
}
|
||
});
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives: [], scripts,
|
||
worldSize: 75,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 46 — «Кликер»
|
||
// Кликай по большому кубу — копятся очки. На очки покупай улучшения
|
||
// (E на кнопках): +урон за клик и авто-доход. Накопи 200 очков.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game46Clicker() {
|
||
const blocks = [];
|
||
for (let x = -8; x <= 7; x++) {
|
||
for (let z = -8; z <= 7; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'cotton-green' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Большой куб-кликер в центре.
|
||
const CUBE_ID = ++id;
|
||
primitives.push({
|
||
id: CUBE_ID, type: 'cube', name: 'Кликер',
|
||
x: 0, y: 2.5, z: 0, sx: 3, sy: 3, sz: 3,
|
||
color: '#ffcc33', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Кнопка апгрейда «сила клика».
|
||
const UP1_ID = ++id;
|
||
primitives.push({
|
||
id: UP1_ID, type: 'cube', name: 'УлучшениеСила',
|
||
x: -6, y: 1, z: 4, sx: 2, sy: 2, sz: 2,
|
||
color: '#e23b3b', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Кнопка апгрейда «авто-доход».
|
||
const UP2_ID = ++id;
|
||
primitives.push({
|
||
id: UP2_ID, type: 'cube', name: 'УлучшениеАвто',
|
||
x: 6, y: 1, z: 4, sx: 2, sy: 2, sz: 2,
|
||
color: '#3357ff', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g46_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «КЛИКЕР» — главный скрипт ===
|
||
|
||
let points = 0; // очки
|
||
let perClick = 1; // очков за клик
|
||
let autoIncome = 0; // очков в секунду автоматически
|
||
const GOAL = 200;
|
||
let won = false;
|
||
|
||
game.ui.score = 0;
|
||
game.ui.showText('Кликай по жёлтому кубу! Цель: 200 очков', 4);
|
||
|
||
// авто-доход: каждую секунду прибавляем autoIncome
|
||
game.every(1, () => {
|
||
if (won) return;
|
||
if (autoIncome > 0) {
|
||
points = points + autoIncome;
|
||
game.ui.score = points;
|
||
checkWin();
|
||
}
|
||
});
|
||
|
||
function checkWin() {
|
||
if (!won && points >= GOAL) {
|
||
won = true;
|
||
game.ui.showText('Победа! Накоплено 200 очков!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
}
|
||
}
|
||
|
||
// Куб-кликер и кнопки апгрейда сообщают сюда через game.broadcast.
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('click', () => {
|
||
if (won) return;
|
||
points = points + perClick;
|
||
game.ui.score = points;
|
||
game.sound.play('click');
|
||
checkWin();
|
||
});
|
||
|
||
game.onMessage('buyPower', () => {
|
||
if (points < 20) {
|
||
game.ui.showText('Нужно 20 очков для улучшения!', 1.5);
|
||
return;
|
||
}
|
||
points = points - 20;
|
||
perClick = perClick + 2;
|
||
game.ui.score = points;
|
||
game.sound.play('pickup');
|
||
game.ui.showText('Сила клика: +' + perClick + ' за клик', 2);
|
||
});
|
||
|
||
game.onMessage('buyAuto', () => {
|
||
if (points < 40) {
|
||
game.ui.showText('Нужно 40 очков для авто-дохода!', 1.5);
|
||
return;
|
||
}
|
||
points = points - 40;
|
||
autoIncome = autoIncome + 3;
|
||
game.ui.score = points;
|
||
game.sound.play('pickup');
|
||
game.ui.showText('Авто-доход: +' + autoIncome + ' в секунду', 2);
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
id: 'g46_cube', name: 'Кликер', target: { kind: 'primitive', id: CUBE_ID },
|
||
code:
|
||
`// === Скрипт куба-кликера ===
|
||
game.self.onClick(() => {
|
||
game.broadcast('click');
|
||
// куб слегка вспыхивает
|
||
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
|
||
});`,
|
||
});
|
||
scripts.push({
|
||
id: 'g46_up1', name: 'УлучшениеСила', target: { kind: 'primitive', id: UP1_ID },
|
||
code:
|
||
`// === Скрипт улучшения «сила клика» (20 очков) ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('buyPower');
|
||
}, { text: 'Купить +силу клика (20)', distance: 3 });`,
|
||
});
|
||
scripts.push({
|
||
id: 'g46_up2', name: 'УлучшениеАвто', target: { kind: 'primitive', id: UP2_ID },
|
||
code:
|
||
`// === Скрипт улучшения «авто-доход» (40 очков) ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('buyAuto');
|
||
}, { text: 'Купить авто-доход (40)', distance: 3 });`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 50,
|
||
spawnPoint: { x: 0, y: 1, z: 6 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 47 — «Квест-побег»
|
||
// Запертая комната. Найди 3 кнопки (одна спрятана), нажми все —
|
||
// откроется дверь выхода. Комната-головоломка.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game47EscapeQuest() {
|
||
const blocks = [];
|
||
// Пол + стены закрытой комнаты 20×20.
|
||
for (let x = -10; x <= 9; x++) {
|
||
for (let z = -10; z <= 9; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
for (let i = -10; i <= 9; i++) {
|
||
for (let h = 1; h <= 4; h++) {
|
||
blocks.push({ x: i, y: h, z: -10, type: 'rock' });
|
||
blocks.push({ x: i, y: h, z: 9, type: 'rock' });
|
||
blocks.push({ x: -10, y: h, z: i, type: 'rock' });
|
||
// правая стена с проёмом для двери (z от -1 до 1)
|
||
if (!(i >= -1 && i <= 1)) blocks.push({ x: 9, y: h, z: i, type: 'rock' });
|
||
}
|
||
}
|
||
|
||
const primitives = [];
|
||
let id = 0;
|
||
// Дверь выхода в правой стене.
|
||
const DOOR_ID = ++id;
|
||
primitives.push({
|
||
id: DOOR_ID, type: 'cube', name: 'Дверь',
|
||
x: 9, y: 2, z: 0, sx: 1, sy: 4, sz: 3,
|
||
color: '#b5651d', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// 3 кнопки: 2 на виду, 1 спрятана за ящиком.
|
||
const btnSpots = [
|
||
{ x: -7, z: -7 }, { x: 6, z: 6 }, { x: -6, z: 7 },
|
||
];
|
||
const btnIds = btnSpots.map((b) => {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cylinder', name: 'Кнопка_' + id,
|
||
x: b.x, y: 0.6, z: b.z, sx: 1.2, sy: 1.2, sz: 1.2,
|
||
color: '#e23b3b', material: 'neon',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
return id;
|
||
});
|
||
// Ящик, прячущий третью кнопку.
|
||
primitives.push({
|
||
id: ++id, type: 'cube', name: 'Ящик',
|
||
x: -6, y: 1.5, z: 5, sx: 2.5, sy: 3, sz: 2.5,
|
||
color: '#7a4a26', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Финиш за дверью — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: 13, y: 1.1, z: 0, sx: 4, sy: 0.2, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА в полный рост над ковриком — ловит игрока
|
||
// телом (тонкий коврик ноги «не касаются»).
|
||
const FINISH_ZONE_ID = ++id;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: 13, y: 1 + 1.25, z: 0, sx: 4, sy: 2.5, sz: 4,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [];
|
||
scripts.push({
|
||
id: 'g47_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
|
||
|
||
let pressed = 0; // сколько кнопок нажато
|
||
const TOTAL = 3;
|
||
let escaped = false;
|
||
|
||
game.ui.showText('Найди и нажми 3 кнопки, чтобы выйти!', 4);
|
||
|
||
// Кнопки и финиш сообщают сюда через game.broadcast.
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('pressButton', () => {
|
||
pressed = pressed + 1;
|
||
game.sound.play('click');
|
||
game.ui.showText('Кнопка ' + pressed + ' из ' + TOTAL, 1.5);
|
||
if (pressed >= TOTAL) {
|
||
// все кнопки — открываем дверь
|
||
const door = game.scene.findOne('Дверь');
|
||
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
|
||
game.ui.showText('Все кнопки нажаты! Дверь открыта!', 3);
|
||
game.sound.play('win');
|
||
}
|
||
});
|
||
|
||
game.onMessage('escape', () => {
|
||
if (escaped) return;
|
||
escaped = true;
|
||
game.ui.showText('Победа! Ты сбежал из комнаты!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
});`,
|
||
});
|
||
btnIds.forEach((bid, idx) => {
|
||
scripts.push({
|
||
id: 'g47_btn_' + (idx + 1), name: 'Кнопка_' + (idx + 1),
|
||
target: { kind: 'primitive', id: bid },
|
||
code:
|
||
`// === Скрипт кнопки ${idx + 1} ===
|
||
let used = false;
|
||
game.self.onInteract(() => {
|
||
if (used) return;
|
||
used = true;
|
||
game.scene.setColor(game.self.ref, '#22dd55'); // нажата — зелёная
|
||
game.broadcast('pressButton');
|
||
}, { text: 'Нажать кнопку', distance: 3 });`,
|
||
});
|
||
});
|
||
scripts.push({
|
||
id: 'g47_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
|
||
game.self.onTouch(() => {
|
||
game.broadcast('escape');
|
||
});`,
|
||
});
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 55,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 48 — «Мультиплеер: Салки»
|
||
// Догонялки на несколько игроков. Один водящий — он догоняет.
|
||
// В одиночку открывается как демо; настоящая игра — в комнате.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game48MpTag() {
|
||
const blocks = [];
|
||
for (let x = -14; x <= 13; x++) {
|
||
for (let z = -14; z <= 13; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
// укрытия
|
||
const primitives = [];
|
||
let id = 0;
|
||
const wallSpots = [
|
||
{ x: -7, z: -6 }, { x: 6, z: -7 }, { x: -3, z: 5 },
|
||
{ x: 8, z: 4 }, { x: 0, z: 9 }, { x: -9, z: 8 },
|
||
];
|
||
for (const w of wallSpots) {
|
||
id++;
|
||
primitives.push({
|
||
id, type: 'cube', name: 'Укрытие_' + id,
|
||
x: w.x, y: 2, z: w.z, sx: 3.5, sy: 4, sz: 1.5,
|
||
color: '#6b7280', material: 'matte',
|
||
canCollide: true, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g48_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
|
||
//
|
||
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
|
||
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
|
||
// игроков. В одиночку игра показывает только правила.
|
||
|
||
game.ui.showText('Салки! Опубликуй игру для игры с друзьями', 4);
|
||
|
||
// общий счётчик игроков комнаты — виден всем
|
||
function refresh() {
|
||
const n = game.players.count();
|
||
game.ui.set('info', 'Игроков в комнате: ' + n, { x: 50, y: 8 });
|
||
|
||
// водящий выбирается так: первый зашедший игрок
|
||
const all = game.players.all();
|
||
if (all.length > 0) {
|
||
game.room.set('tagger', all[0].sessionId);
|
||
}
|
||
}
|
||
|
||
// при входе/выходе игрока — обновляем
|
||
game.onPlayerJoin((p) => {
|
||
game.ui.showText(p.name + ' присоединился к салкам!', 2);
|
||
refresh();
|
||
});
|
||
game.onPlayerLeave(() => refresh());
|
||
|
||
refresh();
|
||
|
||
// если ты водящий — догоняй других; если убегаешь — прячься.
|
||
game.room.onChange('tagger', (taggerId) => {
|
||
const me = game.players.me();
|
||
if (me && me.sessionId === taggerId) {
|
||
game.ui.showText('Ты ВОДЯЩИЙ! Догоняй и осаль других!', 3);
|
||
} else {
|
||
game.ui.showText('Убегай от водящего!', 3);
|
||
}
|
||
});`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 80,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
multiplayer: true,
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 49 — «Мультиплеер: Гонка»
|
||
// Соревнование игроков на трассе. Кто первым доедет до финиша —
|
||
// его имя в общем счёте комнаты.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game49MpRace() {
|
||
const blocks = [];
|
||
for (let x = -5; x <= 4; x++) {
|
||
for (let z = 0; z <= 70; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'greystone' });
|
||
}
|
||
}
|
||
for (let z = 0; z <= 70; z++) {
|
||
blocks.push({ x: -5, y: 1, z, type: 'rock' });
|
||
blocks.push({ x: 4, y: 1, z, type: 'rock' });
|
||
}
|
||
|
||
const primitives = [];
|
||
// Финиш — видимый коврик НА полу (пол-блоки y=0, верх y=1).
|
||
const FINISH_ID = 1;
|
||
primitives.push({
|
||
id: FINISH_ID, type: 'cube', name: 'Финиш',
|
||
x: -0.5, y: 1.1, z: 68, sx: 10, sy: 0.2, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: true, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
// Невидимая ФИНИШ-ЗОНА в полный рост над ковриком — ловит игрока
|
||
// телом (тонкий коврик ноги «не касаются»).
|
||
const FINISH_ZONE_ID = 2;
|
||
primitives.push({
|
||
id: FINISH_ZONE_ID, type: 'cube', name: 'ФинишЗона',
|
||
x: -0.5, y: 1 + 1.25, z: 68, sx: 10, sy: 2.5, sz: 2,
|
||
color: '#22dd55', material: 'neon',
|
||
canCollide: false, visible: false, anchored: true, mass: 1,
|
||
rotationX: 0, rotationY: 0, rotationZ: 0,
|
||
});
|
||
|
||
const scripts = [
|
||
{
|
||
id: 'g49_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
|
||
//
|
||
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
|
||
// игру с галочкой «Мультиплеер».
|
||
|
||
game.ui.showText('Гонка! Беги к финишу первым', 3);
|
||
|
||
// показываем, сколько игроков и кто уже финишировал
|
||
function refresh() {
|
||
const n = game.players.count();
|
||
const winner = game.room.get('winner');
|
||
let txt = 'Игроков: ' + n;
|
||
if (winner) txt = txt + ' | Победил: ' + winner;
|
||
game.ui.set('info', txt, { x: 50, y: 8 });
|
||
}
|
||
refresh();
|
||
|
||
game.onPlayerJoin(() => refresh());
|
||
game.onPlayerLeave(() => refresh());
|
||
|
||
// когда кто-то финишировал — обновляем у всех
|
||
game.room.onChange('winner', () => refresh());
|
||
|
||
// Финиш сообщает сюда через game.broadcast('finish').
|
||
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
|
||
// видна, поэтому связь только через сообщения broadcast/onMessage.
|
||
game.onMessage('finish', () => {
|
||
// если победитель ещё не определён — записываем себя
|
||
if (!game.room.get('winner')) {
|
||
const me = game.players.me();
|
||
const myName = me ? me.name : 'Игрок';
|
||
game.room.set('winner', myName);
|
||
game.ui.showText('Ты пришёл первым! Победа!', 5);
|
||
game.sound.play('win');
|
||
const p = game.player.position;
|
||
game.scene.spawnParticles('confetti',
|
||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||
} else {
|
||
game.ui.showText('Финиш! Но кто-то был быстрее.', 4);
|
||
}
|
||
});`,
|
||
},
|
||
{
|
||
id: 'g49_finish', name: 'Финиш', target: { kind: 'primitive', id: FINISH_ZONE_ID },
|
||
code:
|
||
`// === Скрипт финиша ===
|
||
// Висит на невидимой зоне над ковриком — игрок входит в неё телом.
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish');
|
||
});`,
|
||
},
|
||
];
|
||
|
||
return wrap({
|
||
blocks, primitives, scripts,
|
||
worldSize: 90,
|
||
spawnPoint: { x: -0.5, y: 1, z: 2 },
|
||
multiplayer: true,
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// ИГРА 50 — «Своя игра»
|
||
// Не готовая игра, а чистая песочница: ровная площадка, пара блоков
|
||
// для примера и скрипт-приветствие. Урок учит придумывать свою игру.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
function game50MakeYourOwn() {
|
||
const blocks = [];
|
||
// большая ровная площадка для творчества
|
||
for (let x = -15; x <= 14; x++) {
|
||
for (let z = -15; z <= 14; z++) {
|
||
blocks.push({ x, y: 0, z, type: 'grass' });
|
||
}
|
||
}
|
||
|
||
const scripts = [{
|
||
id: 'g50_main', name: 'Главный скрипт', target: null,
|
||
code:
|
||
`// === «СВОЯ ИГРА» — твоя песочница ===
|
||
//
|
||
// Это пустая площадка. Здесь ты придумываешь и собираешь
|
||
// СВОЮ игру с нуля. Удали этот текст и пиши свой код.
|
||
//
|
||
// С чего начать:
|
||
// 1. Реши, КАКАЯ это игра (паркур / гонка / стрелялка / квест).
|
||
// 2. Построй сцену из блоков и примитивов.
|
||
// 3. Поставь точку спавна.
|
||
// 4. Добавь цель — финиш, счёт или врагов.
|
||
// 5. Напиши скрипты, оживляющие игру.
|
||
//
|
||
// Всё, что нужно, ты уже знаешь из уроков 1-49. Удачи!
|
||
|
||
game.ui.showText('Твоя песочница! Создай свою игру', 4);`,
|
||
}];
|
||
|
||
return wrap({
|
||
blocks, primitives: [], scripts,
|
||
worldSize: 80,
|
||
spawnPoint: { x: 0, y: 1, z: 0 },
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// Реестр билдеров. Ключ = id игры из docsGames.js.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
export const GAME_BUILDERS = {
|
||
'collect-coins': game1CollectCoins,
|
||
'platform-jump': game2PlatformJump,
|
||
'dont-fall': game3DontFall,
|
||
'button-door': game4ButtonDoor,
|
||
'maze': game5Maze,
|
||
'color-tiles': game6ColorTiles,
|
||
'catch-falling': game7CatchFalling,
|
||
'run-to-finish': game8RunToFinish,
|
||
'traffic-light': game9TrafficLight,
|
||
'spring-jump': game10SpringJump,
|
||
'echo-room': game11EchoRoom,
|
||
'code-door': game12CodeDoor,
|
||
'trader': game13Trader,
|
||
'collect-by-tag': game14CollectByTag,
|
||
'shooting-range': game15ShootingRange,
|
||
'lava-floor': game16LavaFloor,
|
||
'key-chest': game17KeyChest,
|
||
'swing': game18Swing,
|
||
'elevator': game19Elevator,
|
||
'enemy-names': game20EnemyNames,
|
||
'chaser': game21Chaser,
|
||
'danger-zone': game22DangerZone,
|
||
'switches': game23Switches,
|
||
'falling-bridge': game24FallingBridge,
|
||
'flyby-camera': game25FlybyCamera,
|
||
'coin-magnet': game26CoinMagnet,
|
||
'double-jump': game27DoubleJump,
|
||
'ghost-walls': game28GhostWalls,
|
||
'shop': game29Shop,
|
||
'quest-tasks': game30QuestTasks,
|
||
'base-defense': game31BaseDefense,
|
||
'lap-race': game32LapRace,
|
||
'boss-platformer': game33BossPlatformer,
|
||
'harvest': game34Harvest,
|
||
'hide-from-npc': game35HideFromNpc,
|
||
'box-puzzle': game36BoxPuzzle,
|
||
'obstacle-course': game37ObstacleCourse,
|
||
'music-game': game38MusicGame,
|
||
'tower-build': game39TowerBuild,
|
||
'wave-survival': game40WaveSurvival,
|
||
'adventure-platformer': game41AdventurePlatformer,
|
||
'rpg-village': game42RpgVillage,
|
||
'obstacle-race': game43ObstacleRace,
|
||
'tower-defense': game44TowerDefense,
|
||
'arena-shooter': game45ArenaShooter,
|
||
'clicker': game46Clicker,
|
||
'escape-quest': game47EscapeQuest,
|
||
'mp-tag': game48MpTag,
|
||
'mp-race': game49MpRace,
|
||
'make-your-own': game50MakeYourOwn,
|
||
};
|
||
|
||
/** Есть ли готовый билдер (играбельная версия) для игры с таким id. */
|
||
export function hasGameBuilder(id) {
|
||
return typeof GAME_BUILDERS[id] === 'function';
|
||
}
|
||
|
||
/** Построить project_data для игры-урока. Возвращает объект или null. */
|
||
export function buildGameProject(id) {
|
||
const fn = GAME_BUILDERS[id];
|
||
return fn ? fn() : null;
|
||
}
|