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)
254 lines
8.3 KiB
JavaScript
254 lines
8.3 KiB
JavaScript
/**
|
||
* 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 };
|
||
}
|