feat(studio): +25 готовых механик из Вики (вся партия 3 — все остальные)

Добавлены все оставшиеся механики из 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>
This commit is contained in:
min 2026-06-05 09:55:32 +03:00
parent f270854795
commit ed7310a532

View File

@ -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. */