diff --git a/src/editor/KubikonEditor.jsx b/src/editor/KubikonEditor.jsx index 0ef3930..bbf3c6a 100644 --- a/src/editor/KubikonEditor.jsx +++ b/src/editor/KubikonEditor.jsx @@ -3124,6 +3124,11 @@ const KubikonEditor = () => { logs={scriptLogs} onClear={() => setScriptLogs([])} onClose={() => setConsoleOpen(false)} + onOpenScript={(scriptId) => { + // Открыть скрипт-источник ошибки в редакторе. + try { sceneRef.current?.selection?.selectScript?.(scriptId); } catch (e) {} + openScriptTab(scriptId); + }} /> {/* Monaco-редактор скрипта (этап 2.2). Активен когда выбран таб со скриптом. */} diff --git a/src/editor/ScriptConsole.jsx b/src/editor/ScriptConsole.jsx index bbdb9a0..4f3fcd0 100644 --- a/src/editor/ScriptConsole.jsx +++ b/src/editor/ScriptConsole.jsx @@ -25,7 +25,7 @@ const LEVEL_BG = { warn: 'rgba(245, 158, 11, 0.12)', }; -const ScriptConsole = ({ logs = [], onClear, onClose, visible }) => { +const ScriptConsole = ({ logs = [], onClear, onClose, visible, onOpenScript }) => { const listRef = useRef(null); const [copyState, setCopyState] = useState('idle'); @@ -260,16 +260,38 @@ const ScriptConsole = ({ logs = [], onClear, onClose, visible }) => { ? `3px solid ${LEVEL_COLORS[l.level]}` : '3px solid transparent', paddingLeft: 8, + display: 'flex', alignItems: 'flex-start', gap: 8, }}> - - {new Date(l.ts || Date.now()).toLocaleTimeString().slice(0, 8)} + + + {new Date(l.ts || Date.now()).toLocaleTimeString().slice(0, 8)} + + {l.level === 'error' && } + {l.level === 'warn' && } + {l.level === 'info' && } + {l.text} - {l.level === 'error' && } - {l.level === 'warn' && } - {l.level === 'info' && } - {l.text} + {/* Ссылка на скрипт-источник (клик открывает его). */} + {l.scriptId && ( + + )} )) )} diff --git a/src/editor/ToolboxModal.jsx b/src/editor/ToolboxModal.jsx index c5b78de..61e58d4 100644 --- a/src/editor/ToolboxModal.jsx +++ b/src/editor/ToolboxModal.jsx @@ -470,7 +470,7 @@ const ToolboxModal = ({ { id: '3d', label: '3D-объекты', icon: 'cube', desc: '700+ моделей: природа, дома, мебель, NPC' }, { id: 'fx', label: 'Эффекты', icon: 'sparkles', desc: 'Частицы, лучи, маркеры' }, { id: '2d', label: '2D-картинки', icon: 'image', desc: 'Иконки и текстуры для интерфейса' }, - { id: 'gameplay', label: 'Готовые механики', icon: 'zap', desc: '12 механик: вставил — работает' }, + { id: 'gameplay', label: 'Готовые механики', icon: 'zap', desc: `${GAMEPLAY_KITS.length} механик: вставил — работает` }, { id: 'plugins', label: 'Плагины', icon: 'puzzle', desc: 'Расширения студии' }, { id: 'audio', label: 'Аудио', icon: 'sound', desc: 'Звуки и музыка' }, ]; diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 1280a55..15a896b 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -68,6 +68,7 @@ export class GameRuntime { start(scripts) { this.stop(); this._isRunning = true; + this.scripts = scripts || []; // для привязки логов/ошибок к скрипту // eslint-disable-next-line no-console console.log('[GameRuntime] start called with scripts:', scripts); // Phase 6.5: lazy-init физ-мира. Wasm-инициализация асинхронная (~50мс), @@ -1548,7 +1549,13 @@ export class GameRuntime { /** Команда от Worker'а пришла — применяем на сцене. */ _handleCommand(scriptId, cmd, payload) { if (cmd === 'log') { - this._log(payload?.level || 'info', payload?.text || ''); + // Привязываем запись к скрипту-источнику (для ссылки в консоли). + let scriptName = null; + try { + const meta = (this.scripts || []).find(s => s.id === scriptId); + scriptName = meta?.name || scriptId; + } catch (e) { scriptName = scriptId; } + this._log(payload?.level || 'info', payload?.text || '', scriptId, scriptName); return; } if (cmd === 'player.teleport') { @@ -4213,9 +4220,9 @@ export class GameRuntime { } } - _log(level, text) { + _log(level, text, scriptId = null, scriptName = null) { if (this._onLog) { - try { this._onLog({ level, text, ts: Date.now() }); } catch (e) { /* ignore */ } + try { this._onLog({ level, text, ts: Date.now(), scriptId, scriptName }); } catch (e) { /* ignore */ } } } diff --git a/src/editor/engine/GameplayKits.js b/src/editor/engine/GameplayKits.js index 05b8fc9..93518b9 100644 --- a/src/editor/engine/GameplayKits.js +++ b/src/editor/engine/GameplayKits.js @@ -578,21 +578,51 @@ game.hud.setHpVisible(false);` }], { 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: 'Дверь-код' }], + desc: 'Красивая дверь с кодовой панелью: подойди — появится поле ввода, введи код (1234) — откроется. (Вики: «Дверь по коду»)', + icon: 'keypad', category: 'world', + // Красивая дверь (полотно + рамка) + кодовая панель на стене. + prims: [ + { type: 'cube', x: 0, y: 2, z: 0, sx: 0.25, sy: 3.8, sz: 2.6, color: '#5a6478', material: 'metal', name: 'Полотно двери-код' }, + { type: 'cube', x: 0.16, y: 2.6, z: 0, sx: 0.06, sy: 0.9, sz: 1.8, color: '#79879a', material: 'metal', canCollide: false, name: 'Панель двери' }, + { type: 'cube', x: 0.16, y: 1.3, z: 0, sx: 0.06, sy: 0.9, sz: 1.8, color: '#79879a', material: 'metal', canCollide: false, name: 'Панель двери низ' }, + { type: 'cube', x: 0.3, y: 2, z: 0.95, sx: 0.15, sy: 0.6, sz: 0.5, color: '#ffd23a', material: 'neon', canCollide: false, name: 'Кодовая панель' }, + { type: 'cube', x: 0, y: 2, z: -1.55, sx: 0.4, sy: 4.2, sz: 0.4, color: '#3a4250', material: 'metal', name: 'Косяк левый' }, + { type: 'cube', x: 0, y: 2, z: 1.55, sx: 0.4, sy: 4.2, sz: 0.4, color: '#3a4250', material: 'metal', name: 'Косяк правый' }, + { type: 'cube', x: 0, y: 4.1, z: 0, sx: 0.4, sy: 0.4, sz: 3.5, color: '#3a4250', material: 'metal', name: 'Перемычка' }, + ], scripts: [{ attachTo: 'on-target', code: -`// Дверь по коду 1234. Поле ввода снизу. +`// Дверь по коду 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; +const RADIUS = 6; +let opened = false, near = false, inp = null; +game.ui.set('codehint', '', {}); +game.onTick(() => { + if (opened) return; + const pl = game.player.position; + const dx = pl.x - p0.x, dz = pl.z - p0.z; + const d = Math.sqrt(dx*dx + dz*dz); + if (d < RADIUS && !near) { + near = true; + inp = game.gui.create('textbox', { id:'codein', x:50, y:86, w:24, h:8, anchor:'center', placeholder:'Код...', textSize:20 }); + game.ui.set('codehint', '🔢 Введи код двери и нажми Enter', {x:50,y:78,anchor:'bottom',color:'#fff',size:16}); + } else if (d >= RADIUS && near) { + near = false; + if (inp) game.gui.remove('codein'); + game.ui.set('codehint', '', {}); + } +}); 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}); + if (String(text).trim() === CODE) { + opened = true; + game.self.move(p0.x, p0.y - 4.2, p0.z); // дверь уезжает вниз (открыта) + game.gui.remove('codein'); + game.ui.set('codehint', '✓ Открыто!', {x:50,y:78,anchor:'bottom',color:'#36d57a',size:18}); + game.after(2, () => game.ui.set('codehint', '', {})); + } else { + game.ui.set('codehint', '✗ Неверный код', {x:50,y:78,anchor:'bottom',color:'#ff5555',size:18}); + } });` }], }, { diff --git a/src/editor/engine/ScriptSandboxWorker.js b/src/editor/engine/ScriptSandboxWorker.js index 433dc78..1b8b98f 100644 --- a/src/editor/engine/ScriptSandboxWorker.js +++ b/src/editor/engine/ScriptSandboxWorker.js @@ -779,6 +779,20 @@ function _buildSelfApi() { const id = _target.id ?? _target.ref; _send('scene.setColor', { kind: k, id, ref: (k && id != null) ? (k + ':' + id) : undefined, color: hex }); }, + /** Повесить текст-метку над объектом-носителем (имя/HP). */ + setLabel(text, opts) { + const k = _target.kind; + const id = _target.id ?? _target.ref; + const ref = (k && id != null) ? (k + ':' + id) : undefined; + _send('scene.setLabel', { ref, text: String(text == null ? '' : text), opts: opts || {} }); + }, + /** Убрать метку с объекта-носителя. */ + clearLabel() { + const k = _target.kind; + const id = _target.id ?? _target.ref; + const ref = (k && id != null) ? (k + ':' + id) : undefined; + _send('scene.clearLabel', { ref }); + }, delete() { _send('self.delete', { target: _target }); },