studio/src/editor/engine/GdFinish.js
МИН 61fba4e174
Some checks failed
CI / Lint + Format (push) Failing after 32s
CI / Build (push) Failing after 37s
CI / Secret scan (push) Failing after 37s
CI / PR size check (push) Has been skipped
fix: починка билда + studio.rublox.pro инфра
Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026).
Содержит изменения которые делались в процессе подготовки прод-окружения:

Фиксы импортов после выноса из minecraftia:
- Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/)
- Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/
- API.js скопирован из минки целиком (было 8 экспортов, стало 312)
- Добавлены PLAYER_URL, MyButton_1, недостающие компоненты
- Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require)

Структура ассетов:
- public/kubikon-templates/ → public/assets/kubikon-templates/
- public/kubikon-learn/ → public/assets/kubikon-learn/
- (код искал в /assets/, файлы лежали без /assets/)

Навигация роутов внутри студии:
- /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced)
- /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X

UI:
- Новый компонент StudioHeader (61px, как в минке) + копия favicon
- WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера
- SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке)
- Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык)

Документация:
- docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR
- docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API
- API_USAGE.md — список эндпоинтов backend
- README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/

.gitignore:
- public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 05:01:13 +03:00

170 lines
7.1 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.

/**
* GdFinish — финишные ворота + конфетти при пересечении (этап G5).
*
* Ставит выбранную фабрику ворот на FINISH_X (из cylinder-примитива или из
* самого правого ненулевого x уровня). Поворачивает поперёк трассы (Y=90°).
*
* При вызове .celebrate() запускает конфетти-партикль на 1.5с.
*
* Финиш-событие триггерится в скрипте уровня (по FINISH_X) — мы лишь рисуем
* ворота. Подключим celebrate из BabylonScene при gdFinish сообщении.
*/
import {
Vector3, TransformNode, ParticleSystem, Texture, DynamicTexture,
Color3, Color4, MeshBuilder, StandardMaterial,
} from '@babylonjs/core';
import { FINISH_CATALOG } from '../../admin-preview/gdFinishes/finishFactories';
const DEFAULT_FINISH_BY_EPOCH = {
1: 'f1_v4', // Рустик (выбор юзера)
2: 'f2_v5', // Бронзовая
3: 'f3_v3',
4: 'f4_v4',
5: 'f5_v2',
6: 'f6_v4',
7: 'f7_v1',
8: 'f8_v5',
9: 'f9_v4',
10: 'f10_v5',
};
export class GdFinish {
constructor() {
this.scene = null;
this._scene3d = null;
this._handle = null;
this._root = null;
this._particles = null;
this._particleEmitter = null;
this._celebrated = false;
}
attach(scene, scene3d, epoch = 1, finishId = null) {
if (!scene || !scene3d) return;
this.scene = scene;
this._scene3d = scene3d;
const id = finishId || DEFAULT_FINISH_BY_EPOCH[epoch] || 'f1_v1';
const factory = FINISH_CATALOG.find(f => f.id === id);
if (!factory) {
console.warn('[GdFinish] фабрика не найдена:', id);
return;
}
// Определяем FINISH_X — ищем cylinder-примитив (это стандартный gd_finish)
let finishX = null;
const pm = scene3d.primitiveManager;
if (pm) {
for (const data of pm.instances.values()) {
if (String(data.type) === 'cylinder' && typeof data.x === 'number' && data.x > 50) {
// самый правый cylinder = финиш
if (finishX == null || data.x > finishX) finishX = data.x;
}
}
}
if (finishX == null) {
console.warn('[GdFinish] не нашли cylinder-финиш, ставлю на x=1000');
finishX = 1000;
}
// Скрываем оригинальный cylinder если он рядом с финишем
if (pm) {
for (const data of pm.instances.values()) {
if (String(data.type) === 'cylinder' && Math.abs(data.x - finishX) < 2 && data.mesh) {
try { data.mesh.setEnabled(false); } catch (e) {}
this._hiddenFinishCylinder = data.mesh;
break;
}
}
}
const h = factory.make(scene, `gd_finish_inst`);
if (!h || !h.root) return;
this._handle = h;
this._root = h.root;
this._root.position = new Vector3(finishX, 1, 0);
this._root.rotation.y = Math.PI / 2; // поперёк трассы
this._finishX = finishX;
this._setupConfetti(finishX);
// Автоматически срабатывает когда куб игрока пересекает FINISH_X.
// Скрипт уровня сам показывает win-screen, мы только конфетти добавим.
this._onBeforeRender = () => {
if (this._celebrated) return;
const pp = this._scene3d?.player?._pos;
if (!pp) return;
if (pp.x >= this._finishX - 1.5) {
this.celebrate();
}
};
this.scene.onBeforeRenderObservable.add(this._onBeforeRender);
}
/** Создать ParticleSystem конфетти (старт по celebrate). */
_setupConfetti(finishX) {
// Маленький emitter-mesh — невидимая точка над финишем
const emitter = MeshBuilder.CreateBox('gd_confetti_emitter', { size: 0.1 }, this.scene);
emitter.position.set(finishX, 4, 0);
emitter.isVisible = false;
this._particleEmitter = emitter;
// Текстура частицы — квадратик canvas (просто белый круг)
const T = 32;
const dt = new DynamicTexture('gd_confetti_tex', { width: T, height: T }, this.scene, true);
const ctx = dt.getContext();
ctx.clearRect(0, 0, T, T);
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(T / 2, T / 2, T / 2 - 2, 0, Math.PI * 2);
ctx.fill();
dt.hasAlpha = true;
dt.update();
const ps = new ParticleSystem('gd_confetti', 400, this.scene);
ps.particleTexture = dt;
ps.emitter = emitter;
ps.minEmitBox = new Vector3(-1.5, 0, -1.5);
ps.maxEmitBox = new Vector3(1.5, 0, 1.5);
// Конфетти разных цветов
ps.color1 = new Color4(1, 0.3, 0.4, 1);
ps.color2 = new Color4(0.3, 0.7, 1, 1);
ps.colorDead = new Color4(1, 1, 1, 0);
ps.minSize = 0.15; ps.maxSize = 0.35;
ps.minLifeTime = 1.0; ps.maxLifeTime = 2.5;
ps.emitRate = 0; // пока выключен
ps.gravity = new Vector3(0, -8, 0);
ps.direction1 = new Vector3(-3, 5, -3);
ps.direction2 = new Vector3(3, 8, 3);
ps.minAngularSpeed = -3; ps.maxAngularSpeed = 3;
ps.minEmitPower = 1; ps.maxEmitPower = 3;
ps.updateSpeed = 0.02;
ps.start(); // активен но без эмиссии
this._particles = ps;
}
/** Включить конфетти на ~1.5 секунды. Вызывает уровень при пересечении финиша. */
celebrate() {
if (this._celebrated || !this._particles) return;
this._celebrated = true;
this._particles.emitRate = 250;
setTimeout(() => {
try { this._particles.emitRate = 0; } catch (e) {}
}, 1500);
}
dispose() {
try {
if (this._onBeforeRender && this.scene) {
this.scene.onBeforeRenderObservable.removeCallback(this._onBeforeRender);
this._onBeforeRender = null;
}
} catch (e) {}
if (this._handle && this._handle.dispose) try { this._handle.dispose(); } catch (e) {}
if (this._particles) try { this._particles.dispose(); } catch (e) {}
if (this._particleEmitter) try { this._particleEmitter.dispose(); } catch (e) {}
if (this._hiddenFinishCylinder) try { this._hiddenFinishCylinder.setEnabled(true); } catch (e) {}
this._handle = null;
this._root = null;
this._particles = null;
this._particleEmitter = null;
this._hiddenFinishCylinder = null;
this.scene = null;
this._scene3d = null;
}
}