studio/src/editor/utils/levelToScene.js
МИН 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

254 lines
8.3 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.

/**
* levelToScene — превращает LEVELS[] в готовые primitives + blocks для проекта.
*
* Используется при save из 2D-редактора. Логика идентична серверному
* `_p265_redesign.py` (тому что был одноразовым) — теперь это часть кода.
*
* Главный экспорт: buildSceneObjectsForLevels(levels, options).
* options.startId — стартовый id для генерируемых примитивов (по умолчанию 30000).
*
* Возвращает { primitives: [], blocks: [] }.
*/
let _idCounter = 0;
function makeIdAlloc(start) { _idCounter = start; return () => ++_idCounter; }
function basePrim(over) {
return Object.assign({
id: 0, x: 0, y: 0, z: 0,
sx: 1, sy: 1, sz: 1, rx: 0, ry: 0, rz: 0,
mass: 0, name: '', type: 'box', color: '#ffffff',
visible: true, material: 'plastic',
anchored: true, canCollide: false, folderId: null,
}, over || {});
}
function makeSpike(nid, x) {
return [basePrim({
id: nid(), type: 'cone', x, y: 1.35, z: 0,
sx: 0.9, sy: 1.2, sz: 0.9,
color: '#e94545', material: 'neon', name: 'spike',
})];
}
function makeSpikeTop(nid, x) {
return [basePrim({
id: nid(), type: 'cone', x, y: 6.0, z: 0,
sx: 0.9, sy: 1.2, sz: 0.9, rx: 180,
color: '#a44', material: 'neon', name: 'spike_top',
})];
}
function makeTramp(nid, x) {
return [
basePrim({ id: nid(), type: 'box', x, y: 1.01, z: 0, sx: 1.8, sy: 0.02, sz: 1.8,
color: '#ffcc00', material: 'neon', name: 'tramp_glow' }),
basePrim({ id: nid(), type: 'box', x, y: 1.15, z: 0, sx: 1.8, sy: 0.3, sz: 1.8,
color: '#ff9533', material: 'neon', name: 'tramp' }),
];
}
function makeSpeedPad(nid, x) {
return [
basePrim({ id: nid(), type: 'box', x, y: 1.01, z: 0, sx: 1.8, sy: 0.02, sz: 1.8,
color: '#33ddff', material: 'neon', name: 'speedpad_glow' }),
basePrim({ id: nid(), type: 'box', x, y: 1.10, z: 0, sx: 1.8, sy: 0.2, sz: 1.8,
color: '#55ccff', material: 'neon', name: 'speedpad' }),
];
}
function makeJumpOrb(nid, x, y) {
const innerId = nid();
return {
items: [
basePrim({ id: innerId, type: 'sphere', x, y, z: 0,
sx: 0.9, sy: 0.9, sz: 0.9,
color: '#ffe44a', material: 'neon', name: 'jumporb' }),
basePrim({ id: nid(), type: 'sphere', x, y, z: 0,
sx: 1.3, sy: 1.3, sz: 1.3,
color: '#ffaa00', material: 'neon', name: 'jumporb_glow' }),
],
primId: innerId,
};
}
function makeGravityFlip(nid, x) {
const pid = nid();
return {
items: [
basePrim({ id: pid, type: 'box', x, y: 3.5, z: 0, sx: 0.4, sy: 4.5, sz: 2.2,
color: '#b266ff', material: 'neon', name: 'gravity_portal' }),
basePrim({ id: nid(), type: 'box', x, y: 3.5, z: 0, sx: 0.45, sy: 4.7, sz: 0.5,
color: '#ffffff', material: 'neon', name: 'gravity_portal_glow' }),
],
primId: pid,
};
}
function makeMoving(nid, x, yMin) {
const pid = nid();
return {
items: [basePrim({
id: pid, type: 'box', x, y: yMin, z: 0, sx: 2.5, sy: 0.5, sz: 2,
color: '#22cc88', material: 'plastic', name: 'movplat',
canCollide: true, anchored: true,
})],
primId: pid,
};
}
function makeStep(nid, x, y, sx = 2, sy = 0.5) {
return [basePrim({
id: nid(), type: 'box', x, y, z: 0, sx, sy, sz: 2,
color: '#5a8aff', material: 'plastic', name: 'step',
canCollide: true, anchored: true,
})];
}
function makeCeilingBlock(nid, x, y, length) {
return [basePrim({
id: nid(), type: 'box', x, y, z: 0, sx: length, sy: 1, sz: 3,
color: '#446', material: 'plastic', name: 'ceiling',
canCollide: true, anchored: true,
})];
}
/** Сгенерировать воксельный пол для уровня (cotton-green верх + 4 stone). */
function addFloorBlocks(blocks, sx, finishX, holes) {
const inHole = (col) => {
for (const [a, b] of holes) if (col >= a && col < b) return true;
return false;
};
let count = 0;
for (let dx = 0; dx <= (finishX - sx) + 4; dx++) {
if (inHole(dx)) continue;
const wx = sx + dx;
for (const z of [-1, 0, 1]) {
blocks.push({ x: wx, y: 0, z, mass: 1, type: 'cotton-green',
visible: true, anchored: true, folderId: null, canCollide: true });
for (const y of [-1, -2, -3, -4]) {
blocks.push({ x: wx, y, z, mass: 1, type: 'stone',
visible: true, anchored: true, folderId: null, canCollide: true });
}
count += 5;
}
}
return count;
}
/**
* Главная функция. Принимает массив уровней, возвращает примитивы и блоки.
* Не трогает декор (z=13/21/22/35/50) — это отдельная функция (см. ниже).
*/
export function buildSceneObjectsForLevels(levels, options = {}) {
const nid = makeIdAlloc(options.startId || 30000);
const primitives = [];
const blocks = [];
const updatedLevels = [];
for (const L of levels) {
const sx = L.spawnX || 0;
const width = L.width || 240;
const finishX = (L.finishX != null) ? L.finishX : (sx + width - 5);
const holes = L.holes || [];
// ПОЛ
addFloorBlocks(blocks, sx, finishX, holes);
// СТУПЕНЬКИ
for (const st of (L.steps || [])) {
primitives.push(...makeStep(nid, st.x, st.y, st.sx || 2, st.sy || 0.5));
}
// ШИПЫ
for (const dx of (L.spikes || [])) {
primitives.push(...makeSpike(nid, sx + dx));
}
for (const dx of (L.spikesTop || [])) {
primitives.push(...makeSpikeTop(nid, sx + dx));
}
// ТРАМПЛИНЫ
const trampsForScript = [];
for (const t of (L.tramps || [])) {
const rel = typeof t === 'number' ? t : (t.x != null ? t.x - sx : 0);
trampsForScript.push(rel);
primitives.push(...makeTramp(nid, sx + rel));
}
// SPEED PADS
const padsForScript = [];
for (const p of (L.speedPads || [])) {
const rel = typeof p === 'number' ? p : (p.x != null ? p.x - sx : 0);
padsForScript.push(rel);
primitives.push(...makeSpeedPad(nid, sx + rel));
}
// JUMP ORBS
const orbsForScript = [];
for (const orb of (L.jumpOrbs || [])) {
const wx = orb.worldX != null ? orb.worldX : sx + (orb.x || 0);
const wy = orb.worldY != null ? orb.worldY : (orb.y || 3.5);
const { items, primId } = makeJumpOrb(nid, wx, wy);
primitives.push(...items);
orbsForScript.push({ primId, worldX: wx, worldY: wy });
}
// GRAVITY FLIPS
const flipsForScript = [];
for (const gf of (L.gravityFlips || [])) {
const wx = gf.worldX != null ? gf.worldX : sx + (gf.x || 0);
const { items, primId } = makeGravityFlip(nid, wx);
primitives.push(...items);
flipsForScript.push({ primId, worldX: wx });
}
// CEILINGS (отрезки 4-блоков)
for (const cl of (L.ceilings || [])) {
const wx0 = cl.worldX != null ? cl.worldX : sx + (cl.x || 0);
const len = cl.length || 4;
const ceilingY = L.ceilingY || 7;
for (let i = 0; i < len; i += 4) {
primitives.push(...makeCeilingBlock(nid, wx0 + i + 2, ceilingY, 4));
}
}
// MOVING PLATFORMS
const movingForScript = [];
for (const mp of (L.movingPlatforms || [])) {
const wx = mp.worldX != null ? mp.worldX : sx + (mp.x || 0);
const yMin = mp.yMin != null ? mp.yMin : 1.0;
const yMax = mp.yMax != null ? mp.yMax : 2.5;
const period = mp.period != null ? mp.period : 3.0;
const { items, primId } = makeMoving(nid, wx, yMin);
primitives.push(...items);
movingForScript.push({
primId, worldX: wx, yMin, yMax, period,
phase: mp.phase != null ? mp.phase : Math.random() * 6.28,
});
}
updatedLevels.push({
id: L.id,
name: L.name || `Уровень ${L.id}`,
width,
spawnX: sx,
finishX,
holes,
spikes: L.spikes || [],
spikesTop: L.spikesTop || [],
tramps: trampsForScript,
speedPads: padsForScript,
movingPlatforms: movingForScript,
jumpOrbs: orbsForScript,
gravityFlips: flipsForScript,
ceilings: L.ceilings || [],
steps: L.steps || [],
doubleJump: !!L.doubleJump,
ceilingY: L.ceilingY || null,
});
}
return { primitives, blocks, updatedLevels };
}