studio/src/editor/ScriptConsole.jsx
min 3bf1e77230 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>
2026-06-05 19:27:09 +03:00

304 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useRef, useState } from 'react';
import Icon from './Icon';
/**
* ScriptConsole — нижняя выдвижная панель консоли скриптов (синий wow-стиль).
*
* Показывает game.log() и ошибки выполнения. Auto-scroll вниз при новых
* сообщениях. Кнопка «Очистить» сбрасывает буфер.
*
* Props:
* logs — массив { level, text, ts }
* onClear — () => void
* onClose — () => void
* visible — bool (если false — компонент не рендерится)
*/
const LEVEL_COLORS = {
info: '#6d8aff',
error: '#ff6b6b',
warn: '#f5b342',
};
const LEVEL_BG = {
info: 'rgba(79, 116, 255, 0.12)',
error: 'rgba(239, 68, 68, 0.14)',
warn: 'rgba(245, 158, 11, 0.12)',
};
const ScriptConsole = ({ logs = [], onClear, onClose, visible, onOpenScript }) => {
const listRef = useRef(null);
const [copyState, setCopyState] = useState('idle');
useEffect(() => {
if (!visible) return;
const el = listRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [logs, visible]);
const handleCopy = async () => {
const text = logs.map(l => {
const ts = new Date(l.ts || Date.now()).toLocaleTimeString().slice(0, 8);
const prefix = l.level === 'error' ? '[ERROR]' : l.level === 'warn' ? '[WARN]' : '[INFO]';
return `${ts} ${prefix} ${l.text || ''}`;
}).join('\n');
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
setCopyState('copied');
setTimeout(() => setCopyState('idle'), 1500);
} catch (e) {
setCopyState('error');
setTimeout(() => setCopyState('idle'), 1500);
}
};
if (!visible) return null;
const errorsCount = logs.filter(l => l.level === 'error').length;
const warnsCount = logs.filter(l => l.level === 'warn').length;
return (
<div style={{
position: 'absolute',
left: 0, right: 0, bottom: 0,
height: 240,
background: '#1b1b1b',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
borderTop: '1px solid #3a3a3a',
color: '#e8e8ea',
display: 'flex',
flexDirection: 'column',
zIndex: 50,
fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
fontSize: 12.5,
boxShadow: '0 -12px 32px rgba(0, 0, 0, 0.5)',
}}>
{/* Header */}
<div style={{
padding: '10px 14px',
borderBottom: '1px solid #3a3a3a',
display: 'flex',
alignItems: 'center',
gap: 10,
background: 'linear-gradient(180deg, #2e2e2e 0%, #252525 100%)',
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
fontSize: 13,
fontWeight: 700,
color: '#e8e8ea',
}}>
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
}}>
<div style={{
width: 28, height: 28, borderRadius: 8,
background: 'linear-gradient(135deg, #4f74ff 0%, #3a57d8 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14,
boxShadow: '0 4px 10px rgba(79, 116, 255, 0.45)',
}}><Icon name="duplicate" size={14} /></div>
<span style={{ fontWeight: 800, letterSpacing: -0.2, color: '#e8e8ea' }}>
Консоль скриптов
</span>
<span style={{
background: '#1b1b1b',
padding: '2px 8px',
borderRadius: 999,
fontSize: 11,
fontWeight: 800,
color: '#9a9a9e',
}}>{logs.length}</span>
{errorsCount > 0 && (
<span style={{
background: 'rgba(239, 68, 68, 0.16)',
color: '#ff6b6b',
padding: '2px 8px',
borderRadius: 999,
fontSize: 11, fontWeight: 800,
border: '1px solid rgba(239, 68, 68, 0.35)',
}}><Icon emoji="❌" size={11} /> {errorsCount}</span>
)}
{warnsCount > 0 && (
<span style={{
background: 'rgba(245, 158, 11, 0.16)',
color: '#f5b342',
padding: '2px 8px',
borderRadius: 999,
fontSize: 11, fontWeight: 800,
border: '1px solid rgba(245, 158, 11, 0.35)',
}}><Icon emoji="⚠" size={11} /> {warnsCount}</span>
)}
</div>
<div style={{ flex: 1 }} />
<button
onClick={handleCopy}
disabled={logs.length === 0}
style={{
background: copyState === 'copied'
? 'linear-gradient(135deg, #22d97a 0%, #0f9d56 100%)'
: '#2e2e2e',
border: '1px solid ' + (copyState === 'copied' ? 'transparent' : '#3a3a3a'),
color: copyState === 'copied' ? '#fff' : (logs.length === 0 ? '#7a7a7e' : '#e8e8ea'),
borderRadius: 8, padding: '5px 12px',
cursor: logs.length === 0 ? 'not-allowed' : 'pointer',
fontSize: 11, fontWeight: 700,
opacity: logs.length === 0 ? 0.55 : 1,
transition: 'all 200ms ease',
fontFamily: 'inherit',
boxShadow: copyState === 'copied' ? '0 4px 12px rgba(34, 217, 122, 0.40)' : 'none',
}}
title="Скопировать все логи в буфер"
>
{copyState === 'copied'
? <><Icon emoji="✓" size={12} /> Скопировано</>
: copyState === 'error'
? <><Icon emoji="✕" size={12} /> Ошибка</>
: <><Icon emoji="📋" size={12} /> Копировать</>}
</button>
<button
onClick={onClear}
style={{
background: '#2e2e2e',
border: '1px solid #3a3a3a',
color: '#e8e8ea',
borderRadius: 8, padding: '5px 12px',
cursor: 'pointer', fontSize: 11, fontWeight: 700,
transition: 'all 150ms ease',
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.16)';
e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.4)';
e.currentTarget.style.color = '#ff6b6b';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#2e2e2e';
e.currentTarget.style.borderColor = '#3a3a3a';
e.currentTarget.style.color = '#e8e8ea';
}}
title="Очистить консоль"
>
<Icon name="trash" size={12} /> Очистить
</button>
<button
onClick={onClose}
style={{
width: 28, height: 28,
background: '#2e2e2e',
border: '1px solid #3a3a3a',
color: '#9a9a9e',
borderRadius: 8,
cursor: 'pointer', fontSize: 16, fontWeight: 700, lineHeight: 1,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 150ms ease',
fontFamily: 'inherit',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#383838';
e.currentTarget.style.color = '#e8e8ea';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#2e2e2e';
e.currentTarget.style.color = '#9a9a9e';
}}
title="Закрыть"
><Icon name="close" size={13} /> </button>
</div>
{/* Лог-строки */}
<div
ref={listRef}
style={{
flex: 1,
overflowY: 'auto',
padding: '10px 14px',
lineHeight: 1.55,
userSelect: 'text',
WebkitUserSelect: 'text',
cursor: 'text',
}}
>
{logs.length === 0 ? (
<div style={{
color: '#9a9a9e',
fontStyle: 'italic',
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
textAlign: 'center', padding: 20,
}}>
Здесь появятся логи <code style={{
background: 'rgba(79, 116, 255, 0.18)',
color: '#6d8aff',
padding: '1px 6px',
borderRadius: 4,
fontFamily: 'inherit',
fontSize: 12,
fontWeight: 700,
}}>game.log()</code> и ошибки скриптов.
</div>
) : (
logs.map((l, i) => (
<div key={i} style={{
color: LEVEL_COLORS[l.level] || '#e8e8ea',
whiteSpace: 'pre-wrap',
padding: '3px 8px',
margin: '1px -8px',
borderRadius: 6,
background: LEVEL_BG[l.level] || 'transparent',
borderLeft: l.level
? `3px solid ${LEVEL_COLORS[l.level]}`
: '3px solid transparent',
paddingLeft: 8,
display: 'flex', alignItems: 'flex-start', gap: 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.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>
))
)}
</div>
</div>
);
};
export default ScriptConsole;