Добавлены все оставшиеся механики из TOOLBOX_KITS_FROM_WIKI.md: Мир: зона опасности, шипы, светофор, грядка-урожай, падающие предметы. Интерфейс: счётчик очков, HP-бар, дверь по коду (textbox), метка с именем, обратный отсчёт, 3D-стрелка-указатель. Эффекты: костёр (particles fire), магнит монет. NPC и бой (новая категория): преследователь, торговец (modal.dialog), мишень, враг с HP, волна врагов, диалог/кат-сцена, машина (vehicle:car). Экономика (новая категория): магазин-кнопка, кликер, ключ+замок. +2 категории китов (NPC и бой, Экономика). Всего ~37 китов. Опущены «Главное меню» и «Экран загрузки» — требуют целой сцены, не «1 клик». Все 45 скриптов прошли синтаксис-проверку, билд зелёный. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
817 lines
46 KiB
JavaScript
817 lines
46 KiB
JavaScript
/**
|
||
* GameplayKits — каталог готовых механик для Toolbox (задача 17, фаза T2).
|
||
*
|
||
* Каждый kit — это готовый кусок поведения, который автор вставляет одним кликом
|
||
* из Тулбокса (вкладка «Готовые механики»). При вставке:
|
||
* - scripts с attachTo:'global' → добавляются как глобальный скрипт игры;
|
||
* - scripts с attachTo:'on-target' → создаётся примитив-маркер + скрипт на нём;
|
||
* - prims[] → создаются примитивы на сцене (визуал кита).
|
||
*
|
||
* Все киты написаны НАМИ на белом-листе game-API (ScriptSandboxWorker) →
|
||
* заведомо безопасны, исполняются в существующем sandbox (нет доступа к DOM/fetch).
|
||
*
|
||
* Фича-парность: тот же файл копируется в rublox-player/src/engine/ (киты — это
|
||
* данные-скрипты, исполняются движком плеера так же).
|
||
*/
|
||
|
||
export const KIT_CATEGORIES = [
|
||
{ id: 'all', label: 'Все' },
|
||
{ id: 'movement', label: 'Движение' },
|
||
{ id: 'world', label: 'Мир' },
|
||
{ id: 'ui', label: 'Интерфейс' },
|
||
{ id: 'fx', label: 'Эффекты' },
|
||
{ id: 'npc', label: 'NPC и бой' },
|
||
{ id: 'economy', label: 'Экономика' },
|
||
];
|
||
|
||
export const GAMEPLAY_KITS = [
|
||
{
|
||
id: 'shift-to-run',
|
||
name: 'Бег на Shift',
|
||
desc: 'Игрок ускоряется в 1.8× при удержании Shift и возвращается к обычной скорости при отпускании.',
|
||
icon: 'zap', category: 'movement',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Бег на Shift
|
||
game.onKey('shift', () => game.player.setSpeed(1.8));
|
||
game.onKeyUp('shift', () => game.player.setSpeed(1.0));` }],
|
||
},
|
||
{
|
||
id: 'double-jump',
|
||
name: 'Двойной прыжок',
|
||
desc: 'Разрешает второй прыжок прямо в воздухе. Нажми Space ещё раз во время прыжка.',
|
||
icon: 'arrow-up', category: 'movement',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Двойной прыжок: второй прыжок в воздухе по Space
|
||
game.player.setDoubleJump(true);
|
||
game.ui.set('dj', 'Двойной прыжок включён! Жми Space в воздухе.', { x: 50, y: 90, anchor: 'bottom', color: '#fff', size: 16 });
|
||
game.after(5, () => game.ui.set('dj', ''));` }],
|
||
},
|
||
{
|
||
id: 'day-night-cycle',
|
||
name: 'Смена дня и ночи',
|
||
desc: 'Небо плавно переключается день → закат → ночь → день по кругу (использует Skybox задачи 16).',
|
||
icon: 'cloud', category: 'world',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Авто-цикл дня и ночи
|
||
const phases = ['clear-summer-day', 'sunset', 'starry-night', 'clear-summer-day'];
|
||
let i = 0;
|
||
game.scene.setSkybox({ preset: phases[0] });
|
||
game.every(8, () => {
|
||
i = (i + 1) % phases.length;
|
||
game.scene.skybox.fadeTo({ preset: phases[i] }, 3);
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'currency-counter',
|
||
name: 'Счётчик монет',
|
||
desc: 'Счётчик монет в углу HUD. Другие механики шлют game.broadcast("coins", {add: N}) — счётчик обновляется.',
|
||
icon: 'circle', category: 'ui',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Счётчик монет в HUD. Прибавить монеты из любого скрипта:
|
||
// game.broadcast('coins', { add: 100 });
|
||
let coins = 0;
|
||
function show() { game.ui.set('coins', '🪙 ' + coins, { x: 92, y: 6, anchor: 'top', color: '#ffd23a', size: 22 }); }
|
||
show();
|
||
game.onMessage('coins', (m) => { coins += (m && m.add) ? m.add : 1; show(); });` }],
|
||
},
|
||
{
|
||
id: 'start-pad',
|
||
name: 'Стартовая площадка',
|
||
desc: 'Светящаяся платформа — игрок появляется НА ней в начале игры (задаёт точку старта).',
|
||
icon: 'flag', category: 'world',
|
||
prims: [{ type: 'cylinder', x: 0, y: 0.15, z: 0, sx: 3, sy: 0.3, sz: 3, color: '#36d57a', material: 'neon', name: 'Стартовая площадка' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Игрок появляется на этой площадке в начале игры.
|
||
// Небольшая задержка — чтобы позиция объекта и игрок успели проинициализироваться.
|
||
game.after(0.1, () => {
|
||
const p = game.self.position;
|
||
game.player.teleport(p.x, p.y + 1.5, p.z);
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'checkpoint',
|
||
name: 'Чекпоинт',
|
||
desc: 'Светящийся столб-чекпоинт. При касании сохраняет прогресс и показывает уведомление.',
|
||
icon: 'flag', category: 'world',
|
||
prims: [{ type: 'cylinder', x: 0, y: 1.5, z: 0, sx: 0.6, sy: 3, sz: 0.6, color: '#4d6bff', material: 'neon', name: 'Чекпоинт' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Чекпоинт: касание → сообщение
|
||
game.self.onInteract(() => {
|
||
game.ui.set('cp', '✓ Чекпоинт сохранён!', { x: 50, y: 85, anchor: 'bottom', color: '#36d57a', size: 18 });
|
||
game.after(2, () => game.ui.set('cp', ''));
|
||
}, { text: 'Активировать', key: 'f', distance: 4 });` }],
|
||
},
|
||
{
|
||
id: 'confetti',
|
||
name: 'Конфетти',
|
||
desc: 'Праздничный фонтан конфетти из этого объекта. Кубики разлетаются и падают. Запускается периодически.',
|
||
icon: 'sparkles', category: 'fx',
|
||
prims: [{ type: 'sphere', x: 0, y: 1, z: 0, sx: 0.5, sy: 0.5, sz: 0.5, color: '#ff5ab0', material: 'neon', name: 'Конфетти-источник', canCollide: false }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Конфетти вылетает из ПОЗИЦИИ этого объекта (не из центра сцены).
|
||
function burst() {
|
||
const p = game.self.position; // где стоит конфетти-источник
|
||
for (let k = 0; k < 16; k++) {
|
||
const col = ['#ff5ab0','#ffd23a','#4d6bff','#36d57a','#ff7a3a'][k % 5];
|
||
game.scene.spawn('primitive:cube', {
|
||
x: p.x + (Math.random()-0.5)*0.6,
|
||
y: p.y + 0.5,
|
||
z: p.z + (Math.random()-0.5)*0.6,
|
||
sx: 0.22, sy: 0.22, sz: 0.22, color: col,
|
||
anchored: false, canCollide: false, lifetime: 2.5,
|
||
});
|
||
}
|
||
}
|
||
burst();
|
||
game.every(3, burst);` }],
|
||
},
|
||
{
|
||
id: 'floating-platform',
|
||
name: 'Парящая платформа',
|
||
desc: 'Платформа, которая плавно качается вверх-вниз — для паркура.',
|
||
icon: 'square', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 2, z: 0, sx: 4, sy: 0.5, sz: 4, color: '#c8a86a', material: 'matte', name: 'Парящая платформа' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Качание платформы вверх-вниз
|
||
let t = 0; const baseY = 2;
|
||
game.onTick((dt) => {
|
||
t += dt;
|
||
game.self.move(game.self.position.x, baseY + Math.sin(t * 1.5) * 1.2, game.self.position.z);
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'rotating-trap',
|
||
name: 'Вращающийся объект',
|
||
desc: 'Объект, который постоянно вращается — препятствие или декор.',
|
||
icon: 'refresh', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 1.5, z: 0, sx: 5, sy: 0.4, sz: 0.6, color: '#e0483c', material: 'matte', name: 'Вертушка' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Постоянное вращение
|
||
let a = 0;
|
||
game.onTick((dt) => { a += dt * 1.5; game.self.rotate(a); });` }],
|
||
},
|
||
{
|
||
id: 'timer-hud',
|
||
name: 'Таймер забега',
|
||
desc: 'Секундомер в HUD — считает время с начала игры. Основа для гонок на время.',
|
||
icon: 'clock', category: 'ui',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Таймер забега
|
||
let t = 0;
|
||
game.every(0.1, () => {
|
||
t += 0.1;
|
||
game.ui.set('timer', '⏱ ' + t.toFixed(1) + ' c', { x: 50, y: 6, anchor: 'top', color: '#ffffff', size: 22 });
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'welcome-message',
|
||
name: 'Приветствие',
|
||
desc: 'Показывает приветственное сообщение при входе в игру и убирает через 5 секунд.',
|
||
icon: 'message', category: 'ui',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Приветствие
|
||
game.ui.set('welcome', '👋 Добро пожаловать в игру!', { x: 50, y: 40, anchor: 'center', color: '#ffffff', size: 30 });
|
||
game.after(5, () => game.ui.set('welcome', ''));` }],
|
||
},
|
||
{
|
||
id: 'loot-crate',
|
||
name: 'Сундук с лутом',
|
||
desc: 'Золотой сундук. При взаимодействии «открывается» — даёт награду и сообщение.',
|
||
icon: 'box', category: 'world',
|
||
prims: [
|
||
{ type: 'cube', x: 0, y: 0.6, z: 0, sx: 1.6, sy: 1.2, sz: 1.2, color: '#b5862e', material: 'metal', name: 'Сундук' },
|
||
{ type: 'cube', x: 0, y: 1.35, z: 0, sx: 1.7, sy: 0.4, sz: 1.3, color: '#d4a843', material: 'metal', name: 'Крышка сундука', canCollide: false },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Сундук с лутом — даёт 100 монет (через счётчик монет, если он добавлен).
|
||
let opened = false;
|
||
game.self.onInteract(() => {
|
||
if (opened) { game.ui.set('loot', 'Сундук уже открыт.', { x: 50, y: 85, anchor: 'bottom', color: '#bbb', size: 16 }); return; }
|
||
opened = true;
|
||
game.broadcast('coins', { add: 100 }); // обновит «Счётчик монет», если он есть
|
||
game.ui.set('loot', '🎁 Ты получил награду: +100 монет!', { x: 50, y: 85, anchor: 'bottom', color: '#ffd23a', size: 18 });
|
||
game.after(3, () => game.ui.set('loot', ''));
|
||
}, { text: 'Открыть сундук', key: 'f', distance: 4 });` }],
|
||
},
|
||
|
||
// ===== Партия 1 из Вики (киты 13-17) =====
|
||
|
||
{
|
||
id: 'trampoline',
|
||
name: 'Батут (пружина)',
|
||
desc: 'Яркая платформа-батут — наступи на неё, и игрока подбросит высоко вверх. (Вики: «Прыжок-пружина»)',
|
||
icon: 'arrow-up', category: 'movement',
|
||
prims: [{ type: 'cylinder', x: 0, y: 0.3, z: 0, sx: 3, sy: 0.6, sz: 3, color: '#ff3c8e', material: 'neon', name: 'Батут' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Батут: касание → подброс игрока вверх.
|
||
game.self.onTouch(() => {
|
||
game.player.setVy(20); // вертикальный импульс (как трамплин)
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'speed-pad',
|
||
name: 'Лента ускорения',
|
||
desc: 'Жёлтая плита-ускоритель — наступи, и игрок бежит быстрее несколько секунд. (Вики: «бусты скорости»)',
|
||
icon: 'zap', category: 'movement',
|
||
prims: [{ type: 'cube', x: 0, y: 0.1, z: 0, sx: 3, sy: 0.2, sz: 5, color: '#ffd23a', material: 'neon', name: 'Лента ускорения', canCollide: false }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Лента ускорения: касание → x2 скорости на 3 секунды.
|
||
let boosting = false;
|
||
game.self.onTouch(() => {
|
||
if (boosting) return;
|
||
boosting = true;
|
||
game.player.setSpeed(2.0);
|
||
game.ui.set('boost', '⚡ Ускорение!', { x: 50, y: 80, anchor: 'bottom', color: '#ffd23a', size: 18 });
|
||
game.after(3, () => { game.player.setSpeed(1.0); game.ui.set('boost', ''); boosting = false; });
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'teleport-portal',
|
||
name: 'Портал-телепорт',
|
||
desc: 'Два синих портала. Вошёл в портал — мгновенно переносишься ко второму. Поставь второй портал где нужно. (Вики: «секретный путь»)',
|
||
icon: 'sparkles', category: 'movement',
|
||
prims: [
|
||
{ type: 'cylinder', x: 0, y: 1.5, z: 0, sx: 0.4, sy: 3, sz: 3, color: '#4d6bff', material: 'neon', name: 'Портал A' },
|
||
{ type: 'cylinder', x: 8, y: 1.5, z: 0, sx: 0.4, sy: 3, sz: 3, color: '#4dffd6', material: 'neon', name: 'Портал B', canCollide: false },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Портал A: касание → телепорт к «Портал B» (ищем его по имени в момент входа).
|
||
// Передвигай «Портал B» куда угодно — телепорт всегда попадёт к нему.
|
||
let cd = false;
|
||
game.self.onTouch(() => {
|
||
if (cd) return;
|
||
const b = game.scene.findOne('Портал B'); // ищем второй портал
|
||
if (!b || !b.position) return;
|
||
cd = true;
|
||
game.player.teleport(b.position.x, b.position.y + 1, b.position.z);
|
||
game.after(1.2, () => { cd = false; }); // защита от повторного входа
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'disappearing-platform',
|
||
name: 'Исчезающая платформа',
|
||
desc: 'Платформа пропадает под ногами через секунду после касания и возвращается через 3с. (Вики: «Не упади», «Падающий мост»)',
|
||
icon: 'square', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 0.25, z: 0, sx: 4, sy: 0.5, sz: 4, color: '#e06a3c', material: 'matte', name: 'Исчезающая платформа' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Исчезающая платформа: наступил → через 1с пропадает, через 3с возвращается.
|
||
let busy = false;
|
||
game.self.onTouch(() => {
|
||
if (busy) return; busy = true;
|
||
game.after(1, () => { game.self.setVisible(false); game.self.setCollide(false); });
|
||
game.after(3, () => { game.self.setVisible(true); game.self.setCollide(true); busy = false; });
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'door-button',
|
||
name: 'Дверь по кнопке E',
|
||
desc: 'Красивая дверь с рамкой, филёнками и ручкой. Нажми E — плавно распахивается вокруг петли. (Вики: «Кнопка-открывашка»)',
|
||
icon: 'door', category: 'world',
|
||
// Полотно двери — ПЕРВЫЙ prim (на нём скрипт). Остальные части — рамка
|
||
// (неподвижный косяк) + декор полотна. Всё уходит в одну папку.
|
||
prims: [
|
||
// 0) Полотно двери (тёмное дерево).
|
||
{ type: 'cube', x: 0, y: 2, z: 0, sx: 0.25, sy: 3.8, sz: 2.6, color: '#6b4423', material: 'matte', name: 'Полотно двери' },
|
||
// Филёнки (светлее, чуть выступают) — верхняя и нижняя.
|
||
{ type: 'cube', x: 0.16, y: 2.9, z: 0, sx: 0.08, sy: 1.2, sz: 1.9, color: '#8a5a2e', material: 'matte', canCollide: false, name: 'Филёнка верх' },
|
||
{ type: 'cube', x: 0.16, y: 1.2, z: 0, sx: 0.08, sy: 1.4, sz: 1.9, color: '#8a5a2e', material: 'matte', canCollide: false, name: 'Филёнка низ' },
|
||
// Ручка (золотая).
|
||
{ type: 'sphere', x: 0.28, y: 2, z: 0.95, sx: 0.3, sy: 0.3, sz: 0.3, color: '#e0b030', material: 'metal', canCollide: false, name: 'Ручка' },
|
||
// Косяк-рамка (неподвижная) — две стойки + перемычка.
|
||
{ type: 'cube', x: 0, y: 2, z: -1.55, sx: 0.4, sy: 4.2, sz: 0.4, color: '#4a3018', material: 'matte', name: 'Косяк левый' },
|
||
{ type: 'cube', x: 0, y: 2, z: 1.55, sx: 0.4, sy: 4.2, sz: 0.4, color: '#4a3018', material: 'matte', name: 'Косяк правый' },
|
||
{ type: 'cube', x: 0, y: 4.1, z: 0, sx: 0.4, sy: 0.4, sz: 3.5, color: '#4a3018', material: 'matte', name: 'Перемычка' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Дверь по E: ПЛАВНО поворачивается вокруг петли (левой грани).
|
||
// Скрипт на полотне двери. Филёнки/ручка двигаются вместе как часть полотна?
|
||
// Нет — они отдельные примитивы, поэтому анимируем только полотно (game.self),
|
||
// а декор оставляем — на низкой толщине двери смотрится цельно.
|
||
const p0 = game.self.position; // центр полотна (закрытое положение)
|
||
const halfW = 1.3; // половина ширины полотна по Z (sz=2.6)
|
||
const hingeX = p0.x;
|
||
const hingeZ = p0.z - halfW; // петля у левого края
|
||
let open = false;
|
||
let cur = 0, target = 0; // текущий и целевой угол (радианы)
|
||
const SPEED = Math.PI; // рад/сек → ~0.5с на 90°
|
||
|
||
// Декор полотна — двигаем вместе с дверью. Запоминаем их СМЕЩЕНИЕ относительно
|
||
// центра полотна (в закрытом виде), чтобы вращать вокруг той же петли.
|
||
const decorNames = ['Филёнка верх', 'Филёнка низ', 'Ручка'];
|
||
const decor = [];
|
||
for (const nm of decorNames) {
|
||
const o = game.scene.findOne(nm);
|
||
if (o && o.position) {
|
||
decor.push({ obj: o, dx: o.position.x - p0.x, dy: o.position.y - p0.y, dz: o.position.z - p0.z });
|
||
}
|
||
}
|
||
|
||
// Поворот локального вектора (lx,lz) вокруг оси Y на angle — согласованно с
|
||
// тем, как Babylon поворачивает меш при rotation.y=angle (левосторонняя СК).
|
||
function rotY(lx, lz, a) {
|
||
const s = Math.sin(a), c = Math.cos(a);
|
||
return { x: lx * c + lz * s, z: -lx * s + lz * c };
|
||
}
|
||
function place(angle) {
|
||
// Полотно: центр = петля + повёрнутый локальный вектор (0, +halfW).
|
||
const pc = rotY(0, halfW, angle);
|
||
const cx = hingeX + pc.x;
|
||
const cz = hingeZ + pc.z;
|
||
game.self.move(cx, p0.y, cz);
|
||
game.self.rotate(angle);
|
||
// Декор: центр полотна + повёрнутое локальное смещение (той же формулой).
|
||
for (const d of decor) {
|
||
const r = rotY(d.dx, d.dz, angle);
|
||
d.obj.move(cx + r.x, p0.y + d.dy, cz + r.z);
|
||
if (d.obj.rotate) d.obj.rotate(angle);
|
||
}
|
||
}
|
||
|
||
// Один постоянный тик плавно ведёт cur → target.
|
||
game.onTick((dt) => {
|
||
if (cur === target) return;
|
||
const step = SPEED * dt;
|
||
if (Math.abs(target - cur) <= step) cur = target;
|
||
else cur += Math.sign(target - cur) * step;
|
||
place(cur);
|
||
});
|
||
game.self.onInteract(() => {
|
||
open = !open;
|
||
target = open ? Math.PI / 2 : 0; // 90° открыта / 0° закрыта
|
||
}, { text: 'Открыть / закрыть', key: 'e', distance: 5 });` }],
|
||
},
|
||
|
||
// ===== Партия 2 из Вики (киты 18-22) =====
|
||
|
||
{
|
||
id: 'color-tiles',
|
||
name: 'Цветная плитка',
|
||
desc: 'Наступи на плитку — она меняет цвет на случайный. (Вики: «Цветные плитки»)',
|
||
icon: 'palette', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 0.1, z: 0, sx: 2.5, sy: 0.2, sz: 2.5, color: '#cfd8dc', material: 'matte', name: 'Цветная плитка' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Плитка меняет цвет при касании.
|
||
const colors = ['#ff5ab0','#ffd23a','#4d6bff','#36d57a','#ff7a3a','#a05aff'];
|
||
let i = 0;
|
||
game.self.onTouch(() => {
|
||
i = (i + 1) % colors.length;
|
||
game.self.setColor(colors[i]);
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'lava-floor',
|
||
name: 'Лава (урон по касанию)',
|
||
desc: 'Раскалённая плита: наступишь — теряешь здоровье каждую секунду, пока стоишь. (Вики: «Лава-пол»)',
|
||
icon: 'lava', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 0.1, z: 0, sx: 5, sy: 0.2, sz: 5, color: '#ff4422', material: 'neon', name: 'Лава' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Лава: пока игрок на плите — урон каждую секунду.
|
||
let onLava = false, timer = null;
|
||
game.self.onTouch(() => {
|
||
if (onLava) return; onLava = true;
|
||
const tick = () => { if (!onLava) return; game.player.damage(15);
|
||
game.ui.set('lava', '🔥 Горячо! -15 HP', { x: 50, y: 80, anchor: 'bottom', color: '#ff6644', size: 18 });
|
||
timer = game.after(1, tick); };
|
||
tick();
|
||
});
|
||
game.self.onUntouch(() => { onLava = false; if (timer) game.cancel(timer); game.ui.set('lava', ''); });` }],
|
||
},
|
||
{
|
||
id: 'elevator',
|
||
name: 'Лифт',
|
||
desc: 'Платформа-лифт сама ездит вверх-вниз между двумя этажами. Встань на неё и катайся. (Вики: «Лифт»)',
|
||
icon: 'elevator', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 0.5, z: 0, sx: 4, sy: 0.5, sz: 4, color: '#7a8a9a', material: 'metal', name: 'Лифт' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Лифт: плавно ездит между нижней и верхней высотой.
|
||
const p0 = game.self.position;
|
||
const lowY = p0.y, highY = p0.y + 8;
|
||
let t = 0;
|
||
game.onTick((dt) => {
|
||
t += dt;
|
||
// Синусоида 0..1 с паузами на концах (период ~8с).
|
||
const k = (Math.sin(t * 0.5 - Math.PI/2) + 1) / 2;
|
||
game.self.move(p0.x, lowY + (highY - lowY) * k, p0.z);
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'finish-line',
|
||
name: 'Финиш (победа)',
|
||
desc: 'Финишная плита: дойди до неё — на экране «ПОБЕДА!» и управление блокируется. (Вики: «Беги к финишу»)',
|
||
icon: 'flag', category: 'ui',
|
||
prims: [{ type: 'cube', x: 0, y: 0.15, z: 0, sx: 4, sy: 0.3, sz: 2, color: '#ffd23a', material: 'neon', name: 'Финиш', canCollide: false }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Финиш: касание → экран победы.
|
||
let done = false;
|
||
game.self.onTouch(() => {
|
||
if (done) return; done = true;
|
||
game.ui.set('win', '🏆 ПОБЕДА!', { x: 50, y: 42, anchor: 'center', color: '#ffd23a', size: 48 });
|
||
game.ui.set('winsub', 'Ты дошёл до финиша!', { x: 50, y: 54, anchor: 'center', color: '#fff', size: 22 });
|
||
game.player.setInputBlocked(true);
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'sound-tile',
|
||
name: 'Звуковая плитка',
|
||
desc: 'Наступи на плитку — играет звук. Из таких можно собрать мелодию. (Вики: «Эхо-комната»)',
|
||
icon: 'sound', category: 'fx',
|
||
prims: [{ type: 'cube', x: 0, y: 0.1, z: 0, sx: 2, sy: 0.2, sz: 2, color: '#6f8bff', material: 'neon', name: 'Звуковая плитка' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Плитка играет звук при касании + подсвечивается.
|
||
let cd = false;
|
||
game.self.onTouch(() => {
|
||
if (cd) return; cd = true;
|
||
game.sound.play('coin');
|
||
game.self.setColor('#ffffff');
|
||
game.after(0.25, () => { game.self.setColor('#6f8bff'); cd = false; });
|
||
});` }],
|
||
},
|
||
|
||
// ===== Партия 3 из Вики (остальные механики) =====
|
||
|
||
// --- Мир ---
|
||
{
|
||
id: 'damage-zone',
|
||
name: 'Зона опасности',
|
||
desc: 'Невидимая зона: пока игрок внутри — теряет здоровье. (Вики: «Зона опасности»)',
|
||
icon: 'warning', category: 'world',
|
||
prims: [{ type: 'cube', x: 0, y: 2, z: 0, sx: 6, sy: 4, sz: 6, color: '#ff3344', material: 'glass', canCollide: false, name: 'Зона опасности' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Зона урона: внутри — 10 HP/сек.
|
||
let inside = false, t = null;
|
||
game.self.onTouch(() => { if (inside) return; inside = true;
|
||
const tick = () => { if (!inside) return; game.player.damage(10);
|
||
game.ui.set('dz', '☠ Опасно! -10 HP', { x:50, y:78, anchor:'bottom', color:'#ff5555', size:18 });
|
||
t = game.after(1, tick); }; tick(); });
|
||
game.self.onUntouch(() => { inside = false; if (t) game.cancel(t); game.ui.set('dz',''); });` }],
|
||
},
|
||
{
|
||
id: 'spikes-trap',
|
||
name: 'Шипы-ловушка',
|
||
desc: 'Ряд острых шипов: наступишь — мгновенный урон. (Вики: «Полоса препятствий»)',
|
||
icon: 'warning', category: 'world',
|
||
prims: [
|
||
{ type: 'cube', x: 0, y: 0.1, z: 0, sx: 4, sy: 0.2, sz: 1.5, color: '#555', material: 'metal', name: 'Основание шипов' },
|
||
{ type: 'cone', x: -1.2, y: 0.7, z: 0, sx: 0.6, sy: 1.2, sz: 0.6, color: '#cccccc', material: 'metal', canCollide: false, name: 'Шип 1' },
|
||
{ type: 'cone', x: 0, y: 0.7, z: 0, sx: 0.6, sy: 1.2, sz: 0.6, color: '#cccccc', material: 'metal', canCollide: false, name: 'Шип 2' },
|
||
{ type: 'cone', x: 1.2, y: 0.7, z: 0, sx: 0.6, sy: 1.2, sz: 0.6, color: '#cccccc', material: 'metal', canCollide: false, name: 'Шип 3' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Шипы: касание → урон + отброс.
|
||
let cd = false;
|
||
game.self.onTouch(() => { if (cd) return; cd = true;
|
||
game.player.damage(34);
|
||
game.ui.set('sp', '🗡 Ой! -34 HP', { x:50, y:78, anchor:'bottom', color:'#ff5555', size:18 });
|
||
game.after(1.2, () => { game.ui.set('sp',''); cd = false; }); });` }],
|
||
},
|
||
{
|
||
id: 'traffic-light',
|
||
name: 'Светофор',
|
||
desc: 'Светофор переключает красный/жёлтый/зелёный по таймеру. (Вики: «Светофор»)',
|
||
icon: 'light', category: 'world',
|
||
prims: [
|
||
{ type: 'cube', x: 0, y: 3, z: 0, sx: 1.2, sy: 4, sz: 1.2, color: '#2a2a2a', material: 'matte', name: 'Корпус светофора' },
|
||
{ type: 'sphere', x: 0, y: 4.2, z: 0.6, sx: 0.8, sy: 0.8, sz: 0.4, color: '#5a0000', material: 'neon', canCollide: false, name: 'Красный' },
|
||
{ type: 'sphere', x: 0, y: 3.3, z: 0.6, sx: 0.8, sy: 0.8, sz: 0.4, color: '#5a5a00', material: 'neon', canCollide: false, name: 'Жёлтый' },
|
||
{ type: 'sphere', x: 0, y: 2.4, z: 0.6, sx: 0.8, sy: 0.8, sz: 0.4, color: '#005a00', material: 'neon', canCollide: false, name: 'Зелёный' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Светофор: переключение фаз каждые 2 сек.
|
||
const R = game.scene.findOne('Красный'), Y = game.scene.findOne('Жёлтый'), G = game.scene.findOne('Зелёный');
|
||
const phases = [['#ff0000','#5a5a00','#005a00'], ['#5a0000','#ffcc00','#005a00'], ['#5a0000','#5a5a00','#00ff00']];
|
||
let p = 0;
|
||
function show(){ const c = phases[p]; if(R) R.color = c[0]; if(Y) Y.color = c[1]; if(G) G.color = c[2]; }
|
||
show();
|
||
game.every(2, () => { p = (p+1) % phases.length; show(); });` }],
|
||
},
|
||
{
|
||
id: 'harvest-plant',
|
||
name: 'Грядка с урожаем',
|
||
desc: 'Растение растёт, по клику собираешь урожай (+10 монет) и оно вырастает заново. (Вики: «Сбор урожая»)',
|
||
icon: 'plant', category: 'world',
|
||
prims: [
|
||
{ type: 'cube', x: 0, y: 0.2, z: 0, sx: 2, sy: 0.4, sz: 2, color: '#6b4a2e', material: 'matte', name: 'Грядка' },
|
||
{ type: 'sphere', x: 0, y: 0.8, z: 0, sx: 1, sy: 1, sz: 1, color: '#3f9a48', material: 'matte', name: 'Урожай' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Грядка: урожай растёт, по взаимодействию — сбор +10 монет.
|
||
const plant = game.scene.findOne('Урожай');
|
||
let ripe = true;
|
||
game.self.onInteract(() => {
|
||
if (!ripe) { game.ui.set('h','Ещё не созрело...', {x:50,y:80,anchor:'bottom',color:'#bbb',size:16}); return; }
|
||
ripe = false;
|
||
game.broadcast('coins', { add: 10 });
|
||
if (plant) plant.visible = false;
|
||
game.ui.set('h','🌾 Собрано! +10 монет', {x:50,y:80,anchor:'bottom',color:'#ffd23a',size:18});
|
||
game.after(3, () => { if (plant) plant.visible = true; ripe = true; game.ui.set('h',''); });
|
||
}, { text: 'Собрать урожай', key: 'e', distance: 4 });` }],
|
||
},
|
||
{
|
||
id: 'falling-objects',
|
||
name: 'Падающие предметы',
|
||
desc: 'Из этой точки с неба сыплются кубики — лови их или уворачивайся. (Вики: «Поймай падающее»)',
|
||
icon: 'box', category: 'world',
|
||
prims: [{ type: 'sphere', x: 0, y: 8, z: 0, sx: 0.6, sy: 0.6, sz: 0.6, color: '#4d6bff', material: 'neon', canCollide: false, name: 'Тучка-источник' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Каждые 1.5с роняет куб из позиции источника.
|
||
const p = game.self.position;
|
||
game.every(1.5, () => {
|
||
game.scene.spawn('primitive:cube', {
|
||
x: p.x + (Math.random()-0.5)*6, y: p.y, z: p.z + (Math.random()-0.5)*6,
|
||
sx: 0.6, sy: 0.6, sz: 0.6, color: '#ffaa33', anchored: false, canCollide: true, lifetime: 6 });
|
||
});` }],
|
||
},
|
||
|
||
// --- Интерфейс ---
|
||
{
|
||
id: 'score-counter',
|
||
name: 'Счётчик очков',
|
||
desc: 'Счёт очков в HUD. Другие механики шлют game.broadcast("score",{add:N}). (Вики: «Собери монетки»)',
|
||
icon: 'star', category: 'ui',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Счётчик очков. Прибавить: game.broadcast('score', { add: 1 });
|
||
let score = 0;
|
||
function show(){ game.ui.set('score', '⭐ ' + score, { x:8, y:6, anchor:'top', color:'#ffd23a', size:22 }); }
|
||
show();
|
||
game.onMessage('score', (m) => { score += (m && m.add) ? m.add : 1; show(); });` }],
|
||
},
|
||
{
|
||
id: 'hp-bar',
|
||
name: 'Полоска здоровья',
|
||
desc: 'Показывает HP игрока в углу экрана, обновляется при уроне/лечении. (Вики: «Лава-пол»)',
|
||
icon: 'warning', category: 'ui',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// HP-индикатор игрока в HUD.
|
||
function show(){ const hp = Math.max(0, Math.round(game.player.hp));
|
||
game.ui.set('hp', '❤ ' + hp, { x:8, y:12, anchor:'top', color: hp>30?'#36d57a':'#ff4444', size:22 }); }
|
||
show();
|
||
game.every(0.3, show);` }],
|
||
},
|
||
{
|
||
id: 'code-door',
|
||
name: 'Дверь по коду',
|
||
desc: 'Поле ввода: введи правильный код (1234) — дверь открывается. (Вики: «Дверь по коду»)',
|
||
icon: 'keypad', category: 'ui',
|
||
prims: [{ type: 'cube', x: 0, y: 2, z: 0, sx: 0.5, sy: 4, sz: 3, color: '#5a6478', material: 'metal', name: 'Дверь-код' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Дверь по коду 1234. Поле ввода снизу.
|
||
const CODE = '1234';
|
||
const inp = game.gui.create('textbox', { id:'codein', x:50, y:88, w:24, h:8, anchor:'center', placeholder:'Код...', textSize:20 });
|
||
game.ui.set('codehint', 'Введи код двери (1234) и нажми Enter', {x:50,y:80,anchor:'bottom',color:'#fff',size:16});
|
||
let opened = false;
|
||
const p0 = game.self.position;
|
||
game.gui.onSubmit('codein', (text) => {
|
||
if (opened) return;
|
||
if (String(text).trim() === CODE) { opened = true; game.self.move(p0.x, p0.y-4.2, p0.z);
|
||
game.ui.set('codehint', '✓ Открыто!', {x:50,y:80,anchor:'bottom',color:'#36d57a',size:18}); }
|
||
else game.ui.set('codehint', '✗ Неверный код', {x:50,y:80,anchor:'bottom',color:'#ff5555',size:18});
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'name-label',
|
||
name: 'Метка с именем',
|
||
desc: 'Над объектом висит табличка с текстом (имя/HP). (Вики: «Имена над врагами»)',
|
||
icon: 'tag', category: 'ui',
|
||
prims: [{ type: 'cube', x: 0, y: 1, z: 0, sx: 1.5, sy: 2, sz: 1.5, color: '#c83030', material: 'matte', name: 'Объект с меткой' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Метка-табличка над объектом.
|
||
game.self.setLabel('Враг ❤ 100', { color: '#ffffff', bg: '#c83030' });` }],
|
||
},
|
||
{
|
||
id: 'countdown',
|
||
name: 'Обратный отсчёт',
|
||
desc: 'Таймер обратного отсчёта в HUD. По нулю — событие (тут просто сообщение). (Вики: «продержись N секунд»)',
|
||
icon: 'clock', category: 'ui',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Обратный отсчёт 30 секунд.
|
||
let left = 30;
|
||
game.ui.set('cd', '⏳ ' + left, { x:50, y:6, anchor:'top', color:'#fff', size:26 });
|
||
const id = game.every(1, () => {
|
||
left--; game.ui.set('cd', '⏳ ' + left, { x:50, y:6, anchor:'top', color: left<=5?'#ff4444':'#fff', size:26 });
|
||
if (left <= 0) { game.cancel(id); game.ui.set('cd', '⏰ Время вышло!', { x:50, y:42, anchor:'center', color:'#ff4444', size:40 }); }
|
||
});` }],
|
||
},
|
||
|
||
// --- Эффекты ---
|
||
{
|
||
id: 'fire-emitter',
|
||
name: 'Костёр (огонь)',
|
||
desc: 'Источник частиц огня — горит постоянно. (Палитра эффектов)',
|
||
icon: 'sparkles', category: 'fx',
|
||
prims: [{ type: 'cylinder', x: 0, y: 0.2, z: 0, sx: 1.2, sy: 0.4, sz: 1.2, color: '#3a2a1a', material: 'matte', name: 'Костёр' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Постоянный огонь из точки костра.
|
||
const p = game.self.position;
|
||
function fire(){ game.scene.spawnParticles('fire', { x:p.x, y:p.y+0.5, z:p.z }, { duration: 1.5, count: 40 }); }
|
||
fire(); game.every(1.2, fire);` }],
|
||
},
|
||
{
|
||
id: 'magnet-coins',
|
||
name: 'Магнит монет',
|
||
desc: 'Монета сама летит к игроку, когда он подходит близко. (Вики: «Магнит монет»)',
|
||
icon: 'circle', category: 'fx',
|
||
prims: [{ type: 'cylinder', x: 0, y: 1, z: 0, sx: 0.8, sy: 0.2, sz: 0.8, color: '#ffd23a', material: 'metal', name: 'Монета' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Монета летит к игроку, если он ближе 6 м; коснулся — +1 монета.
|
||
let taken = false;
|
||
game.onTick(() => {
|
||
if (taken) return;
|
||
const me = game.self.position, pl = game.player.position;
|
||
const dx = pl.x-me.x, dy = pl.y-me.y, dz = pl.z-me.z;
|
||
const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
|
||
if (d < 1.2) { taken = true; game.broadcast('coins', { add: 1 }); game.self.setVisible(false); return; }
|
||
if (d < 6) { game.self.move(me.x+dx*0.12, me.y+dy*0.12, me.z+dz*0.12); }
|
||
});` }],
|
||
},
|
||
|
||
// --- NPC и бой ---
|
||
{
|
||
id: 'npc-chaser',
|
||
name: 'NPC-преследователь',
|
||
desc: 'Враг бежит за игроком по всему уровню. (Вики: «Преследователь»)',
|
||
icon: 'chase', category: 'npc',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Спавним NPC, который преследует игрока.
|
||
const enemy = game.scene.spawnNpc('robot', { x: 8, z: 8, name: 'Охотник', speed: 4 });
|
||
if (enemy && enemy.follow) enemy.follow('player');` }],
|
||
},
|
||
{
|
||
id: 'npc-trader',
|
||
name: 'Торговец (диалог)',
|
||
desc: 'Фигура торговца: подойди, нажми E — открывается диалог. (Вики: «Торговец»)',
|
||
icon: 'trader', category: 'npc',
|
||
prims: [
|
||
{ type: 'cylinder', x: 0, y: 1, z: 0, sx: 1.2, sy: 2, sz: 1.2, color: '#3a6ea5', material: 'matte', name: 'Торговец' },
|
||
{ type: 'sphere', x: 0, y: 2.3, z: 0, sx: 0.9, sy: 0.9, sz: 0.9, color: '#e8c8a0', material: 'matte', canCollide: false, name: 'Голова торговца' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Торговец с диалогом по E.
|
||
game.self.setLabel('Торговец Боб', { color:'#fff', bg:'#3a6ea5' });
|
||
game.self.onInteract(() => {
|
||
game.modal.dialog('Торговец Боб', [
|
||
'Привет, путник! Заходи за товарами.',
|
||
'У меня лучшие мечи во всём королевстве!',
|
||
'Возвращайся, когда накопишь монет.',
|
||
]);
|
||
}, { text: 'Поговорить', key: 'e', distance: 4 });` }],
|
||
},
|
||
{
|
||
id: 'shooting-target',
|
||
name: 'Мишень для стрельбы',
|
||
desc: 'Кликни по мишени — +10 очков, мишень исчезает и появляется снова. (Вики: «Тир»)',
|
||
icon: 'crosshair', category: 'npc',
|
||
prims: [
|
||
{ type: 'cylinder', x: 0, y: 2, z: 0, sx: 0.3, sy: 2, sz: 2, color: '#ffffff', material: 'matte', name: 'Мишень' },
|
||
{ type: 'cylinder', x: 0.2, y: 2, z: 0, sx: 0.1, sy: 1.2, sz: 1.2, color: '#ff3333', material: 'matte', canCollide: false, name: 'Кольцо мишени' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Мишень: клик → +10 очков, прячется на 1.5с.
|
||
let active = true;
|
||
game.self.onClick(() => {
|
||
if (!active) return; active = false;
|
||
game.broadcast('score', { add: 10 });
|
||
game.self.setVisible(false);
|
||
game.ui.set('hit', '🎯 +10!', { x:50, y:75, anchor:'bottom', color:'#36d57a', size:20 });
|
||
game.after(1.5, () => { game.self.setVisible(true); active = true; game.ui.set('hit',''); });
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'enemy-hp',
|
||
name: 'Враг с HP',
|
||
desc: 'Враг с полоской здоровья над головой. Кликай — урон, при нуле HP погибает. (Вики: «Имена над врагами», «босс»)',
|
||
icon: 'boss', category: 'npc',
|
||
prims: [{ type: 'cube', x: 0, y: 1.5, z: 0, sx: 1.5, sy: 3, sz: 1.5, color: '#7a2030', material: 'matte', name: 'Враг' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Враг с HP: клик → -20 HP, метка обновляется, при 0 — исчезает.
|
||
let hp = 100;
|
||
function lbl(){ game.self.setLabel('Враг ❤ ' + hp, { color:'#fff', bg:'#7a2030' }); }
|
||
lbl();
|
||
game.self.onClick(() => {
|
||
hp -= 20; if (hp <= 0) { game.self.setVisible(false); game.broadcast('score', { add: 50 });
|
||
game.ui.set('kill', '💀 Враг повержен! +50', { x:50, y:75, anchor:'bottom', color:'#ffd23a', size:18 }); return; }
|
||
lbl();
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'enemy-wave',
|
||
name: 'Волна врагов',
|
||
desc: 'Спавнер: выпускает врагов волнами по таймеру. (Вики: «Выживание от волн», tower defense)',
|
||
icon: 'zombie', category: 'npc',
|
||
prims: [{ type: 'cylinder', x: 0, y: 0.15, z: 0, sx: 2, sy: 0.3, sz: 2, color: '#7a2030', material: 'neon', name: 'Портал врагов' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Каждые 5с спавнит 2 врагов из точки портала, они идут к игроку.
|
||
const p = game.self.position;
|
||
function wave(){
|
||
for (let i=0;i<2;i++){ const e = game.scene.spawnNpc('robot', { x:p.x+(Math.random()-0.5)*2, z:p.z+(Math.random()-0.5)*2, name:'Враг', speed:3 });
|
||
if (e && e.follow) e.follow('player'); }
|
||
}
|
||
game.after(2, wave); game.every(5, wave);` }],
|
||
},
|
||
|
||
// --- Экономика ---
|
||
{
|
||
id: 'shop-button',
|
||
name: 'Магазин (кнопка покупки)',
|
||
desc: 'GUI-кнопка магазина: покупка предмета за 50 монет (если хватает). (Вики: «Магазин»)',
|
||
icon: 'cart', category: 'economy',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Кнопка магазина: купить за 50 монет.
|
||
let coins = 100; // локальный баланс кита (для демо)
|
||
game.ui.set('bal', '🪙 ' + coins, { x:92, y:6, anchor:'top', color:'#ffd23a', size:22 });
|
||
game.onMessage('coins', (m) => { coins += (m&&m.add)?m.add:0; game.ui.set('bal','🪙 '+coins,{x:92,y:6,anchor:'top',color:'#ffd23a',size:22}); });
|
||
game.gui.create('button', { id:'buybtn', x:50, y:90, w:26, h:9, anchor:'center', text:'Купить меч — 50 🪙',
|
||
bgGradient:{ stops:['#ffe066','#e0a000'], angle:90 }, textColor:'#3a2a00', textSize:18, fontWeight:800, borderRadius:12 });
|
||
game.gui.onClick('buybtn', () => {
|
||
if (coins >= 50) { game.broadcast('coins', { add: -50 }); game.ui.set('shopmsg','✓ Куплено!',{x:50,y:80,anchor:'bottom',color:'#36d57a',size:18}); }
|
||
else game.ui.set('shopmsg','✗ Мало монет',{x:50,y:80,anchor:'bottom',color:'#ff5555',size:18});
|
||
game.after(2, () => game.ui.set('shopmsg',''));
|
||
});` }],
|
||
},
|
||
{
|
||
id: 'clicker-button',
|
||
name: 'Кликер',
|
||
desc: 'GUI-кнопка: кликай и копи очки. (Вики: «Кликер»)',
|
||
icon: 'click', category: 'economy',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Кликер: кнопка по центру, клик → +1 очко.
|
||
let n = 0;
|
||
function show(){ game.ui.set('clk', '👆 ' + n, { x:50, y:20, anchor:'center', color:'#fff', size:36 }); }
|
||
show();
|
||
game.gui.create('button', { id:'clickbtn', x:50, y:55, w:30, h:14, anchor:'center', text:'КЛИК!',
|
||
bgGradient:{ stops:['#6f8bff','#3a4ed0'], angle:90 }, textColor:'#fff', textSize:32, fontWeight:900, borderRadius:18,
|
||
hover:{ scale:1.05 }, active:{ scale:0.94 } });
|
||
game.gui.onClick('clickbtn', () => { n++; show(); });` }],
|
||
},
|
||
{
|
||
id: 'key-lock',
|
||
name: 'Ключ и замок',
|
||
desc: 'Подбери ключ, затем открой запертую дверь. Без ключа дверь не открывается. (Вики: «Ключ и сундук»)',
|
||
icon: 'key', category: 'economy',
|
||
prims: [
|
||
{ type: 'cube', x: 0, y: 1, z: 0, sx: 0.4, sy: 0.8, sz: 0.4, color: '#ffd23a', material: 'metal', name: 'Ключ' },
|
||
{ type: 'cube', x: 6, y: 2, z: 0, sx: 0.5, sy: 4, sz: 3, color: '#6b4423', material: 'matte', name: 'Запертая дверь' },
|
||
],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Ключ (этот объект) подбирается касанием → открывает «Запертую дверь».
|
||
let hasKey = false;
|
||
game.self.onTouch(() => {
|
||
if (hasKey) return; hasKey = true;
|
||
game.self.setVisible(false);
|
||
game.ui.set('key', '🔑 Ключ найден! Иди к двери (E).', {x:50,y:80,anchor:'bottom',color:'#ffd23a',size:18});
|
||
});
|
||
const door = game.scene.findOne('Запертая дверь');
|
||
if (door && door.onInteract) {
|
||
let opened = false;
|
||
door.onInteract(() => {
|
||
if (!hasKey) { game.ui.set('key','🔒 Заперто. Нужен ключ.',{x:50,y:80,anchor:'bottom',color:'#ff5555',size:18}); return; }
|
||
if (opened) return; opened = true;
|
||
const dp = door.position; door.move(dp.x, dp.y-4.2, dp.z);
|
||
game.ui.set('key','✓ Дверь открыта!',{x:50,y:80,anchor:'bottom',color:'#36d57a',size:18});
|
||
}, { text:'Открыть дверь', key:'e', distance:4 });
|
||
}` }],
|
||
},
|
||
|
||
// --- Из готовых игр (g5) ---
|
||
{
|
||
id: 'spawn-car',
|
||
name: 'Машина (сядь за руль)',
|
||
desc: 'Готовый автомобиль: подойди, держи F — садись за руль, WASD — едь. (Вики: «Такси-симулятор»)',
|
||
icon: 'car', category: 'npc',
|
||
scripts: [{ attachTo: 'global', code:
|
||
`// Спавн машины, на которой можно ездить.
|
||
game.scene.spawn('vehicle:car', { x: 0, y: 0.5, z: 5, model: 'car-sedan', color: '#c83030',
|
||
name: 'Авто', params: { maxSpeed: 18, turnSpeed: 1.7, enginePower: 20 } });
|
||
game.ui.set('carhint', 'Подойди к машине и держи F — за руль!', {x:50,y:90,anchor:'bottom',color:'#fff',size:16});` }],
|
||
},
|
||
{
|
||
id: 'cutscene-dialog',
|
||
name: 'Диалог (кат-сцена)',
|
||
desc: 'Объект, по взаимодействию показывает диалог по строкам. (Вики: «Тайна старого сундука»)',
|
||
icon: 'scroll', category: 'npc',
|
||
prims: [{ type: 'cube', x: 0, y: 0.6, z: 0, sx: 1.4, sy: 1.2, sz: 1, color: '#8a6a3a', material: 'matte', name: 'Рассказчик' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Диалог по строкам через готовый game.modal.dialog.
|
||
const lines = ['Давным-давно здесь стоял замок...', 'Его охранял древний страж.', 'Найди три ключа, чтобы войти!'];
|
||
game.self.onInteract(() => {
|
||
game.modal.dialog('Рассказчик', lines);
|
||
}, { text:'Поговорить', key:'e', distance:4 });` }],
|
||
},
|
||
{
|
||
id: 'guide-arrow',
|
||
name: '3D-стрелка-указатель',
|
||
desc: 'Стрелка-подсказка «иди сюда» ведёт игрока к цели. (Вики: «Туториал — собери монетки»)',
|
||
icon: 'flag', category: 'ui',
|
||
prims: [{ type: 'cylinder', x: 0, y: 1, z: 0, sx: 1, sy: 2, sz: 1, color: '#ffd23a', material: 'neon', name: 'Цель-указатель' }],
|
||
scripts: [{ attachTo: 'on-target', code:
|
||
`// Стрелка от игрока к этому объекту-цели.
|
||
const arrow = game.fx.pointer({ from: 'player', to: game.self, preset: 'guide' });
|
||
game.self.onTouch(() => { if (arrow && arrow.remove) arrow.remove();
|
||
game.ui.set('arr','✓ Дошёл!', {x:50,y:80,anchor:'bottom',color:'#36d57a',size:18}); });` }],
|
||
},
|
||
];
|
||
|
||
/** Найти кит по id. */
|
||
export function getKit(id) {
|
||
return GAMEPLAY_KITS.find(k => k.id === id) || null;
|
||
}
|