From 34060c90c385830270a69473bad23cf111effa9a Mon Sep 17 00:00:00 2001
From: min
Date: Tue, 2 Jun 2026 22:00:26 +0300
Subject: [PATCH 1/2] =?UTF-8?q?feat(12):=20=D0=B2=D0=BD=D1=83=D1=82=D1=80?=
=?UTF-8?q?=D0=B8=D0=B8=D0=B3=D1=80=D0=BE=D0=B2=D0=BE=D0=B9=20Loading=20Sc?=
=?UTF-8?q?reen=20(game.loading)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Программный экран загрузки для перехода между мирами:
- game.loading.show(opts) → хэндл (setProgress/setText/setCover/close/onSkip/onComplete)
- game.loading.transition(opts) → Promise (фейковый прогресс за duration)
- cover sceneSnapshot, прогресс-бар+процент, спиннер, кнопка Пропустить, логотип
- blockInput + пауза симуляции, fadeIn/Out; tick независим от paused
- настройки проекта «Экран загрузки» (логотип/акцент/дефолты) + 3 сниппета
- LoadingScreenOverlay.js (новый DOM-оверлей), worker namespace loading,
cmd loading.* + _ensureLoadingScreen, serialize/load конфига в scene
- вики g5 #59 guide-taxi (карточка + урок), тест-игра «Такси-босс» id 2427
Co-Authored-By: Claude Opus 4.8
---
src/community/docsGames.js | 5 +
src/community/docsLessons.jsx | 112 ++++++
src/editor/GameSettingsModal.jsx | 84 +++++
src/editor/KubikonEditor.jsx | 15 +
src/editor/engine/BabylonScene.js | 46 +++
src/editor/engine/GameRuntime.js | 38 +++
src/editor/engine/LoadingScreenOverlay.js | 399 ++++++++++++++++++++++
src/editor/engine/ScriptSandboxWorker.js | 79 +++++
src/editor/engine/snippets.js | 41 +++
9 files changed, 819 insertions(+)
create mode 100644 src/editor/engine/LoadingScreenOverlay.js
diff --git a/src/community/docsGames.js b/src/community/docsGames.js
index 817b899..c035355 100644
--- a/src/community/docsGames.js
+++ b/src/community/docsGames.js
@@ -338,4 +338,9 @@ export const GAMES = [
desc: 'Tycoon-механика «купи → поставь»: магазин-инвентарь снизу, клик по слоту → полупрозрачная тень предмета летит за курсором, ЛКМ ставит на свой участок (можно стопкой), R/колесо — поворот, ПКМ/Esc — отмена. Денег мало → слот серый. Воксельный мир: трава, деревья, пруд.',
mechanics: ['game.placement.start', 'game.inventoryUi (магазин-слоты)', 'onPlace/onCancel/onMove', 'тень-превью формой модели', 'снап к сетке + стопка', 'проверка баланса (не в минус)', 'воксельные модели + ландшафт'],
previewShot: 'guide-zavod-scene.png', openProjectId: 2345, ready: true },
+ { id: 'guide-taxi', num: 59, group: 'g5', stars: 2, icon: 'loader',
+ title: 'Такси-босс — экран загрузки между мирами',
+ desc: 'Программный экран загрузки (как в Roblox при телепорте): кликнул такси → весь экран плавно затемняется, в центре снимок сцены, жёлтый прогресс-бар заполняется за 4с, процент, кнопка «Пропустить», спиннер «Загрузка». Дальше — телепорт в город и закатное небо. В городе кнопка «Магазин» делает короткий переход.',
+ mechanics: ['game.loading.transition (Promise)', 'game.loading.show (хэндл setProgress/close)', 'cover: sceneSnapshot (снимок сцены)', 'прогресс-бар + процент + спиннер', 'кнопка «Пропустить» (onSkip)', 'blockInput + пауза симуляции', 'воксельный город (такси/небоскрёбы)'],
+ previewShot: 'guide-taxi-scene.png', openProjectId: 2427, ready: true },
];
diff --git a/src/community/docsLessons.jsx b/src/community/docsLessons.jsx
index 3682d17..13f6103 100644
--- a/src/community/docsLessons.jsx
+++ b/src/community/docsLessons.jsx
@@ -8255,6 +8255,118 @@ game.placement.onCancel(() => game.ui.set('hint', '', {}));`}
),
},
+ 'guide-taxi': {
+ body: (
+ <>
+
Что получится
+
+ Игрок стоит в гараже рядом с жёлтым такси. Кликнул по
+ такси → весь экран плавно затемняется, в центре — снимок
+ сцены, под ним жёлтый прогресс-бар заполняется за 4 секунды,
+ крупно процент, кнопка «Пропустить» и спиннер
+ «Загрузка». Через 4 секунды экран исчезает — игрок уже
+ в городе, небо стало закатным. Это та самая «загрузка между
+ мирами», которую в больших играх показывают при телепорте на новый
+ уровень (Taxi Boss, Brookhaven, Jailbreak).
+
+
+
+
+
Чему научишься
+
+
game.loading.transition(opts) — готовый переход: показал
+ экран, заполнил бар за duration секунд, сам скрыл.
+ Возвращает Promise — пишешь await и
+ продолжаешь код уже «на новом уровне»;
+
game.loading.show(opts) — ручной режим: возвращает
+ хэндл с setProgress(0..1), setText(),
+ close() — для реальной загрузки ресурсов;
+
cover: {`{ sceneSnapshot: true }`} — кадр текущей сцены
+ автоматически становится картинкой-превью;
+
кнопка «Пропустить» (skipButton) и
+ спиннер (spinner) — включаются одной опцией;
+
blockInput + пауза — во время загрузки WASD/мышь не
+ работают, а симуляция замирает (NPC не двигаются), автоматически.
+
+
+
Шаг 1. Магазин по умолчанию — настройки игры
+
+ В Настройки → Экран загрузки один раз задаёшь логотип
+ игры, цвет акцента (бар и кнопка) и галочки «спиннер» /
+ «кнопка Пропустить» по умолчанию. Дальше любой game.loading
+ в этой игре берёт их сам — не повторяешь стиль в каждом вызове.
+
+
+
Шаг 2. Клик по такси → переход в город
+
+ На самом такси висит маленький скрипт. game.self.onClick —
+ клик именно по этому объекту. Внутри — await
+ game.loading.transition(...): код «замирает», пока крутится
+ загрузка, и продолжается, когда она закончилась (или игрок нажал
+ «Пропустить»).
+
+
+ {`game.self.onClick(async () => {
+ await game.loading.transition({
+ cover: { sceneSnapshot: true }, // снимок текущей сцены как картинка
+ duration: 4, // бар заполняется 4 секунды
+ text: 'Едем в центр города...',
+ skipButton: true, // можно пропустить ожидание
+ spinner: true, // спиннер «Загрузка» справа
+ });
+ // Этот код выполнится ПОСЛЕ загрузки (экран уже скрыт):
+ game.player.teleport(100, 2, 100); // телепорт в город
+ game.scene.environment = 'sunset'; // закатное небо
+});`}
+
+ transition — это «фейковый» прогресс на заданное время
+ (для красивого перехода). Для реальной загрузки ресурсов есть
+ show + setProgress — см. Шаг 3.
+
+
+
Шаг 3. Ручной прогресс (реальная загрузка)
+
+ Если грузишь много объектов и хочешь показать настоящий
+ прогресс — открой экран через show и двигай бар сам
+ через setProgress. Закрой через close().
+
+
+ {`const lo = game.loading.show({ progressBar: true, spinner: true });
+const total = 10;
+let i = 0;
+const step = () => {
+ i++;
+ // ... подгрузить i-й кусок мира ...
+ lo.setProgress(i / total); // двигаем бар вручную
+ if (i < total) game.after(0.2, step); // следующий шаг через 0.2с
+ else lo.close(); // всё загружено — спрятать экран
+};
+step();`}
+
+
+
+
Почему это удобно
+
+ Один проект — несколько миров (гараж, город, магазин), а
+ переключение между ними прячется за красивым экраном загрузки.
+ Игрок не видит «телепорт рывком» — видит плавную загрузку, как в
+ больших играх. А await делает код линейным: «показать
+ загрузку → дождаться → продолжить».
+
+
+
+ В городе есть кнопка «Магазин» — она делает короткий переход
+ (1.5с) к зданию-магазину. Сделай по аналогии ещё одну точку: кнопку
+ «Гараж», которая через loading.transition на 1 секунду
+ возвращает игрока к такси (teleport(0, 2, 0)) и ставит
+ дневное небо (environment = 'day').
+
+ >
+ ),
+ },
+
};
/** Есть ли готовый текст урока для игры с таким id. */
diff --git a/src/editor/GameSettingsModal.jsx b/src/editor/GameSettingsModal.jsx
index fdfa26c..e579b3d 100644
--- a/src/editor/GameSettingsModal.jsx
+++ b/src/editor/GameSettingsModal.jsx
@@ -45,8 +45,14 @@ 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);
const [error, setError] = useState('');
const fileInputRef = useRef(null);
+ const logoInputRef = useRef(null);
// Заполняем поля ОДИН РАЗ при открытии модала.
// Не зависим от `initial` — родитель часто передаёт литерал-объект,
@@ -60,6 +66,11 @@ 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);
setMaxPlayers(
typeof initial?.max_players === 'number'
? Math.max(2, Math.min(50, initial.max_players))
@@ -96,6 +107,16 @@ 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);
+ };
+
const handleSubmit = (e) => {
e.preventDefault();
const trimmedTitle = title.trim();
@@ -120,6 +141,12 @@ 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,
+ },
});
};
@@ -300,6 +327,63 @@ const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot
)}
+ {/* Экран загрузки (задача 12) */}
+
+
+ Экран загрузки
+
+
+ Логотип и цвет акцента для экранов загрузки между мирами (game.loading).
+
- Игрок стоит в гараже рядом с жёлтым такси. Кликнул по
- такси → весь экран плавно затемняется, в центре — снимок
- сцены, под ним жёлтый прогресс-бар заполняется за 4 секунды,
- крупно процент, кнопка «Пропустить» и спиннер
- «Загрузка». Через 4 секунды экран исчезает — игрок уже
- в городе, небо стало закатным. Это та самая «загрузка между
- мирами», которую в больших играх показывают при телепорте на новый
- уровень (Taxi Boss, Brookhaven, Jailbreak).
+ Три локации в одном проекте, между ними — красивая загрузка.
+ Игрок в гараже у жёлтого такси жмёт «Поехать в город» → весь
+ экран плавно затемняется, в центре снимок сцены, жёлтый
+ прогресс-бар заполняется за 4 секунды, крупно процент,
+ кнопка «Пропустить» и спиннер «Загрузка». Через 4 секунды
+ экран исчезает — игрок уже в городе с высотками и закатным небом.
+ В городе кнопка «Магазин» делает короткий переход внутрь
+ закрытого магазина (ряды стеллажей, кассы), а «Назад» возвращает на
+ улицу. Это та самая «загрузка между мирами» из больших игр (Taxi Boss,
+ Brookhaven, Jailbreak) — несколько миров без отдельных «уровней».
game.ui.set('hint', '', {}));`}
в этой игре берёт их сам — не повторяешь стиль в каждом вызове.
-
Шаг 2. Клик по такси → переход в город
+
Шаг 2. Кнопка «Поехать» → переход в город
- На самом такси висит маленький скрипт. game.self.onClick —
- клик именно по этому объекту. Внутри — await
- game.loading.transition(...): код «замирает», пока крутится
+ Делаем кнопку и вешаем на неё переход. await
+ game.loading.transition(...) «замораживает» код, пока крутится
загрузка, и продолжается, когда она закончилась (или игрок нажал
- «Пропустить»).
+ «Пропустить»). После — телепорт и смена окружения: игрок «оказался» в
+ новом мире.
-
- {`game.self.onClick(async () => {
+
+ {`game.gui.create('button', {
+ id: 'btn_go', x: 50, y: 92, w: 26, h: 9, anchor: 'center',
+ text: 'Поехать в город', textColor: '#3a2a00', textSize: 20, fontWeight: 800,
+ bgGradient: { stops: ['#ffd23a', '#e0a000'], angle: 90 }, borderRadius: 12,
+});
+game.gui.onClick('btn_go', async () => {
+ game.gui.remove('btn_go');
await game.loading.transition({
cover: { sceneSnapshot: true }, // снимок текущей сцены как картинка
duration: 4, // бар заполняется 4 секунды
@@ -8320,8 +8328,11 @@ game.placement.onCancel(() => game.ui.set('hint', '', {}));`}
game.scene.environment = 'sunset'; // закатное небо
});`}
- transition — это «фейковый» прогресс на заданное время
- (для красивого перехода). Для реальной загрузки ресурсов есть
+ transition — «фейковый» прогресс на заданное время (для
+ красивого перехода). Точно так же сделана кнопка «Магазин»:
+ переход 1.5с → teleport внутрь закрытого
+ зала-магазина (отдельная локация со стеллажами и кассами) → кнопка
+ «Назад» возвращает в город. Для реальной загрузки ресурсов есть
show + setProgress — см. Шаг 3.
@@ -8345,23 +8356,26 @@ const step = () => {
step();`}
+ caption="Город изнутри: перекрёсток с высотками по обеим сторонам и Макдональдс в конце улицы — одна из трёх локаций проекта (гараж · город · интерьер магазина)." />
Почему это удобно
- Один проект — несколько миров (гараж, город, магазин), а
- переключение между ними прячется за красивым экраном загрузки.
- Игрок не видит «телепорт рывком» — видит плавную загрузку, как в
- больших играх. А await делает код линейным: «показать
- загрузку → дождаться → продолжить».
+ Один проект — три мира (гараж · город · интерьер магазина),
+ собранные в разных углах сцены, а переключение между ними прячется за
+ красивым экраном загрузки. Игрок не видит «телепорт рывком» — видит
+ плавную загрузку, как в больших играх. А await делает код
+ линейным: «показать загрузку → дождаться → телепортировать → сменить
+ небо». Так из одного проекта получается целая игра с локациями, без
+ отдельных уровней-файлов.
- В городе есть кнопка «Магазин» — она делает короткий переход
- (1.5с) к зданию-магазину. Сделай по аналогии ещё одну точку: кнопку
- «Гараж», которая через loading.transition на 1 секунду
- возвращает игрока к такси (teleport(0, 2, 0)) и ставит
- дневное небо (environment = 'day').
+ Добавь четвёртую локацию — например «парк». Поставь её в ещё
+ одном углу сцены (скажем, x = -100), сделай кнопку «В парк»
+ и переход на неё через loading.transition (свой текст и
+ duration). Не забудь кнопку «Назад», которая возвращает в
+ город. Подсказка: каждый переход = await transition →
+ teleport(...) → environment = '...'.
>
),
--
2.47.2