fix(studio): self.setLabel, дверь по коду (красивая+радиус), счётчик механик, ссылка на скрипт в консоли
1) Дверь по коду: красивая составная дверь (полотно+рамка+кодовая панель),
поле ввода появляется ТОЛЬКО когда игрок в радиусе 6м (onTick по дистанции).
2) game.self.setLabel/clearLabel добавлены (кит «Метка с именем» падал
'setLabel is not a function').
3) Плитка «Готовые механики» в тулбоксе считает киты динамически
(GAMEPLAY_KITS.length), а не хардкод «12».
4) Консоль: ошибки/логи скриптов привязаны к источнику — справа строки
кликабельная ссылка «📄 имя скрипта», открывает скрипт в редакторе
(_log прокидывает scriptId/scriptName).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
045f892aaa
commit
3bf1e77230
@ -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). Активен когда выбран таб со скриптом. */}
|
||||
|
||||
@ -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,
|
||||
}}>
|
||||
<span style={{
|
||||
color: '#94a3b8', marginRight: 10, fontWeight: 700,
|
||||
}}>
|
||||
{new Date(l.ts || Date.now()).toLocaleTimeString().slice(0, 8)}
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{
|
||||
color: '#94a3b8', marginRight: 10, fontWeight: 700,
|
||||
}}>
|
||||
{new Date(l.ts || Date.now()).toLocaleTimeString().slice(0, 8)}
|
||||
</span>
|
||||
{l.level === 'error' && <span><Icon name="error" size={14} /></span>}
|
||||
{l.level === 'warn' && <span><Icon name="warning" size={14} /></span>}
|
||||
{l.level === 'info' && <span style={{ opacity: 0.7 }}>● </span>}
|
||||
{l.text}
|
||||
</span>
|
||||
{l.level === 'error' && <span><Icon name="error" size={14} /></span>}
|
||||
{l.level === 'warn' && <span><Icon name="warning" size={14} /></span>}
|
||||
{l.level === 'info' && <span style={{ opacity: 0.7 }}>● </span>}
|
||||
{l.text}
|
||||
{/* Ссылка на скрипт-источник (клик открывает его). */}
|
||||
{l.scriptId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenScript?.(l.scriptId)}
|
||||
title={'Открыть скрипт: ' + (l.scriptName || l.scriptId)}
|
||||
style={{
|
||||
flex: '0 0 auto', maxWidth: 160,
|
||||
background: 'rgba(79,116,255,0.16)',
|
||||
border: '1px solid rgba(79,116,255,0.3)',
|
||||
color: '#8aa0ff', borderRadius: 6,
|
||||
padding: '1px 8px', fontSize: 11, fontWeight: 700,
|
||||
cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
📄 {l.scriptName || l.scriptId}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@ -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: 'Звуки и музыка' },
|
||||
];
|
||||
|
||||
@ -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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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});
|
||||
}
|
||||
});` }],
|
||||
},
|
||||
{
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user