From ed7310a532baa2cc4eab9c3c9ebc7428992b6111 Mon Sep 17 00:00:00 2001 From: min Date: Fri, 5 Jun 2026 09:55:32 +0300 Subject: [PATCH] =?UTF-8?q?feat(studio):=20+25=20=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D1=85=20=D0=BC=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D0=92=D0=B8=D0=BA=D0=B8=20(=D0=B2=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=B0=D1=80=D1=82=D0=B8=D1=8F=203=20=E2=80=94?= =?UTF-8?q?=20=D0=B2=D1=81=D0=B5=20=D0=BE=D1=81=D1=82=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены все оставшиеся механики из 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 --- src/editor/engine/GameplayKits.js | 384 ++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) diff --git a/src/editor/engine/GameplayKits.js b/src/editor/engine/GameplayKits.js index fc4cd30..4d5086d 100644 --- a/src/editor/engine/GameplayKits.js +++ b/src/editor/engine/GameplayKits.js @@ -20,6 +20,8 @@ export const KIT_CATEGORIES = [ { id: 'world', label: 'Мир' }, { id: 'ui', label: 'Интерфейс' }, { id: 'fx', label: 'Эффекты' }, + { id: 'npc', label: 'NPC и бой' }, + { id: 'economy', label: 'Экономика' }, ]; export const GAMEPLAY_KITS = [ @@ -424,6 +426,388 @@ game.self.onTouch(() => { 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. */