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)
148 lines
6.4 KiB
JavaScript
148 lines
6.4 KiB
JavaScript
/**
|
||
* VoxelChunk — единица хранения и рендеринга воксельного мира.
|
||
*
|
||
* Один чанк — куб 32×32×32 ячеек (стандарт Minecraft).
|
||
* Внутри — Uint8Array(32768): индекс материала каждой ячейки в палитре слоя.
|
||
* 0 = пусто (воздух)
|
||
* 1..255 = индекс в palette[] слоя
|
||
*
|
||
* Чанк идентифицируется (cx, cy, cz) в chunk-space — целые числа.
|
||
* Мировые координаты ячейки внутри чанка:
|
||
* worldX = (cx * 32 + localX) * voxelSize
|
||
* где voxelSize — параметр слоя (0.25 для terrain, 0.05 для deco).
|
||
*
|
||
* Layout: linear-ordered по Y → Z → X для cache-friendly меширования
|
||
* (когда мы идём по слоям сверху вниз во время surface culling).
|
||
* index = localY * 32*32 + localZ * 32 + localX
|
||
*
|
||
* Production assumptions:
|
||
* - Чанк создаётся пустым (вся ячейка = 0)
|
||
* - Прямая модификация data — допустимо, но dirty-флаг надо ставить вручную
|
||
* - Когда чанк пустой (nonEmptyCount=0) — слой может удалить его из Map
|
||
*/
|
||
|
||
export const CHUNK_SIZE = 32;
|
||
export const CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 32768
|
||
|
||
export class VoxelChunk {
|
||
/**
|
||
* @param {number} cx - chunk X (целое)
|
||
* @param {number} cy - chunk Y
|
||
* @param {number} cz - chunk Z
|
||
*/
|
||
constructor(cx, cy, cz) {
|
||
this.cx = cx;
|
||
this.cy = cy;
|
||
this.cz = cz;
|
||
/** Uint8Array(32768) — material index каждой ячейки. */
|
||
this.data = new Uint8Array(CHUNK_VOLUME);
|
||
/** Сколько занятых (non-zero) ячеек. Поддерживается setLocal. */
|
||
this.nonEmptyCount = 0;
|
||
/** Нужно ли перестроить mesh при следующем frame'е. */
|
||
this.dirty = true;
|
||
/** Ссылка на собранный mesh (или Map<matIdx, Mesh>). Устанавливает
|
||
* ChunkMesher/Renderer; чанк сам не знает что это. */
|
||
this.meshRef = null;
|
||
/** Версия — инкрементится при каждой записи. Используется
|
||
* для дельта-синка в мультиплеере (Этап 8). */
|
||
this.version = 0;
|
||
}
|
||
|
||
/**
|
||
* Преобразовать локальные (x,y,z) в индекс data[].
|
||
* Координаты — 0..31. Без bounds check (для скорости — caller должен
|
||
* сам проверять, что в диапазоне).
|
||
*/
|
||
static localIndex(lx, ly, lz) {
|
||
return ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx;
|
||
}
|
||
|
||
/**
|
||
* Прочитать material index в локальной (lx,ly,lz). Возвращает 0 если
|
||
* вне границ чанка — caller обычно использует это для проверки соседей
|
||
* (если мы за границей чанка → воздух, или вызывающий должен сам
|
||
* проверять соседний чанк).
|
||
*/
|
||
getLocal(lx, ly, lz) {
|
||
if (lx < 0 || lx >= CHUNK_SIZE ||
|
||
ly < 0 || ly >= CHUNK_SIZE ||
|
||
lz < 0 || lz >= CHUNK_SIZE) {
|
||
return 0;
|
||
}
|
||
return this.data[VoxelChunk.localIndex(lx, ly, lz)];
|
||
}
|
||
|
||
/**
|
||
* Записать material index в локальную ячейку.
|
||
* matIdx = 0 — очистка (удаление voxel'а).
|
||
* Возвращает true если значение изменилось (нужна перестройка mesh'а).
|
||
*/
|
||
setLocal(lx, ly, lz, matIdx) {
|
||
if (lx < 0 || lx >= CHUNK_SIZE ||
|
||
ly < 0 || ly >= CHUNK_SIZE ||
|
||
lz < 0 || lz >= CHUNK_SIZE) {
|
||
return false;
|
||
}
|
||
const idx = VoxelChunk.localIndex(lx, ly, lz);
|
||
const old = this.data[idx];
|
||
if (old === matIdx) return false;
|
||
// Обновляем nonEmptyCount
|
||
if (old === 0 && matIdx !== 0) {
|
||
this.nonEmptyCount++;
|
||
} else if (old !== 0 && matIdx === 0) {
|
||
this.nonEmptyCount--;
|
||
}
|
||
this.data[idx] = matIdx;
|
||
this.dirty = true;
|
||
this.version++;
|
||
return true;
|
||
}
|
||
|
||
/** Полная очистка чанка. */
|
||
clear() {
|
||
if (this.nonEmptyCount === 0) return;
|
||
this.data.fill(0);
|
||
this.nonEmptyCount = 0;
|
||
this.dirty = true;
|
||
this.version++;
|
||
}
|
||
|
||
isEmpty() {
|
||
return this.nonEmptyCount === 0;
|
||
}
|
||
|
||
/**
|
||
* Координаты левого-нижнего-переднего угла чанка в мире, в воксельных
|
||
* (не мировых!) индексах. Используется при surface culling для
|
||
* вычисления глобальных координат воксел'а.
|
||
*/
|
||
voxelOriginX() { return this.cx * CHUNK_SIZE; }
|
||
voxelOriginY() { return this.cy * CHUNK_SIZE; }
|
||
voxelOriginZ() { return this.cz * CHUNK_SIZE; }
|
||
}
|
||
|
||
/**
|
||
* Хелпер: преобразовать мировые целочисленные voxel-координаты (gx,gy,gz)
|
||
* в (chunkKey, localX, localY, localZ).
|
||
* gx — это grid-индекс voxel'а (как сейчас в TerrainManager.voxels Map),
|
||
* не координата в метрах. Перевод в метры — отдельно: world = gx * voxelSize.
|
||
*/
|
||
export function voxelToChunk(gx, gy, gz) {
|
||
// Math.floor вместо >>0 — потому что отрицательные voxel-координаты
|
||
// должны идти в чанки -1, -2, ... а не в 0 (как делает побитовый shift).
|
||
const cx = Math.floor(gx / CHUNK_SIZE);
|
||
const cy = Math.floor(gy / CHUNK_SIZE);
|
||
const cz = Math.floor(gz / CHUNK_SIZE);
|
||
// Локальные координаты — всегда 0..31 (используем mod с защитой от
|
||
// отрицательных).
|
||
const lx = gx - cx * CHUNK_SIZE;
|
||
const ly = gy - cy * CHUNK_SIZE;
|
||
const lz = gz - cz * CHUNK_SIZE;
|
||
return { cx, cy, cz, lx, ly, lz };
|
||
}
|
||
|
||
/** Ключ чанка в Map (строка "cx,cy,cz"). */
|
||
export function chunkKey(cx, cy, cz) {
|
||
return `${cx},${cy},${cz}`;
|
||
}
|