diff --git a/src/community/docsData.jsx b/src/community/docsData.jsx
index ae4fcc7..e237717 100644
--- a/src/community/docsData.jsx
+++ b/src/community/docsData.jsx
@@ -134,7 +134,7 @@ const AI_CONTEXT = `Ты — помощник по написанию скрип
=== СЦЕНА game.scene ===
spawn(type, opts) -> объект. type: 'cube'|'sphere'|'cylinder'|'cone'|'pyramid'|'torus'|'wedge'|'plane' (примитивы), 'model:ID', 'block:ID', 'vehicle:car', 'light:point', 'billboard', 'trigger'.
- opts: {x,y,z, sx,sy,sz, rotationX,rotationY,rotationZ, color:'#hex', material:'matte'|'neon'|'metal'|'glass'|'studs', name, anchored:true(висит)/false(падает), canCollide, visible, mass, lifetime(сек до авто-удаления)}
+ opts: {x,y,z, sx,sy,sz, rotationX,rotationY,rotationZ, color:'#hex', material:'matte'|'neon'|'metal'|'glass'|'studs'|'chrome'|'water'|'iridescent', name, anchored:true(висит)/false(падает), canCollide, visible, mass, lifetime(сек до авто-удаления)}
delete(ref); move(ref,x,y,z); setRotation(ref,rx,ry,rz); setColor(ref,'#hex'); setMaterial(ref,name); setVisible(ref,bool); setCollide(ref,bool); setOpacity(ref,0..1); setScale(ref,sx,sy,sz)
setLabel(ref,text,opts); clearLabel(ref); setData(ref,key,val); getData(ref,key)
find(name)->[...]; findOne(name)->объект|null; all('primitive'|'model'|'block')
@@ -186,6 +186,10 @@ shake(amp, sec); setFov(deg); focusOn(ref,{distance,height}); cutscene([{x,y,z}.
=== ОКРУЖЕНИЕ game.environment ===
setSkyColor('#hex'); setFog({enabled,color,density}); setTimeOfDay(0..24)
+=== ГРАФИКА/ЭФФЕКТЫ game.graphics (по умолч. выкл) ===
+setPreset('off'|'low'|'medium'|'high'|'ultra'|'cinematic'|'vivid'|'night'|'retro'|'soft')
+setBloom(bool,{intensity:0..1,threshold:0..1}); setVignette(0..1.5); setColorGrading({contrast,saturation,exposure}); setShadows('off'|'hard'|'soft'|'medium'|'high'); setSSAO(bool); setDepthOfField(bool); setAntialiasing(bool); off()
+
=== ИНВЕНТАРЬ / ПРЕДМЕТЫ ===
game.items.define([{id,name,emoji,rarity:'common'|'rare'|'epic'|'legendary',maxStack,onUseEffect:'heal:50'}])
game.inventory.give(id,count); .take(id,count); .has(id); .open(); .list()
@@ -208,7 +212,8 @@ game.log(...) — в консоль; game.random(min,max,целое?); game.clam
- Повороты в РАДИАНАХ (Math.PI/2 = 90°).
- Счётчики/общую логику держи в ОДНОМ глобальном скрипте; объекты шлют game.broadcast (скрипты объектов не видят переменные друг друга).
- spawn примитива: тип без префикса ('cube'), модели — с 'model:', машина — 'vehicle:car'.
-- material только: matte, neon, metal, glass, studs.
+- material: matte, neon, metal, glass, studs, chrome (зеркало), water (вода), iridescent (переливы).
+- эффекты включаются через game.graphics.setPreset(...) или в настройках игры; по умолчанию выкл.
- Для сбора предмета: game.self.onTouch(()=>{ game.broadcast('coin'); game.self.delete(); }).
- Не используй require() и DOM/window — только game.* и обычный JS.
@@ -3302,6 +3307,152 @@ game.save.get('progress', (data) => {
},
],
},
+
+ // ════════════════════════════════════════════════════
+ // РАЗДЕЛ — ГРАФИКА И ЭФФЕКТЫ (шейдеры)
+ // ════════════════════════════════════════════════════
+ {
+ id: 'graphics',
+ icon: 'sparkles',
+ title: 'Графика и эффекты',
+ summary: 'Шейдеры-эффекты: свечение, цветокоррекция, тени, красивые материалы (хром, вода, переливы). Через настройки и из скриптов.',
+ sections: [
+ {
+ id: 'gfx-what',
+ title: 'GR1. Что такое эффекты (шейдеры)',
+ body: (
+ <>
+
+ Эффекты (шейдеры) делают картинку игры красивее —
+ как «шейдер-паки» в Майнкрафте. Это:
+
+
+ Свечение (Bloom) — яркие объекты (неон, лампы,
+ солнце) светятся и переливаются;
+ Цветокоррекция — насыщенность и контраст, как
+ в кино;
+ Виньетка — мягкое затемнение по краям экрана;
+ Тени — мягкие или резкие, контактные тени в углах;
+ Глубина резкости — размытие дальнего плана;
+ Сглаживание — убирает «лесенки» на краях.
+
+
+ По умолчанию эффекты выключены — игра выглядит как
+ раньше. Включи их в настройках или из скрипта, когда захочешь
+ «прокачать» картинку. На слабых компах/телефонах тяжёлые
+ эффекты сами упрощаются, чтобы игра не тормозила.
+
+ >
+ ),
+ },
+ {
+ id: 'gfx-settings',
+ title: 'GR2. Включить эффекты в настройках',
+ body: (
+ <>
+
+ Открой Настройки игры (шестерёнка
+ вверху).
+
+
+ Найди раздел «Графика и эффекты» и выбери пресет :
+
+
+ Выключено — без эффектов (по умолчанию);
+ Низкое / Среднее / Высокое / Ультра — всё больше
+ красоты (и нагрузки);
+ 🎬 Кино — контраст + виньетка + глубина резкости;
+ 🌈 Сочное — яркие насыщенные цвета;
+ 🌙 Ночь — тёмная атмосфера, сильное свечение;
+ 📺 Ретро — старый ламповый вид;
+ ☁️ Мягкое — нежная пастельная картинка.
+
+
+ Или настрой вручную галочками (свечение, сглаживание,
+ виньетка, контактные тени) и ползунками (насыщенность,
+ контраст). Нажми Сохранить — эффект
+ появится сразу.
+
+
+ Поставь несколько неоновых кубов, включи пресет «Ночь» — они
+ будут красиво светиться в темноте.
+
+ >
+ ),
+ },
+ {
+ id: 'gfx-materials',
+ title: 'GR3. Красивые материалы',
+ body: (
+ <>
+
+ У каждого примитива есть материал (в свойствах объекта
+ или при создании из скрипта). Кроме обычных есть «шейдерные»:
+
+
+ neon — светится (works с Bloom особенно красиво);
+ metal — металлический блик;
+ chrome — зеркальный хром;
+ glass — прозрачное стекло;
+ water — полупрозрачная вода с бликами;
+ iridescent — переливы (бензиновая плёнка, кристалл);
+ studs — лего-пупырышки;
+ matte — матовый (без блеска, по умолчанию).
+
+
+ {`// хромированная сфера
+game.scene.spawn('sphere', { x: 0, y: 2, z: 0, color: '#dfe6f0', material: 'chrome' });
+// светящийся неон-куб
+game.scene.spawn('cube', { x: 3, y: 2, z: 0, color: '#00ffd0', material: 'neon' });
+// переливающийся кристалл
+game.scene.spawn('cone', { x: -3, y: 2, z: 0, color: '#a06bff', material: 'iridescent' });
+
+// поменять материал существующего объекта:
+const obj = game.scene.findOne('Вода');
+obj.material = 'water';`}
+ >
+ ),
+ },
+ {
+ id: 'gfx-api',
+ title: 'GR4. Эффекты из скриптов (game.graphics)',
+ body: (
+ <>
+
+ Эффекты можно включать и менять на ходу из скрипта —
+ например затемнить мир ночью или включить «кино» в катсцене.
+
+
+ {`// применить готовый пресет
+game.graphics.setPreset('cinematic');
+
+// точечная настройка
+game.graphics.setBloom(true, { intensity: 0.7 }); // свечение
+game.graphics.setVignette(0.5); // затемнение краёв
+game.graphics.setColorGrading({ saturation: 1.4, contrast: 1.1 });
+game.graphics.setShadows('soft'); // off|hard|soft|medium|high
+game.graphics.setSSAO(true); // контактные тени
+game.graphics.setDepthOfField(true); // размытие дальнего плана
+
+// выключить всё
+game.graphics.off();`}
+ Пример — плавно «сделать ночь» при входе в пещеру:
+ {`const cave = game.scene.findOne('ВходВПещеру');
+cave.onTouch(() => {
+ game.graphics.setPreset('night');
+ game.environment.setTimeOfDay(0);
+ game.ui.showText('Темнеет...', 2);
+});`}
+
+ Эффекты применяются ко всему экрану. Если игра должна
+ работать на телефонах — не включай разом DoF + SSAO + ультра-
+ тени: движок их урежет, но лучше выбрать пресет полегче.
+
+ >
+ ),
+ },
+ ],
+ },
];
diff --git a/src/editor/GameDecorModal.jsx b/src/editor/GameDecorModal.jsx
new file mode 100644
index 0000000..a353810
--- /dev/null
+++ b/src/editor/GameDecorModal.jsx
@@ -0,0 +1,440 @@
+import React, { useState, useRef, useEffect } from 'react';
+import cl from './GameSettingsModal.module.css';
+import Icon from './Icon';
+
+/**
+ * GameDecorModal — «оформление» игры: Графика и эффекты, Стартовый экран
+ * (Ken Burns), Экран загрузки. Вынесено из настроек во вкладку «Игра»
+ * (TopRibbon) тремя отдельными кнопками. Открывается с нужной секцией через
+ * проп `section` ('graphics' | 'startscreen' | 'loadingscreen'), вверху —
+ * табы для переключения между ними.
+ *
+ * Props:
+ * open — открыто ли
+ * section — какую секцию показать первой
+ * initial — { loading_screen:{...}, graphics:{...} } (из scene/проекта)
+ * onClose — закрыть без сохранения
+ * onSave(data) — data = { loading_screen, graphics } (как в GameSettingsModal)
+ */
+const MAX_IMG_BYTES = 500 * 1024;
+
+const TABS = [
+ { id: 'graphics', title: 'Графика', icon: 'sparkles' },
+ { id: 'startscreen', title: 'Стартовый экран', icon: 'loader' },
+ { id: 'loadingscreen', title: 'Экран загрузки', icon: 'loader' },
+];
+
+const GameDecorModal = ({ open, section = 'graphics', initial, onClose, onSave }) => {
+ const [tab, setTab] = useState(section);
+
+ // Экран загрузки
+ const [loadingLogo, setLoadingLogo] = useState('');
+ const [loadingAccent, setLoadingAccent] = useState('#ffc020');
+ const [loadingSpinner, setLoadingSpinner] = useState(true);
+ const [loadingSkip, setLoadingSkip] = useState(false);
+ // Стартовый Ken-Burns экран
+ const [lsEnabled, setLsEnabled] = useState(true);
+ const [lsBackground, setLsBackground] = useState('');
+ const [lsCover, setLsCover] = useState('');
+ const [lsStyle, setLsStyle] = useState('ken-burns');
+ const [lsPlaceName, setLsPlaceName] = useState('');
+ const [lsStudioName, setLsStudioName] = useState('');
+ const [lsVerified, setLsVerified] = useState(false);
+ const [lsDuration, setLsDuration] = useState(2.5);
+ const [lsProgressBar, setLsProgressBar] = useState(true);
+ // Графика
+ const [gfxPreset, setGfxPreset] = useState('off');
+ const [gfxBloom, setGfxBloom] = useState(false);
+ const [gfxFxaa, setGfxFxaa] = useState(false);
+ const [gfxVignette, setGfxVignette] = useState(false);
+ const [gfxSsao, setGfxSsao] = useState(false);
+ const [gfxSaturation, setGfxSaturation] = useState(1.0);
+ const [gfxContrast, setGfxContrast] = useState(1.0);
+ const [error, setError] = useState('');
+
+ const logoInputRef = useRef(null);
+ const lsBgInputRef = useRef(null);
+ const lsCoverInputRef = useRef(null);
+
+ useEffect(() => {
+ if (!open) return;
+ setTab(section || 'graphics');
+ const ls = initial?.loading_screen || {};
+ setLoadingLogo(ls.logo || '');
+ setLoadingAccent(ls.accentColor || '#ffc020');
+ setLoadingSpinner(ls.defaultSpinner !== false);
+ setLoadingSkip(!!ls.defaultSkipButton);
+ setLsEnabled(ls.enabled !== false);
+ setLsBackground(ls.background || '');
+ setLsCover(ls.cover || '');
+ setLsStyle(ls.style || 'ken-burns');
+ setLsPlaceName(ls.placeName || '');
+ setLsStudioName(ls.studioName || '');
+ setLsVerified(!!ls.verified);
+ setLsDuration(Number.isFinite(ls.duration) && ls.duration > 0 ? ls.duration : 2.5);
+ setLsProgressBar(ls.progressBar !== false);
+ const gx = initial?.graphics || {};
+ setGfxPreset(gx.preset || 'off');
+ setGfxBloom(!!(gx.bloom && gx.bloom.enabled));
+ setGfxFxaa(!!gx.fxaa);
+ setGfxVignette(!!(gx.vignette && gx.vignette.enabled));
+ setGfxSsao(!!gx.ssao);
+ setGfxSaturation((gx.grading && Number.isFinite(gx.grading.saturation)) ? gx.grading.saturation : 1.0);
+ setGfxContrast((gx.grading && Number.isFinite(gx.grading.contrast)) ? gx.grading.contrast : 1.0);
+ setError('');
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open, section]);
+
+ if (!open) return null;
+
+ const handleLogoSelect = (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Логотип: только PNG, JPG или WEBP'); return; }
+ if (file.size > MAX_IMG_BYTES) { setError('Логотип слишком большой (макс. 500 КБ)'); return; }
+ const reader = new FileReader();
+ reader.onload = (ev) => { setLoadingLogo(ev.target.result); setError(''); };
+ reader.readAsDataURL(file);
+ };
+ const handleLsImage = (e, setter) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Только PNG, JPG или WEBP'); return; }
+ if (file.size > MAX_IMG_BYTES) { setError('Изображение слишком большое (макс. 500 КБ)'); return; }
+ const reader = new FileReader();
+ reader.onload = (ev) => { setter(ev.target.result); setError(''); };
+ reader.readAsDataURL(file);
+ };
+
+ const applyPresetToToggles = (preset) => {
+ setGfxPreset(preset);
+ const P = {
+ off: { b: false, f: false, v: false, s: false, sat: 1.0, con: 1.0 },
+ low: { b: true, f: true, v: false, s: false, sat: 1.0, con: 1.0 },
+ medium: { b: true, f: true, v: true, s: false, sat: 1.1, con: 1.05 },
+ high: { b: true, f: true, v: true, s: true, sat: 1.2, con: 1.1 },
+ ultra: { b: true, f: true, v: true, s: true, sat: 1.25, con: 1.12 },
+ cinematic: { b: true, f: true, v: true, s: true, sat: 1.05, con: 1.18 },
+ vivid: { b: true, f: true, v: false, s: false, sat: 1.5, con: 1.1 },
+ night: { b: true, f: true, v: true, s: true, sat: 0.85, con: 1.2 },
+ retro: { b: false, f: false, v: true, s: false, sat: 0.7, con: 1.3 },
+ soft: { b: true, f: true, v: true, s: false, sat: 1.05, con: 0.95 },
+ }[preset];
+ if (!P) return;
+ setGfxBloom(P.b); setGfxFxaa(P.f); setGfxVignette(P.v);
+ setGfxSsao(P.s); setGfxSaturation(P.sat); setGfxContrast(P.con);
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSave({
+ loading_screen: {
+ logo: loadingLogo || null,
+ accentColor: loadingAccent || '#ffc020',
+ defaultSpinner: loadingSpinner,
+ defaultSkipButton: loadingSkip,
+ enabled: lsEnabled,
+ background: lsBackground || null,
+ cover: lsCover || null,
+ style: lsStyle || 'ken-burns',
+ placeName: lsPlaceName.trim(),
+ studioName: lsStudioName.trim(),
+ verified: lsVerified,
+ duration: Math.max(1, Math.min(10, Number(lsDuration) || 2.5)),
+ progressBar: lsProgressBar,
+ },
+ graphics: {
+ preset: gfxPreset,
+ bloom: { enabled: gfxBloom },
+ fxaa: gfxFxaa,
+ vignette: { enabled: gfxVignette },
+ ssao: gfxSsao,
+ grading: {
+ enabled: (gfxSaturation !== 1.0 || gfxContrast !== 1.0),
+ saturation: Number(gfxSaturation) || 1.0,
+ contrast: Number(gfxContrast) || 1.0,
+ },
+ },
+ });
+ };
+
+ return (
+ { if (e.target === e.currentTarget) onClose(); }}>
+
+
+
+ {/* Табы разделов */}
+
+ {TABS.map((t) => (
+ setTab(t.id)}
+ style={{
+ display: 'flex', alignItems: 'center', gap: 6,
+ padding: '8px 14px', borderRadius: 8, cursor: 'pointer',
+ border: '1px solid ' + (tab === t.id ? 'rgba(120,150,255,0.6)' : 'rgba(255,255,255,0.10)'),
+ background: tab === t.id ? 'rgba(90,120,255,0.18)' : 'transparent',
+ color: tab === t.id ? '#dfe6ff' : '#aab', fontSize: 13, fontWeight: 600,
+ }}
+ >
+ {t.title}
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default GameDecorModal;
diff --git a/src/editor/GameSettingsModal.jsx b/src/editor/GameSettingsModal.jsx
index 4b1ea9b..fdfa26c 100644
--- a/src/editor/GameSettingsModal.jsx
+++ b/src/editor/GameSettingsModal.jsx
@@ -45,26 +45,8 @@ const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot
const [multiplayer, setMultiplayer] = useState(false);
const [maxPlayers, setMaxPlayers] = useState(10);
const [isTest, setIsTest] = useState(false);
- // Задача 12: экран загрузки
- const [loadingLogo, setLoadingLogo] = useState('');
- const [loadingAccent, setLoadingAccent] = useState('#ffc020');
- const [loadingSpinner, setLoadingSpinner] = useState(true);
- const [loadingSkip, setLoadingSkip] = useState(false);
- // Задача 05: стартовый Ken-Burns экран
- const [lsEnabled, setLsEnabled] = useState(true);
- const [lsBackground, setLsBackground] = useState('');
- const [lsCover, setLsCover] = useState('');
- const [lsStyle, setLsStyle] = useState('ken-burns');
- const [lsPlaceName, setLsPlaceName] = useState('');
- const [lsStudioName, setLsStudioName] = useState('');
- const [lsVerified, setLsVerified] = useState(false);
- const [lsDuration, setLsDuration] = useState(2.5);
- const [lsProgressBar, setLsProgressBar] = useState(true);
const [error, setError] = useState('');
const fileInputRef = useRef(null);
- const logoInputRef = useRef(null);
- const lsBgInputRef = useRef(null);
- const lsCoverInputRef = useRef(null);
// Заполняем поля ОДИН РАЗ при открытии модала.
// Не зависим от `initial` — родитель часто передаёт литерал-объект,
@@ -78,21 +60,6 @@ const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot
setIsPublic(!!initial?.is_public);
setMultiplayer(!!initial?.multiplayer);
setIsTest(!!initial?.is_test);
- const ls = initial?.loading_screen || {};
- setLoadingLogo(ls.logo || '');
- setLoadingAccent(ls.accentColor || '#ffc020');
- setLoadingSpinner(ls.defaultSpinner !== false);
- setLoadingSkip(!!ls.defaultSkipButton);
- // Задача 05:
- setLsEnabled(ls.enabled !== false);
- setLsBackground(ls.background || '');
- setLsCover(ls.cover || '');
- setLsStyle(ls.style || 'ken-burns');
- setLsPlaceName(ls.placeName || '');
- setLsStudioName(ls.studioName || '');
- setLsVerified(!!ls.verified);
- setLsDuration(Number.isFinite(ls.duration) && ls.duration > 0 ? ls.duration : 2.5);
- setLsProgressBar(ls.progressBar !== false);
setMaxPlayers(
typeof initial?.max_players === 'number'
? Math.max(2, Math.min(50, initial.max_players))
@@ -129,27 +96,6 @@ const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot
reader.readAsDataURL(file);
};
- const handleLogoSelect = (e) => {
- const file = e.target.files?.[0];
- if (!file) return;
- if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Логотип: только PNG, JPG или WEBP'); return; }
- if (file.size > MAX_THUMBNAIL_BYTES) { setError('Логотип слишком большой (макс. 500 КБ)'); return; }
- const reader = new FileReader();
- reader.onload = (ev) => { setLoadingLogo(ev.target.result); setError(''); };
- reader.readAsDataURL(file);
- };
-
- // Задача 05: универсальный загрузчик изображения (фон / cover-карточка).
- const handleLsImage = (e, setter) => {
- const file = e.target.files?.[0];
- if (!file) return;
- if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Только PNG, JPG или WEBP'); return; }
- if (file.size > MAX_THUMBNAIL_BYTES) { setError('Изображение слишком большое (макс. 500 КБ)'); return; }
- const reader = new FileReader();
- reader.onload = (ev) => { setter(ev.target.result); setError(''); };
- reader.readAsDataURL(file);
- };
-
const handleSubmit = (e) => {
e.preventDefault();
const trimmedTitle = title.trim();
@@ -174,22 +120,6 @@ const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot
multiplayer,
max_players: Math.max(2, Math.min(50, Number(maxPlayers) || 10)),
is_test: isTest,
- loading_screen: {
- logo: loadingLogo || null,
- accentColor: loadingAccent || '#ffc020',
- defaultSpinner: loadingSpinner,
- defaultSkipButton: loadingSkip,
- // Задача 05:
- enabled: lsEnabled,
- background: lsBackground || null,
- cover: lsCover || null,
- style: lsStyle || 'ken-burns',
- placeName: lsPlaceName.trim(),
- studioName: lsStudioName.trim(),
- verified: lsVerified,
- duration: Math.max(1, Math.min(10, Number(lsDuration) || 2.5)),
- progressBar: lsProgressBar,
- },
});
};
@@ -370,172 +300,6 @@ const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot
)}
- {/* Экран загрузки (задача 12) */}
-
-
- Экран загрузки
-
-
- Логотип и цвет акцента для экранов загрузки между мирами (game.loading).
-
-
-
- {loadingLogo
- ?
- :
лого = обложка }
-
-
- logoInputRef.current?.click()}>
- Логотип игры
-
- {loadingLogo && (
- setLoadingLogo('')}>
- Убрать
-
- )}
-
-
-
- Цвет акцента
- setLoadingAccent(e.target.value)}
- style={{ width: 48, height: 32, border: 'none', background: 'none', cursor: 'pointer' }} />
-
-
-
-
- setLoadingSpinner(e.target.checked)} />
-
-
Спиннер
-
Показывать «ЗАГРУЗКА» по умолчанию
-
-
-
- setLoadingSkip(e.target.checked)} />
-
-
Кнопка «Пропустить»
-
Показывать по умолчанию
-
-
-
-
-
- {/* Стартовый экран — Ken Burns + название места (задача 05) */}
-
-
- Стартовый экран входа (Ken Burns)
-
-
- Что видит игрок при входе в игру: размытый фон с медленным движением (Ken Burns), карточка-витрина по центру, название места и автор.
-
-
- setLsEnabled(e.target.checked)} />
-
-
Показывать стартовый экран
-
Если выключено — игрок сразу попадает в 3D-сцену
-
-
-
- {lsEnabled && (
- <>
- {/* Фон + карточка */}
-
-
-
- {!lsBackground && фон (размытый) }
-
-
lsBgInputRef.current?.click()}>
- Фон
-
- {lsBackground && (
-
setLsBackground('')}>
- Убрать
-
- )}
-
handleLsImage(e, setLsBackground)} />
-
-
-
- {!lsCover && карточка }
-
-
lsCoverInputRef.current?.click()}>
- Карточка
-
- {lsCover && (
-
setLsCover('')}>
- Убрать
-
- )}
-
handleLsImage(e, setLsCover)} />
-
-
-
-
- {/* Стиль + длительность + прогресс */}
-
- >
- )}
-
-
{error && {error}
}
diff --git a/src/editor/KubikonEditor.jsx b/src/editor/KubikonEditor.jsx
index ccebae2..b850c97 100644
--- a/src/editor/KubikonEditor.jsx
+++ b/src/editor/KubikonEditor.jsx
@@ -14,6 +14,7 @@ import { getModelThumbnail } from './engine/ModelThumbnails';
import * as Kubikon3DApi from '../api/Kubikon3DService';
import { REALTIME_HTTP } from '../api/API';
import GameSettingsModal from './GameSettingsModal';
+import GameDecorModal from './GameDecorModal';
import SkinManagerModal from './SkinManagerModal';
import PublishModal from './PublishModal';
import EmailConfirmNotice from '../components/EmailConfirmNotice/EmailConfirmNotice';
@@ -931,6 +932,9 @@ const KubikonEditor = () => {
// settingsModalOpen — настройки игры (Roblox Game Settings)
// initialModalOpen — инициальный диалог при создании новой игры
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
+ // Модал «Оформление» (графика/стартовый экран/экран загрузки) — из вкладки «Игра».
+ const [decorModalOpen, setDecorModalOpen] = useState(false);
+ const [decorSection, setDecorSection] = useState('graphics');
// Задача 07: модал управления скинами проекта + список всех скинов (манифест).
const [skinManagerOpen, setSkinManagerOpen] = useState(false);
const [allSkinsList, setAllSkinsList] = useState([]);
@@ -1682,6 +1686,13 @@ const KubikonEditor = () => {
return pd?.scene?.loadingScreen || null;
} catch { return null; }
})()) || null,
+ // Графика/эффекты из scene-JSON (для модала настроек).
+ graphics: (data.project_data && (() => {
+ try {
+ const pd = typeof data.project_data === 'string' ? JSON.parse(data.project_data) : data.project_data;
+ return pd?.scene?.graphics || null;
+ } catch { return null; }
+ })()) || null,
};
// Состояние публикации (этап 3)
setProjectStatus({
@@ -1977,11 +1988,6 @@ const KubikonEditor = () => {
const handleSettingsSave = (data) => {
metaRef.current = { ...metaRef.current, ...data };
setProjectName(data.title);
- // Задача 12: конфиг экрана загрузки → в сцену (попадёт в project_data.scene
- // через toJSON). Логотип-дефолт = обложка проекта.
- try {
- sceneRef.current?.setLoadingConfig?.(data.loading_screen || null, data.thumbnail);
- } catch (e) { /* ignore */ }
setSettingsModalOpen(false);
setInitialModalOpen(false);
if (autoSaveTimerRef.current) {
@@ -1992,6 +1998,28 @@ const KubikonEditor = () => {
doSave();
};
+ // Сохранить «Оформление» (графика / стартовый экран / экран загрузки) из
+ // модала, открытого во вкладке «Игра». Применяем сразу (превью) + в сцену.
+ const handleDecorSave = (data) => {
+ try {
+ sceneRef.current?.setLoadingConfig?.(
+ data.loading_screen || null, metaRef.current?.thumbnail);
+ } catch (e) { /* ignore */ }
+ try {
+ if (data.graphics) sceneRef.current?.setGraphics?.(data.graphics);
+ } catch (e) { /* ignore */ }
+ // запомним в metaRef, чтобы модал открылся с актуальными значениями
+ metaRef.current = {
+ ...metaRef.current,
+ loading_screen: data.loading_screen,
+ graphics: data.graphics,
+ };
+ setDecorModalOpen(false);
+ dirtyRef.current = true;
+ doSave();
+ };
+ const openDecor = (sec) => { setDecorSection(sec); setDecorModalOpen(true); };
+
// Закрыть инициальный диалог: если пользователь не сохранил — возвращаемся в Studio.
const handleInitialClose = () => {
setInitialModalOpen(false);
@@ -2322,6 +2350,9 @@ const KubikonEditor = () => {
}}
onSkins={() => setSkinManagerOpen(true)}
onInvite={handleInvite}
+ onGraphics={() => openDecor('graphics')}
+ onStartScreen={() => openDecor('startscreen')}
+ onLoadingScreen={() => openDecor('loadingscreen')}
collabActive={collabActive}
collabPeers={collabPeers}
hasSelection={!!selection}
@@ -3746,6 +3777,14 @@ const KubikonEditor = () => {
onSave={handleSettingsSave}
onCaptureScreenshot={captureSceneScreenshot}
/>
+ {/* Оформление: графика / стартовый экран / экран загрузки (вкладка «Игра») */}
+ setDecorModalOpen(false)}
+ onSave={handleDecorSave}
+ />
{/* Задача 07: управление скинами проекта (стартовый, магазин, рублики, кастомные .glb) */}
{
activeTool, onToolChange,
isPlaying, onPlayToggle, onSetSpawn,
onSkins, onInvite, collabActive, collabPeers,
+ onGraphics, onStartScreen, onLoadingScreen,
hasSelection,
onDuplicate, onAlignToFloor, onDelete,
onClearScene,
@@ -450,6 +451,25 @@ const TopRibbon = (props) => {
/>
+ {/* Оформление — графика/эффекты, стартовый экран, экран загрузки. */}
+
+
+
+
+
+
{/* «Окружение» (время суток / амбиент / музыка) и
«Скин игрока» переехали в иерархию объектов сцены:
🌞 Освещение / 🎵 Звук / 👤 Игрок. */}
diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js
index e1feeeb..35dad64 100644
--- a/src/editor/engine/BabylonScene.js
+++ b/src/editor/engine/BabylonScene.js
@@ -97,6 +97,7 @@ import { GdForest } from './GdForest';
import { GdPlayerCube } from './GdPlayerCube';
import { GdPlayerTrail } from './GdPlayerTrail';
import { GdPostFx } from './GdPostFx';
+import { GraphicsManager } from './GraphicsManager';
import { PhysicsAABB } from './PhysicsAABB';
import { PlayerController } from './PlayerController';
import { SelectionManager } from './SelectionManager';
@@ -1799,6 +1800,47 @@ export class BabylonScene {
getShadowQuality() { return this._shadowQuality || 'soft'; }
+ /**
+ * Система графики/эффектов («шейдеры»). Лениво создаём GraphicsManager
+ * (постобработка: bloom/FXAA/виньетка/цветокор/DoF + управление тенями/SSAO).
+ * По умолчанию ВЫКЛЮЧЕНО — вызывается только когда автор настроил графику.
+ */
+ _ensureGraphics() {
+ if (this._graphics) {
+ // камера могла смениться (режимы камеры) — синхронизируем
+ const cam = this.scene?.activeCamera || this.camera;
+ if (cam) this._graphics.setCamera(cam);
+ return this._graphics;
+ }
+ const cam = this.scene?.activeCamera || this.camera;
+ if (!this.scene || !cam) return null;
+ this._graphics = new GraphicsManager(this.scene, cam, this, {
+ mobile: !!this._isMobileMode,
+ });
+ return this._graphics;
+ }
+
+ /** Применить настройки графики. settings: {preset} и/или секции
+ * (bloom/vignette/grading/dof/ssao/fxaa/shadows). */
+ setGraphics(settings) {
+ const g = this._ensureGraphics();
+ if (!g) return null;
+ const cfg = g.apply(settings || {});
+ this._graphicsConfig = cfg;
+ return cfg;
+ }
+
+ /** Текущая конфигурация графики (для serialize). */
+ getGraphicsState() {
+ return this._graphics ? this._graphics.serialize() : (this._graphicsConfig || null);
+ }
+
+ /** Выключить все эффекты. */
+ disableGraphics() {
+ if (this._graphics) this._graphics.disableAll();
+ this._graphicsConfig = null;
+ }
+
/** Включить/выключить SSAO пост-эффект (контактные тени).
*
* Используем SSAORenderingPipeline v1 (не v2). v2 требует
@@ -7717,6 +7759,7 @@ export class BabylonScene {
crosshair: this._crosshair || 'dot',
shadowQuality: this._shadowQuality || 'soft',
environment: this.environment ? this.environment.serialize() : null,
+ graphics: this.getGraphicsState(),
skybox: this.skybox ? this.skybox.serialize() : null,
leaderstats: this.leaderstats ? this.leaderstats.serialize() : null,
achievements: this.achievements ? this.achievements.serialize() : null,
@@ -8199,6 +8242,12 @@ export class BabylonScene {
if (state.scene.environment && this.environment) {
this.environment.load(state.scene.environment);
}
+ // Графика/эффекты (шейдеры). Применяем только если автор что-то настроил
+ // и это не 'off' — иначе не трогаем рендер (чистая картинка, дефолт).
+ if (state.scene.graphics && state.scene.graphics.preset
+ && state.scene.graphics.preset !== 'off') {
+ try { this.setGraphics(state.scene.graphics); } catch (e) { /* ignore */ }
+ }
// Кастомное небо (задача 16)
if (state.scene.skybox && this.skybox) {
this.skybox.load(state.scene.skybox);
diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js
index 16dfde9..175d0b1 100644
--- a/src/editor/engine/GameRuntime.js
+++ b/src/editor/engine/GameRuntime.js
@@ -2812,6 +2812,10 @@ export class GameRuntime {
} catch (e) {}
return;
}
+ if (cmd === 'graphics.set') {
+ try { this.scene3d?.setGraphics?.(payload || {}); } catch (e) {}
+ return;
+ }
if (cmd === 'player.setCrouch') {
const player = this.scene3d?.player;
if (player) {
diff --git a/src/editor/engine/GraphicsManager.js b/src/editor/engine/GraphicsManager.js
new file mode 100644
index 0000000..fcb156b
--- /dev/null
+++ b/src/editor/engine/GraphicsManager.js
@@ -0,0 +1,328 @@
+/**
+ * GraphicsManager — система визуальных эффектов («шейдеры») для игр Рублокса.
+ *
+ * Управляет:
+ * - постобработкой экрана через Babylon DefaultRenderingPipeline:
+ * bloom (свечение), FXAA (сглаживание), виньетка, цветокоррекция
+ * (контраст/насыщенность/экспозиция), тонмаппинг, глубина резкости (DoF);
+ * - качеством теней (через scene3d.setShadowQuality);
+ * - контактными тенями SSAO (через scene3d.setSsaoEnabled).
+ *
+ * Управляется И из настроек игры (вкладка «Графика»), И из скриптов
+ * (game.graphics.*). По умолчанию ВСЁ ВЫКЛЮЧЕНО (preset 'off') — старые игры
+ * не меняются, FPS не страдает. Автор включает осознанно.
+ *
+ * Mobile-safe: на слабых устройствах тяжёлые эффекты (DoF, SSAO, ultra-тени,
+ * HDR-bloom) автоматически урезаются, даже если в пресете включены.
+ *
+ * Один и тот же класс используется в студии и плеере (фича-парность).
+ *
+ * Использование:
+ * const gfx = new GraphicsManager(scene, camera, scene3d, { mobile });
+ * gfx.apply({ preset: 'cinematic' });
+ * gfx.apply({ bloom: { enabled: true, intensity: 0.7 } });
+ * gfx.dispose();
+ */
+import {
+ DefaultRenderingPipeline, Color4, ImageProcessingConfiguration,
+} from '@babylonjs/core';
+
+/**
+ * Именованные пресеты. Каждый — полный набор настроек. 'off' = чистая картинка
+ * (pipeline не создаётся вовсе). Значения подобраны так, чтобы быть заметными,
+ * но не «кислотными».
+ */
+export const GRAPHICS_PRESETS = {
+ off: {
+ bloom: { enabled: false },
+ fxaa: false,
+ vignette: { enabled: false },
+ grading: { enabled: false },
+ dof: { enabled: false },
+ ssao: false,
+ shadows: null, // null = не трогаем текущее качество теней
+ },
+ // Лёгкий: только мягкое свечение + сглаживание. Дёшево, годится почти везде.
+ low: {
+ bloom: { enabled: true, intensity: 0.3, threshold: 0.9 },
+ fxaa: true,
+ vignette: { enabled: false },
+ grading: { enabled: false },
+ dof: { enabled: false },
+ ssao: false,
+ shadows: 'hard',
+ },
+ // Средний: свечение + лёгкая виньетка + чуть насыщенности.
+ medium: {
+ bloom: { enabled: true, intensity: 0.45, threshold: 0.85 },
+ fxaa: true,
+ vignette: { enabled: true, weight: 0.5 },
+ grading: { enabled: true, contrast: 1.05, saturation: 1.1, exposure: 1.0 },
+ dof: { enabled: false },
+ ssao: false,
+ shadows: 'soft',
+ },
+ // Высокий: всё кроме DoF, SSAO включён.
+ high: {
+ bloom: { enabled: true, intensity: 0.6, threshold: 0.82 },
+ fxaa: true,
+ vignette: { enabled: true, weight: 0.6 },
+ grading: { enabled: true, contrast: 1.1, saturation: 1.2, exposure: 1.05 },
+ dof: { enabled: false },
+ ssao: true,
+ shadows: 'soft',
+ },
+ // Ультра: + глубина резкости + мягкие каскадные тени.
+ ultra: {
+ bloom: { enabled: true, intensity: 0.7, threshold: 0.8 },
+ fxaa: true,
+ vignette: { enabled: true, weight: 0.65 },
+ grading: { enabled: true, contrast: 1.12, saturation: 1.25, exposure: 1.05 },
+ dof: { enabled: true, focusDistance: 18, focalLength: 50, aperture: 1.2 },
+ ssao: true,
+ shadows: 'high',
+ },
+ // === Стилевые пресеты (художественные) ===
+ cinematic: {
+ bloom: { enabled: true, intensity: 0.55, threshold: 0.8 },
+ fxaa: true,
+ vignette: { enabled: true, weight: 0.85 },
+ grading: { enabled: true, contrast: 1.18, saturation: 1.05, exposure: 1.0 },
+ dof: { enabled: true, focusDistance: 22, focalLength: 60, aperture: 1.0 },
+ ssao: true,
+ shadows: 'soft',
+ },
+ vivid: {
+ bloom: { enabled: true, intensity: 0.65, threshold: 0.78 },
+ fxaa: true,
+ vignette: { enabled: false },
+ grading: { enabled: true, contrast: 1.1, saturation: 1.5, exposure: 1.1 },
+ dof: { enabled: false },
+ ssao: false,
+ shadows: 'soft',
+ },
+ night: {
+ bloom: { enabled: true, intensity: 0.8, threshold: 0.7 },
+ fxaa: true,
+ vignette: { enabled: true, weight: 1.0 },
+ grading: { enabled: true, contrast: 1.2, saturation: 0.85, exposure: 0.8 },
+ dof: { enabled: false },
+ ssao: true,
+ shadows: 'soft',
+ },
+ retro: {
+ bloom: { enabled: false },
+ fxaa: false, // намеренно «пиксельно»
+ vignette: { enabled: true, weight: 1.2 },
+ grading: { enabled: true, contrast: 1.3, saturation: 0.7, exposure: 0.95 },
+ dof: { enabled: false },
+ ssao: false,
+ shadows: 'hard',
+ },
+ soft: {
+ bloom: { enabled: true, intensity: 0.4, threshold: 0.88 },
+ fxaa: true,
+ vignette: { enabled: true, weight: 0.4 },
+ grading: { enabled: true, contrast: 0.95, saturation: 1.05, exposure: 1.05 },
+ dof: { enabled: false },
+ ssao: false,
+ shadows: 'soft',
+ },
+};
+
+// Глубокое слияние пресета и пользовательских оверрайдов.
+function _mergeConfig(base, over) {
+ const out = JSON.parse(JSON.stringify(base || {}));
+ if (!over) return out;
+ for (const k of Object.keys(over)) {
+ const v = over[k];
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
+ out[k] = { ...(out[k] || {}), ...v };
+ } else {
+ out[k] = v;
+ }
+ }
+ return out;
+}
+
+export class GraphicsManager {
+ /**
+ * @param scene Babylon Scene
+ * @param camera активная камера (для pipeline)
+ * @param scene3d ссылка на BabylonScene (для setShadowQuality / setSsaoEnabled / света)
+ * @param opts { mobile:boolean }
+ */
+ constructor(scene, camera, scene3d, opts = {}) {
+ this.scene = scene;
+ this.camera = camera;
+ this.scene3d = scene3d;
+ this.mobile = !!opts.mobile;
+ this._pipeline = null;
+ // Текущая активная конфигурация (после merge + mobile-clamp).
+ this.config = _mergeConfig(GRAPHICS_PRESETS.off, null);
+ this.config.preset = 'off';
+ this.enabled = false;
+ }
+
+ /** Сменить камеру (например после смены режима камеры) — пересобрать pipeline. */
+ setCamera(camera) {
+ if (camera === this.camera) return;
+ this.camera = camera;
+ if (this.enabled) this._rebuildPipeline();
+ }
+
+ /**
+ * Применить настройки графики. Принимает либо {preset}, либо отдельные
+ * секции (bloom/vignette/grading/dof/ssao/fxaa/shadows), либо и то и другое
+ * (оверрайды поверх пресета). Сохраняет состояние в this.config.
+ */
+ apply(settings = {}) {
+ let cfg;
+ if (settings.preset && GRAPHICS_PRESETS[settings.preset]) {
+ cfg = _mergeConfig(GRAPHICS_PRESETS[settings.preset], settings);
+ cfg.preset = settings.preset;
+ } else {
+ // частичный апдейт поверх текущего
+ cfg = _mergeConfig(this.config, settings);
+ cfg.preset = settings.preset || this.config.preset || 'custom';
+ }
+ this.config = this._clampForMobile(cfg);
+ this._applyConfig();
+ return this.config;
+ }
+
+ /** Полностью выключить эффекты (как preset 'off'). */
+ disableAll() {
+ return this.apply({ preset: 'off' });
+ }
+
+ /** Текущая конфигурация (для serialize). */
+ serialize() {
+ // Храним «как просили» (preset + явные оверрайды). Для простоты — весь cfg.
+ return JSON.parse(JSON.stringify(this.config));
+ }
+
+ // --- внутреннее ---
+
+ /** На слабых устройствах гасим самое дорогое, что бы ни просили. */
+ _clampForMobile(cfg) {
+ if (!this.mobile) return cfg;
+ const c = JSON.parse(JSON.stringify(cfg));
+ if (c.dof) c.dof.enabled = false; // DoF дорогой
+ c.ssao = false; // SSAO дорогой
+ if (c.shadows === 'high' || c.shadows === 'medium') c.shadows = 'hard';
+ // bloom оставляем, но без HDR (решается в _rebuildPipeline)
+ c._mobileClamped = true;
+ return c;
+ }
+
+ _applyConfig() {
+ const c = this.config;
+ const anyPipelineFx = (c.bloom && c.bloom.enabled) || c.fxaa
+ || (c.vignette && c.vignette.enabled) || (c.grading && c.grading.enabled)
+ || (c.dof && c.dof.enabled);
+
+ // Тени и SSAO — через scene3d (они вне pipeline).
+ try {
+ if (c.shadows && this.scene3d?.setShadowQuality) {
+ this.scene3d.setShadowQuality(c.shadows);
+ }
+ } catch (e) { /* ignore */ }
+ try {
+ if (this.scene3d?.setSsaoEnabled) this.scene3d.setSsaoEnabled(!!c.ssao);
+ } catch (e) { /* ignore */ }
+
+ if (!anyPipelineFx) {
+ this.enabled = false;
+ this._disposePipeline();
+ return;
+ }
+ this.enabled = true;
+ this._rebuildPipeline();
+ }
+
+ _rebuildPipeline() {
+ this._disposePipeline();
+ if (!this.scene || !this.camera) return;
+ const c = this.config;
+ const useHdr = (c.bloom && c.bloom.enabled) && !this.mobile;
+
+ const p = new DefaultRenderingPipeline('rbx_graphics', useHdr, this.scene, [this.camera]);
+
+ // Bloom
+ p.bloomEnabled = !!(c.bloom && c.bloom.enabled);
+ if (p.bloomEnabled) {
+ p.bloomThreshold = c.bloom.threshold ?? 0.85;
+ p.bloomWeight = c.bloom.intensity ?? 0.5;
+ p.bloomKernel = this.mobile ? 32 : 64;
+ p.bloomScale = 0.5;
+ }
+
+ // FXAA
+ p.fxaaEnabled = !!c.fxaa;
+ p.samples = this.mobile ? 1 : 4;
+
+ // Image processing: виньетка + цветокоррекция + (опц.) тонмаппинг
+ const ip = p.imageProcessing;
+ if (ip) {
+ p.imageProcessingEnabled = true;
+ ip.toneMappingEnabled = false; // как в GdPostFx — иначе картинка темнеет
+ // экспозиция/контраст из grading
+ if (c.grading && c.grading.enabled) {
+ ip.exposure = c.grading.exposure ?? 1.0;
+ ip.contrast = c.grading.contrast ?? 1.0;
+ ip.colorCurvesEnabled = true;
+ try {
+ const curves = ip.colorCurves;
+ if (curves) {
+ // saturation: 1.0 = норма → curves в диапазоне примерно -100..100
+ const sat = c.grading.saturation ?? 1.0;
+ curves.globalSaturation = Math.round((sat - 1.0) * 60);
+ }
+ } catch (e) { /* ignore */ }
+ } else {
+ ip.exposure = 1.0; ip.contrast = 1.0;
+ }
+ // виньетка
+ if (c.vignette && c.vignette.enabled) {
+ ip.vignetteEnabled = true;
+ ip.vignetteWeight = c.vignette.weight ?? 0.6;
+ ip.vignetteColor = new Color4(0, 0, 0, 0);
+ ip.vignetteStretch = 0.3;
+ ip.vignetteCameraFov = 0.5;
+ ip.vignetteBlendMode = ImageProcessingConfiguration.VIGNETTEMODE_MULTIPLY;
+ } else {
+ ip.vignetteEnabled = false;
+ }
+ }
+
+ // Depth of Field (глубина резкости) — только desktop
+ if (c.dof && c.dof.enabled && !this.mobile) {
+ p.depthOfFieldEnabled = true;
+ try {
+ p.depthOfFieldBlurLevel = 1; // 0..2
+ p.depthOfField.focusDistance = (c.dof.focusDistance ?? 18) * 1000; // мм
+ p.depthOfField.focalLength = c.dof.focalLength ?? 50;
+ p.depthOfField.fStop = c.dof.aperture ?? 1.2;
+ } catch (e) { /* ignore */ }
+ } else {
+ p.depthOfFieldEnabled = false;
+ }
+
+ this._pipeline = p;
+ }
+
+ _disposePipeline() {
+ if (this._pipeline) {
+ try { this._pipeline.dispose(); } catch (e) { /* ignore */ }
+ this._pipeline = null;
+ }
+ }
+
+ dispose() {
+ this._disposePipeline();
+ this.scene = null;
+ this.camera = null;
+ this.scene3d = null;
+ }
+}
diff --git a/src/editor/engine/PrimitiveManager.js b/src/editor/engine/PrimitiveManager.js
index 8a50c0c..ed6e386 100644
--- a/src/editor/engine/PrimitiveManager.js
+++ b/src/editor/engine/PrimitiveManager.js
@@ -6,7 +6,7 @@
* - позиция (x, y, z)
* - размер (sx, sy, sz)
* - цвет (#hex)
- * - материал ('matte' | 'metal' | 'glass' | 'neon')
+ * - материал ('matte'|'metal'|'glass'|'neon'|'studs'|'chrome'|'water'|'iridescent')
* - canCollide (bool) — участвует ли в физике коллизий
* - visible (bool) — рисуется ли (anchored — пока заготовка)
*
@@ -533,7 +533,9 @@ export class PrimitiveManager {
break;
case 'glass':
mat.alpha = 0.4;
- mat.specularColor = new Color3(0.5, 0.5, 0.5);
+ mat.specularColor = new Color3(0.8, 0.85, 0.9);
+ mat.specularPower = 96; // более чёткий блик на стекле
+ mat.backFaceCulling = false; // видно «толщину» — глубже эффект
break;
case 'neon':
mat.emissiveColor = Color3.FromHexString(color || '#888888');
@@ -566,6 +568,39 @@ export class PrimitiveManager {
mat.specularColor = new Color3(0, 0, 0);
break;
}
+ case 'chrome': {
+ // Хром/зеркало: яркий узкий блик + лёгкое самосвечение цвета,
+ // чтобы поверхность «играла» даже без cubemap-отражений (их не
+ // грузим — экономим ассеты и FPS на детских машинах).
+ const cc = Color3.FromHexString(color || '#cfd6e0');
+ mat.diffuseColor = new Color3(cc.r * 0.6, cc.g * 0.6, cc.b * 0.6);
+ mat.specularColor = new Color3(1, 1, 1);
+ mat.specularPower = 128; // узкий резкий блик = «металл»
+ mat.emissiveColor = new Color3(cc.r * 0.12, cc.g * 0.12, cc.b * 0.14);
+ break;
+ }
+ case 'water': {
+ // Вода: полупрозрачный голубой с бликами. Анимацию ряби делает
+ // GraphicsManager/Environment по флагу mesh._isWater (опционально).
+ const wc = Color3.FromHexString(color || '#3aa0ff');
+ mat.diffuseColor = wc;
+ mat.alpha = 0.55;
+ mat.specularColor = new Color3(0.9, 0.95, 1.0);
+ mat.specularPower = 64;
+ mat.emissiveColor = new Color3(wc.r * 0.1, wc.g * 0.14, wc.b * 0.2);
+ mesh._isWater = true;
+ break;
+ }
+ case 'iridescent': {
+ // Переливы: насыщенное самосвечение + блик. Цвет «бензиновой
+ // плёнки» — приятно для кристаллов, мыльных пузырей, порталов.
+ const ic = Color3.FromHexString(color || '#a06bff');
+ mat.diffuseColor = ic;
+ mat.emissiveColor = new Color3(ic.r * 0.5, ic.g * 0.35, ic.b * 0.6);
+ mat.specularColor = new Color3(1, 1, 1);
+ mat.specularPower = 96;
+ break;
+ }
case 'matte':
default:
mat.specularColor = new Color3(0, 0, 0);
diff --git a/src/editor/engine/ScriptSandboxWorker.js b/src/editor/engine/ScriptSandboxWorker.js
index 6d38fa4..4119fcd 100644
--- a/src/editor/engine/ScriptSandboxWorker.js
+++ b/src/editor/engine/ScriptSandboxWorker.js
@@ -3890,6 +3890,53 @@ const game = {
_send('environment.setTimeOfDay', { hours: h });
},
},
+ /**
+ * graphics — визуальные эффекты («шейдеры»): постобработка, свечение,
+ * цветокоррекция, тени. По умолчанию всё выключено.
+ */
+ graphics: {
+ /** Применить пресет: 'off'|'low'|'medium'|'high'|'ultra'|
+ * 'cinematic'|'vivid'|'night'|'retro'|'soft'. */
+ setPreset(preset) {
+ if (typeof preset !== 'string') return;
+ _send('graphics.set', { preset });
+ },
+ /** Тонкая настройка (поверх текущего): передай любые секции
+ * {bloom, vignette, grading, dof, ssao, fxaa, shadows}. */
+ set(settings) {
+ if (typeof settings !== 'object' || !settings) return;
+ _send('graphics.set', settings);
+ },
+ /** Свечение. on:bool, opts:{intensity:0..1, threshold:0..1}. */
+ setBloom(on, opts) {
+ _send('graphics.set', { bloom: { enabled: !!on, ...(opts || {}) } });
+ },
+ /** Виньетка (затемнение углов). weight: 0..1.5, 0 = выкл. */
+ setVignette(weight) {
+ const w = Number(weight) || 0;
+ _send('graphics.set', { vignette: { enabled: w > 0, weight: w } });
+ },
+ /** Цветокоррекция: {contrast, saturation, exposure} (1.0 = норма). */
+ setColorGrading(opts) {
+ if (typeof opts !== 'object' || !opts) return;
+ _send('graphics.set', { grading: { enabled: true, ...opts } });
+ },
+ /** Сглаживание (FXAA). */
+ setAntialiasing(on) { _send('graphics.set', { fxaa: !!on }); },
+ /** Глубина резкости: on:bool, opts:{focusDistance, focalLength, aperture}. */
+ setDepthOfField(on, opts) {
+ _send('graphics.set', { dof: { enabled: !!on, ...(opts || {}) } });
+ },
+ /** Качество теней: 'off'|'hard'|'soft'|'medium'|'high'. */
+ setShadows(quality) {
+ if (typeof quality !== 'string') return;
+ _send('graphics.set', { shadows: quality });
+ },
+ /** Контактные тени (SSAO). */
+ setSSAO(on) { _send('graphics.set', { ssao: !!on }); },
+ /** Полностью выключить эффекты. */
+ off() { _send('graphics.set', { preset: 'off' }); },
+ },
save: {
/** Прочитать namespace. fn(data) — data это сохранённый объект или null. */
get(namespace, fn) {