Open-source web player for Rublox games, dual-licensed under AGPL-3.0 + Commercial. Highlights: - Babylon.js 7 + React 18 + Vite 5 stack - Self-contained engine (~46k lines): BlockManager, ModelManager, PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD gamemodes - Configurable backend via VITE_API_BASE and friends — works against staging (dev-api.rublox.pro) out of the box - Standalone mode (VITE_STANDALONE=true) loads a bundled sample game for first-run without any backend - Full docs: README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG - Lint + format scaffolding (ESLint + Prettier + EditorConfig) - Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md - Issue templates: bug_report, feature_request, security_disclosure Removed before public release: - frontend_deploy.py (contained production SSH credentials) - ~27 admin endpoints (kept in private repo) - Hard-coded internal URLs and IPs - All previous git history (clean repo init)
231 lines
9.8 KiB
JavaScript
231 lines
9.8 KiB
JavaScript
/**
|
||
* VoxelTreeBuilder — алгоритм генерации красивого voxel-дерева.
|
||
*
|
||
* Перенесён из WorldGenerator._placeTree (там используется для процедурной
|
||
* генерации мира). Здесь экспортирован как чистая функция, чтобы можно было
|
||
* звать из brush-инструмента "Деревья" в редакторе.
|
||
*
|
||
* Виды деревьев:
|
||
* - 'oak' — дуб: коричневый ствол, зелёная крона
|
||
* - 'birch' — берёза: белый тонкий ствол, листва с оранжевыми вкраплениями
|
||
* - 'autumn' — осеннее: коричневый ствол, ОРАНЖЕВАЯ листва
|
||
*
|
||
* Алгоритм:
|
||
* 1. Толстый ствол (offsets по thickness 0..3) с сужением к верху
|
||
* 2. Утолщение у основания (корни) для thickness >= 2
|
||
* 3. Зигзагообразные ветви — 2-8 шт., расходятся под углом
|
||
* 4. Главная крона (сфера) + крона на конце каждой ветви
|
||
*
|
||
* sizeScale: 0.3..5.0
|
||
* 0.3..0.8 → саженец (~3-5 voxel высота, thin ствол)
|
||
* 1.0 → стандарт (~8-12 voxel, средняя крона)
|
||
* 2.0 → большое (~20 voxel, толстый ствол 3×3, много ветвей)
|
||
* 5.0 → гигантское (~50 voxel, ствол 5×5)
|
||
*/
|
||
|
||
/** Простой 2D-hash, возвращает float ∈ [0..1). Детерминированный. */
|
||
function hash2(x, y, seed) {
|
||
let h = ((x | 0) * 73856093) ^ ((y | 0) * 19349663) ^ ((seed | 0) * 83492791);
|
||
h = (h ^ (h >>> 13)) * 1274126177;
|
||
return ((h ^ (h >>> 16)) >>> 0) / 0xffffffff;
|
||
}
|
||
|
||
/**
|
||
* Шаблон офсетов ствола.
|
||
* 0: один voxel
|
||
* 1: крест 5 voxel'ов (центр + 4 стороны)
|
||
* 2: полный 3×3 (9 voxel'ов)
|
||
* 3: 5×5 без 4-х угловых (21 voxel)
|
||
*/
|
||
function trunkOffsets(thickness) {
|
||
if (thickness <= 0) return [[0, 0]];
|
||
if (thickness === 1) return [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]];
|
||
if (thickness === 2) {
|
||
return [
|
||
[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1],
|
||
[1, 1], [-1, -1], [1, -1], [-1, 1],
|
||
];
|
||
}
|
||
const out = [];
|
||
for (let dx = -2; dx <= 2; dx++) {
|
||
for (let dz = -2; dz <= 2; dz++) {
|
||
if (Math.abs(dx) === 2 && Math.abs(dz) === 2) continue;
|
||
out.push([dx, dz]);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Сфера кроны с пятнистой раскраской и лохматыми краями.
|
||
*/
|
||
function placeCrown(setVoxelFn, cx, cy, cz, r, squish, placeLeaf, seed) {
|
||
const r2sq = r * r;
|
||
const innerR2 = r > 2 ? (r - 2) * (r - 2) : 0;
|
||
const yRange = Math.floor(r * squish) + 2;
|
||
for (let dx = -r - 1; dx <= r + 1; dx++) {
|
||
for (let dy = -Math.floor(r / 2); dy <= yRange; dy++) {
|
||
for (let dz = -r - 1; dz <= r + 1; dz++) {
|
||
const dyEff = dy / squish;
|
||
const d2 = dx * dx + dyEff * dyEff + dz * dz;
|
||
if (d2 > r2sq + 2) continue;
|
||
if (d2 < innerR2) continue;
|
||
const edgeThresh = r2sq - 1.5;
|
||
if (d2 > edgeThresh) {
|
||
if (hash2(cx + dx, cz + dz, seed + 3000 + dy) > 0.5) continue;
|
||
}
|
||
if (dx === 0 && dz === 0 && dy < 0) continue;
|
||
const colorNoise = hash2(cx + dx, cz + dz + dy * 11, seed + 3500);
|
||
placeLeaf(cx + dx, cy + dy, cz + dz, colorNoise);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Главная функция — сгенерировать дерево.
|
||
*
|
||
* @param {function(x,y,z,matId):void} setVoxelFn — колбэк постановки voxel
|
||
* @param {number} tx — voxel-x ствола
|
||
* @param {number} topY — voxel-y поверхности (1-я "земля" под деревом)
|
||
* @param {number} tz — voxel-z ствола
|
||
* @param {'oak'|'birch'|'autumn'} type
|
||
* @param {number} sizeScale — 0.3..5.0
|
||
* @param {number} seed — детерминизирует форму. Дай разные значения для разных деревьев.
|
||
*/
|
||
export function placeVoxelTree(setVoxelFn, tx, topY, tz, type, sizeScale = 1.0, seed = 12345) {
|
||
const r1 = hash2(tx, tz, seed + 2001);
|
||
const r2 = hash2(tx, tz, seed + 2002);
|
||
const r3 = hash2(tx, tz, seed + 2003);
|
||
const scale = Math.max(0.3, Math.min(5.0, sizeScale));
|
||
|
||
let primaryTrunk, secondaryTrunk;
|
||
let primaryLeaf, secondaryLeaf;
|
||
let secondaryLeafChance;
|
||
let trunkH, crownR, crownSquish;
|
||
let trunkThickness;
|
||
|
||
if (type === 'birch') {
|
||
primaryTrunk = 'trunk_white';
|
||
secondaryTrunk = 'trunk_white';
|
||
primaryLeaf = 'leaves';
|
||
secondaryLeaf = 'leaves_orange';
|
||
secondaryLeafChance = 0.10;
|
||
trunkH = Math.round((10 + r1 * 4) * scale);
|
||
crownR = Math.round(3 * scale);
|
||
crownSquish = 1.5;
|
||
trunkThickness = scale < 1.5 ? 0 : 1;
|
||
} else if (type === 'autumn') {
|
||
primaryTrunk = 'trunk';
|
||
secondaryTrunk = 'wood';
|
||
primaryLeaf = 'leaves_orange';
|
||
secondaryLeaf = 'leaves';
|
||
secondaryLeafChance = 0.25;
|
||
trunkH = Math.round((9 + r1 * 4) * scale);
|
||
crownR = Math.round((4 + r2 * 2) * scale);
|
||
crownSquish = 0.85;
|
||
trunkThickness = scale < 0.8 ? 0 : scale < 1.5 ? 1 : scale < 2.5 ? 2 : 3;
|
||
} else {
|
||
// oak (default)
|
||
primaryTrunk = 'trunk';
|
||
secondaryTrunk = 'wood';
|
||
primaryLeaf = 'leaves';
|
||
secondaryLeaf = 'leaves_orange';
|
||
secondaryLeafChance = 0.07;
|
||
trunkH = Math.round((8 + r1 * 4) * scale);
|
||
crownR = Math.round((3 + r2 * 2) * scale);
|
||
crownSquish = 1.0;
|
||
trunkThickness = scale < 0.8 ? 0 : scale < 1.5 ? 1 : scale < 2.5 ? 2 : 3;
|
||
}
|
||
if (trunkH < 3) trunkH = 3;
|
||
if (crownR < 2) crownR = 2;
|
||
|
||
const placeLeaf = (px, py, pz, noise) => {
|
||
const isSecondary = noise < secondaryLeafChance;
|
||
setVoxelFn(px, py, pz, isSecondary ? secondaryLeaf : primaryLeaf);
|
||
};
|
||
const placeTrunk = (px, py, pz, noise) => {
|
||
const isSecondary = (type !== 'birch') && noise < 0.05;
|
||
setVoxelFn(px, py, pz, isSecondary ? secondaryTrunk : primaryTrunk);
|
||
};
|
||
|
||
// Ствол с сужением
|
||
const taperStart = Math.floor(trunkH * 0.7);
|
||
for (let dy = 1; dy <= trunkH; dy++) {
|
||
const thicknessAtY = dy < taperStart
|
||
? trunkThickness
|
||
: Math.max(0, trunkThickness - Math.floor((dy - taperStart) * 0.5));
|
||
const offsets = trunkOffsets(thicknessAtY);
|
||
for (const [ox, oz] of offsets) {
|
||
const noise = hash2(tx + ox * 13, tz + oz * 17 + dy * 7, seed + 2100);
|
||
placeTrunk(tx + ox, topY + dy, tz + oz, noise);
|
||
}
|
||
}
|
||
|
||
// Корни у основания (thickness >= 2)
|
||
if (trunkThickness >= 2) {
|
||
const baseH = Math.min(3, Math.floor(trunkH / 5));
|
||
for (let dy = 1; dy <= baseH; dy++) {
|
||
for (const [ox, oz] of [[2, 0], [-2, 0], [0, 2], [0, -2], [1, 1], [-1, -1], [1, -1], [-1, 1]]) {
|
||
if (hash2(tx + ox * 13, tz + oz * 17 + dy, seed + 2200) > 0.55) continue;
|
||
placeTrunk(tx + ox, topY + dy, tz + oz, 0.5);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ветви — зигзаг
|
||
const branchTips = [];
|
||
const numBranches = type === 'birch'
|
||
? (scale < 1.5 ? 2 : 3)
|
||
: Math.max(2, Math.min(8, Math.round((3 + r3 * 2) * Math.sqrt(scale))));
|
||
|
||
const branchLenBase = Math.max(2, Math.floor(crownR * 0.8 + scale));
|
||
for (let b = 0; b < numBranches; b++) {
|
||
const baseAngle = (b / numBranches) * Math.PI * 2;
|
||
const angleJitter = (hash2(tx + b * 31, tz + b * 53, seed + 2300) - 0.5) * 0.6;
|
||
const angle = baseAngle + angleJitter + r1 * 0.5;
|
||
const startYFrac = 0.5 + (b / numBranches) * 0.45;
|
||
const branchStartY = Math.max(2, Math.floor(trunkH * startYFrac));
|
||
const branchLen = branchLenBase + Math.floor(hash2(tx + b, tz, seed + 2400) * 3);
|
||
|
||
let bx = tx, by = topY + branchStartY, bz = tz;
|
||
const dirX = Math.cos(angle);
|
||
const dirZ = Math.sin(angle);
|
||
for (let step = 1; step <= branchLen; step++) {
|
||
bx = Math.round(tx + dirX * step);
|
||
bz = Math.round(tz + dirZ * step);
|
||
const upBias = step * 0.4;
|
||
const jitterY = (hash2(bx * 7, bz * 11, seed + 2500 + step) - 0.3) * 1.5;
|
||
by = topY + branchStartY + Math.floor(upBias + jitterY);
|
||
|
||
const noise = hash2(bx, bz + step * 3, seed + 2310);
|
||
placeTrunk(bx, by, bz, noise);
|
||
|
||
if (trunkThickness >= 2 && step <= branchLen / 2) {
|
||
const sx = Math.round(bx - dirZ * 0.7);
|
||
const sz = Math.round(bz + dirX * 0.7);
|
||
if (sx !== bx || sz !== bz) {
|
||
placeTrunk(sx, by, sz, noise + 0.1);
|
||
}
|
||
}
|
||
}
|
||
branchTips.push({ x: bx, y: by, z: bz });
|
||
}
|
||
|
||
// Главная крона
|
||
const crownCy = topY + trunkH;
|
||
const mainCrownR = Math.max(2, Math.floor(crownR * 0.85));
|
||
placeCrown(setVoxelFn, tx, crownCy, tz, mainCrownR, crownSquish, placeLeaf, seed);
|
||
|
||
// Кроны на ветвях
|
||
const subCrownRBase = Math.max(2, Math.floor(crownR * 0.6));
|
||
for (const tip of branchTips) {
|
||
const sizeJitter = hash2(tip.x, tip.z, seed + 2600) * 0.4 - 0.2;
|
||
const subR = Math.max(2, Math.round(subCrownRBase * (1.0 + sizeJitter)));
|
||
placeCrown(setVoxelFn, tip.x, tip.y + 1, tip.z, subR, crownSquish, placeLeaf, seed);
|
||
}
|
||
}
|
||
|
||
/** Все доступные виды деревьев — для случайного выбора в plant-кисти. */
|
||
export const TREE_TYPES = ['oak', 'birch', 'autumn'];
|