player/src/engine/VoxelTreeBuilder.js
МИН 87444ee2c8 Initial public release: Rublox Player v1.0
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)
2026-05-27 23:04:04 +03:00

231 lines
9.8 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.

/**
* 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'];