МИН 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

215 lines
7.9 KiB
JavaScript
Raw Permalink 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.

/**
* DensityGrid — хранилище для Roblox-style smooth terrain.
*
* Содержит два параллельных массива одного размера:
* - matData[idx]: Uint8 — id материала (0=пусто, 1+=palette[i])
* - densityData[idx]: Uint8 — плотность 0..255 (0=пусто, 255=solid)
* Промежуточные значения позволяют **плавные** поверхности
* (Surface Nets интерполирует позицию вершины).
*
* Размер ячейки — 4м (как Roblox). Карта 500×128×500 м = 125×32×125 = 500K
* ячеек = 500 КБ matData + 500 КБ density = 1 МБ. Это в 200 раз компактнее
* чем voxel-terrain старой системы на той же площади.
*
* Палитра — общая с TERRAIN_MATERIALS (`grass`, `rock`, `sand`, ...).
*
* Сериализация — компактный RLE (matData отдельно от density).
*/
import { TERRAIN_MATERIALS } from '../TerrainManager';
/** Размер одной ячейки в метрах. Roblox использует 4м. */
export const CELL_SIZE = 4;
/** Threshold: ячейка считается solid если density >= 128 (0..255). */
export const DENSITY_THRESHOLD = 128;
export class DensityGrid {
/**
* @param {Object} opts
* @param {{x,y,z}} opts.origin — мировые координаты ячейки (0,0,0). Размер целочисленный (в cell-units, потом × CELL_SIZE = м).
* @param {{x,y,z}} opts.size — размер в ячейках
* @param {string[]} [opts.palette]
*/
constructor(opts) {
const { origin, size, palette } = opts;
if (!origin || !size) throw new Error('DensityGrid: origin and size required');
this.origin = { x: origin.x|0, y: origin.y|0, z: origin.z|0 };
this.size = { x: size.x|0, y: size.y|0, z: size.z|0 };
// Палитра по умолчанию: [пусто, и всё что есть в TERRAIN_MATERIALS]
this.palette = palette ? palette.slice() : ['', ...Object.keys(TERRAIN_MATERIALS)];
this._matIdByKey = new Map();
for (let i = 0; i < this.palette.length; i++) {
if (this.palette[i]) this._matIdByKey.set(this.palette[i], i);
}
// Strides для линейной индексации: data[z*sxy + y*sx + x]
this._sx = this.size.x;
this._sxy = this.size.x * this.size.y;
const total = this.size.x * this.size.y * this.size.z;
this.matData = new Uint8Array(total);
this.densityData = new Uint8Array(total);
}
/** Линейный индекс. */
_idx(ix, iy, iz) {
return iz * this._sxy + iy * this._sx + ix;
}
/** В пределах ли grid. */
inBounds(ix, iy, iz) {
return ix >= 0 && ix < this.size.x
&& iy >= 0 && iy < this.size.y
&& iz >= 0 && iz < this.size.z;
}
/** Получить density (0..255). Вне bounds → 0. */
getDensity(ix, iy, iz) {
if (!this.inBounds(ix, iy, iz)) return 0;
return this.densityData[this._idx(ix, iy, iz)];
}
/** Получить material-id (0=пусто). Вне bounds → 0. */
getMatId(ix, iy, iz) {
if (!this.inBounds(ix, iy, iz)) return 0;
return this.matData[this._idx(ix, iy, iz)];
}
/** Получить material-key ('grass' и т.п.) или ''. */
getMatKey(ix, iy, iz) {
const m = this.getMatId(ix, iy, iz);
return this.palette[m] || '';
}
/** Решение: считается ли ячейка "solid". */
isSolid(ix, iy, iz) {
return this.getDensity(ix, iy, iz) >= DENSITY_THRESHOLD;
}
/**
* Установить density + material для ячейки.
* @param {number} density 0..255
* @param {string|null} matKey
*/
set(ix, iy, iz, density, matKey) {
if (!this.inBounds(ix, iy, iz)) return;
const idx = this._idx(ix, iy, iz);
this.densityData[idx] = Math.max(0, Math.min(255, density|0));
if (matKey != null) {
let m = this._matIdByKey.get(matKey);
if (m === undefined) {
if (this.palette.length >= 255) return;
m = this.palette.length;
this.palette.push(matKey);
this._matIdByKey.set(matKey, m);
}
this.matData[idx] = m;
}
// Если density упал до 0 — обнуляем mat (логически empty).
if (this.densityData[idx] === 0) this.matData[idx] = 0;
}
/** Очистить полностью. */
clear() {
this.matData.fill(0);
this.densityData.fill(0);
}
/** Подсчёт solid-ячеек. */
countSolid() {
let n = 0;
for (let i = 0; i < this.densityData.length; i++) {
if (this.densityData[i] >= DENSITY_THRESHOLD) n++;
}
return n;
}
/** Преобразование cell-coord → world position center (метры). */
cellToWorld(ix, iy, iz) {
return {
x: (this.origin.x + ix + 0.5) * CELL_SIZE,
y: (this.origin.y + iy + 0.5) * CELL_SIZE,
z: (this.origin.z + iz + 0.5) * CELL_SIZE,
};
}
// =========================================================================
// СЕРИАЛИЗАЦИЯ — RLE
// =========================================================================
serialize() {
const matRLE = this._encodeRLE(this.matData);
const densityRLE = this._encodeRLE(this.densityData);
return {
format: 'robloxterrain-v1',
origin: this.origin,
size: this.size,
palette: this.palette,
mat: this._uint8ToBase64(matRLE),
density: this._uint8ToBase64(densityRLE),
};
}
_encodeRLE(arr) {
const buf = [];
const N = arr.length;
let i = 0;
while (i < N) {
const v = arr[i];
let run = 1;
while (i + run < N && arr[i + run] === v && run < 65535) run++;
buf.push(run & 0xff, (run >> 8) & 0xff, v);
i += run;
}
return new Uint8Array(buf);
}
static _decodeRLE(bytes, totalLength) {
const out = new Uint8Array(totalLength);
let outIdx = 0;
let i = 0;
while (i < bytes.length && outIdx < totalLength) {
const run = bytes[i] | (bytes[i + 1] << 8);
const v = bytes[i + 2];
i += 3;
for (let k = 0; k < run && outIdx < totalLength; k++) {
out[outIdx++] = v;
}
}
return out;
}
static deserialize(obj) {
if (!obj || obj.format !== 'robloxterrain-v1') {
throw new Error('DensityGrid: bad format ' + (obj?.format ?? 'undefined'));
}
const grid = new DensityGrid({
origin: obj.origin,
size: obj.size,
palette: obj.palette,
});
const matRLE = DensityGrid._base64ToUint8(obj.mat);
const densityRLE = DensityGrid._base64ToUint8(obj.density);
const total = grid.matData.length;
grid.matData = DensityGrid._decodeRLE(matRLE, total);
grid.densityData = DensityGrid._decodeRLE(densityRLE, total);
return grid;
}
_uint8ToBase64(arr) {
let binary = '';
const chunkSize = 8192;
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.subarray(i, Math.min(i + chunkSize, arr.length));
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
static _base64ToUint8(b64) {
const binary = atob(b64);
const out = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
return out;
}
}