studio/src/editor/ScriptConsole.jsx
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
Open-source веб-студия для создания игр Рублокса, двойная лицензия
AGPL-3.0 + Коммерческая.

Главное:
- Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16
- Самодостаточный движок ~28к строк (66 файлов): BlockManager,
  TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController,
  ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов
- Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco)
- Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn)
- Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt)
- 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.)
- Конфигурируемый бэкенд через VITE_API_BASE — работает со staging
  (dev-api.rublox.pro) без настройки
- Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка
- Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING,
  SECURITY, CHANGELOG
- ESLint + Prettier + EditorConfig
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Перед публикацией:
- Все импорты из minecraftia заменены на локальные
- Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env
- Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо)
- AdminKubikonModeration не публикуется (модерация — в team.rublox.pro)
- 93 МБ ассетов public/kubikon-assets вынесены в .gitignore
  (раздаются через release artifact)
2026-05-27 23:41:10 +03:00

282 lines
12 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 }) => {
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,
}}>
<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}
</div>
))
)}
</div>
</div>
);
};
export default ScriptConsole;