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)
536 lines
27 KiB
JavaScript
536 lines
27 KiB
JavaScript
/**
|
||
* archFactories — реестр стартовых арок GD по эпохам.
|
||
*
|
||
* 10 эпох × 5 вариантов = 50 арок. Каждая фабрика строит арку:
|
||
* - две вертикальные «колонны» (cylinder/box) высотой ~4м.
|
||
* - горизонтальная «балка» сверху.
|
||
* - надпись «СТАРТ» (DynamicTexture на plane) посередине балки.
|
||
* - подсветка/glow в стиле эпохи.
|
||
*
|
||
* Возвращает (scene, id) => { root: TransformNode, dispose() }.
|
||
* Габариты: ширина ~3.5м, высота ~4м, центр в (0, 0, 0).
|
||
*/
|
||
import {
|
||
MeshBuilder, StandardMaterial, Color3, Vector3, TransformNode,
|
||
DynamicTexture,
|
||
} from '@babylonjs/core';
|
||
|
||
// =========================================================================
|
||
// УТИЛИТЫ
|
||
// =========================================================================
|
||
|
||
function makeMat(scene, name, opts = {}) {
|
||
const m = new StandardMaterial(name, scene);
|
||
m.diffuseColor = new Color3(...(opts.diffuse || [1, 1, 1]));
|
||
m.emissiveColor = new Color3(...(opts.emissive || [0, 0, 0]));
|
||
m.specularColor = new Color3(...(opts.specular || [0.2, 0.2, 0.2]));
|
||
if (opts.specPower != null) m.specularPower = opts.specPower;
|
||
if (opts.alpha != null) m.alpha = opts.alpha;
|
||
if (opts.disableLighting) m.disableLighting = true;
|
||
return m;
|
||
}
|
||
|
||
function makeColumn(scene, name, opts = {}) {
|
||
const {
|
||
height = 3.5, diameter = 0.5,
|
||
shape = 'cylinder', // 'cylinder' | 'box'
|
||
diffuse = [0.4, 0.3, 0.2], emissive = [0.05, 0.04, 0.03],
|
||
specular, specPower,
|
||
} = opts;
|
||
let mesh;
|
||
if (shape === 'box') {
|
||
mesh = MeshBuilder.CreateBox(name, { width: diameter, height, depth: diameter }, scene);
|
||
} else {
|
||
mesh = MeshBuilder.CreateCylinder(name, {
|
||
diameter, height, tessellation: 12,
|
||
}, scene);
|
||
}
|
||
mesh.position.y = height / 2;
|
||
mesh.material = makeMat(scene, `${name}_mat`, { diffuse, emissive, specular, specPower });
|
||
return mesh;
|
||
}
|
||
|
||
function makeBeam(scene, name, opts = {}) {
|
||
const {
|
||
width = 3.5, height = 0.5, depth = 0.5,
|
||
diffuse = [0.4, 0.3, 0.2], emissive = [0.05, 0.04, 0.03],
|
||
specular, specPower,
|
||
} = opts;
|
||
const beam = MeshBuilder.CreateBox(name, { width, height, depth }, scene);
|
||
beam.material = makeMat(scene, `${name}_mat`, { diffuse, emissive, specular, specPower });
|
||
return beam;
|
||
}
|
||
|
||
/** Табличка с надписью «СТАРТ» (DynamicTexture на plane). */
|
||
function makeSignText(scene, name, text, opts = {}) {
|
||
const {
|
||
width = 2.4, height = 0.7,
|
||
bgColor = '#0a1428', textColor = '#22ff66',
|
||
fontSize = 96, fontFamily = 'sans-serif',
|
||
emissive = [0.2, 1.0, 0.4],
|
||
} = opts;
|
||
const TW = 512, TH = 128;
|
||
const dt = new DynamicTexture(`${name}_tex`, { width: TW, height: TH }, scene, true);
|
||
const ctx = dt.getContext();
|
||
ctx.fillStyle = bgColor;
|
||
ctx.fillRect(0, 0, TW, TH);
|
||
ctx.fillStyle = textColor;
|
||
ctx.font = `bold ${fontSize}px ${fontFamily}`;
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(text, TW / 2, TH / 2);
|
||
dt.hasAlpha = false;
|
||
dt.update();
|
||
|
||
const plane = MeshBuilder.CreatePlane(name, { width, height }, scene);
|
||
const mat = new StandardMaterial(`${name}_mat`, scene);
|
||
mat.diffuseTexture = dt;
|
||
mat.emissiveTexture = dt;
|
||
mat.emissiveColor = new Color3(...emissive);
|
||
mat.disableLighting = true;
|
||
mat.backFaceCulling = false;
|
||
plane.material = mat;
|
||
return plane;
|
||
}
|
||
|
||
/** Сферическая «лампочка»-glow (для гирлянд/неона). */
|
||
function makeBulb(scene, name, x, y, z, color, scale = 1) {
|
||
const bulb = MeshBuilder.CreateSphere(name, { diameter: 0.18, segments: 8 }, scene);
|
||
bulb.position.set(x, y, z);
|
||
bulb.scaling.set(scale, scale, scale);
|
||
const mat = makeMat(scene, `${name}_mat`, { diffuse: color, emissive: color, disableLighting: true });
|
||
bulb.material = mat;
|
||
return bulb;
|
||
}
|
||
|
||
// =========================================================================
|
||
// ФАБРИКИ АРОК ПО ЭПОХАМ (по 5 на эпоху)
|
||
// =========================================================================
|
||
|
||
// ----------------------------------------- Эпоха I — Лес -----
|
||
function archForestWood(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.6, GAP = 3.0;
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.55,
|
||
diffuse: [0.40, 0.25, 0.13], emissive: [0.10, 0.07, 0.04] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.55,
|
||
diffuse: [0.40, 0.25, 0.13], emissive: [0.10, 0.07, 0.04] });
|
||
colL.position.x = -GAP / 2;
|
||
colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, {
|
||
width: GAP + 0.7, height: 0.45, depth: 0.45,
|
||
diffuse: [0.45, 0.28, 0.15], emissive: [0.10, 0.07, 0.04],
|
||
});
|
||
beam.position.y = COL_H + 0.225;
|
||
beam.parent = root;
|
||
// листва — зелёная сферка над балкой
|
||
for (let i = 0; i < 5; i++) {
|
||
const leaf = MeshBuilder.CreateSphere(`${id}_leaf_${i}`, { diameter: 0.7 }, scene);
|
||
leaf.position.set(-1.4 + i * 0.7, COL_H + 0.7, 0);
|
||
const mat = makeMat(scene, `${id}_leaf_mat_${i}`, {
|
||
diffuse: [0.2, 0.55, 0.25], emissive: [0.06, 0.18, 0.08],
|
||
});
|
||
leaf.material = mat;
|
||
leaf.parent = root;
|
||
}
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', {
|
||
width: 2.4, height: 0.7, textColor: '#22ff66',
|
||
});
|
||
sign.position.set(0, COL_H - 0.3, 0.26);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archForestStone(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.4, GAP = 3.0;
|
||
const stoneDif = [0.55, 0.55, 0.50];
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.7, shape: 'box',
|
||
diffuse: stoneDif, emissive: [0.10, 0.10, 0.09] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.7, shape: 'box',
|
||
diffuse: stoneDif, emissive: [0.10, 0.10, 0.09] });
|
||
colL.position.x = -GAP / 2;
|
||
colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, {
|
||
width: GAP + 0.9, height: 0.55, depth: 0.55,
|
||
diffuse: stoneDif, emissive: [0.10, 0.10, 0.09],
|
||
});
|
||
beam.position.y = COL_H + 0.275;
|
||
beam.parent = root;
|
||
// мох сверху
|
||
for (let i = 0; i < 7; i++) {
|
||
const moss = MeshBuilder.CreateSphere(`${id}_moss_${i}`, { diameter: 0.35 }, scene);
|
||
moss.position.set(-1.7 + i * 0.55, COL_H + 0.55, 0);
|
||
moss.material = makeMat(scene, `${id}_moss_mat_${i}`,
|
||
{ diffuse: [0.25, 0.5, 0.20], emissive: [0.07, 0.15, 0.05] });
|
||
moss.parent = root;
|
||
}
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#88dd55' });
|
||
sign.position.set(0, COL_H - 0.3, 0.31);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archForestVine(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.6, GAP = 3.0;
|
||
// тонкие колонны-ветки
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.35,
|
||
diffuse: [0.32, 0.22, 0.12], emissive: [0.08, 0.05, 0.03] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.35,
|
||
diffuse: [0.32, 0.22, 0.12], emissive: [0.08, 0.05, 0.03] });
|
||
colL.position.x = -GAP / 2;
|
||
colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
// тонкая балка
|
||
const beam = makeBeam(scene, `${id}_beam`, {
|
||
width: GAP + 0.4, height: 0.25, depth: 0.25,
|
||
diffuse: [0.32, 0.22, 0.12], emissive: [0.08, 0.05, 0.03],
|
||
});
|
||
beam.position.y = COL_H + 0.125;
|
||
beam.parent = root;
|
||
// лианы — много зелёных мелких сфер
|
||
for (let i = 0; i < 12; i++) {
|
||
const x = -1.8 + i * 0.32;
|
||
const dropY = 0.5 + Math.abs(Math.sin(i * 1.3)) * 0.8;
|
||
for (let j = 0; j < 4; j++) {
|
||
const leaf = MeshBuilder.CreateSphere(`${id}_v_${i}_${j}`, { diameter: 0.25 }, scene);
|
||
leaf.position.set(x, COL_H + 0.2 - j * 0.25, 0);
|
||
leaf.material = makeMat(scene, `${id}_vm_${i}_${j}`,
|
||
{ diffuse: [0.15 + (j % 2) * 0.1, 0.55 - j * 0.05, 0.15], emissive: [0.05, 0.18, 0.05] });
|
||
leaf.parent = root;
|
||
}
|
||
}
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#aaff66' });
|
||
sign.position.set(0, COL_H - 0.5, 0.16);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archForestLanterns(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.6, GAP = 3.0;
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.50,
|
||
diffuse: [0.30, 0.18, 0.08], emissive: [0.06, 0.04, 0.02] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.50,
|
||
diffuse: [0.30, 0.18, 0.08], emissive: [0.06, 0.04, 0.02] });
|
||
colL.position.x = -GAP / 2;
|
||
colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, {
|
||
width: GAP + 0.6, height: 0.35, depth: 0.35,
|
||
diffuse: [0.30, 0.18, 0.08], emissive: [0.06, 0.04, 0.02],
|
||
});
|
||
beam.position.y = COL_H + 0.175;
|
||
beam.parent = root;
|
||
// тёплые лампочки-гирлянда
|
||
const colors = [[1.0, 0.85, 0.30], [1.0, 0.55, 0.20], [1.0, 0.95, 0.65]];
|
||
for (let i = 0; i < 9; i++) {
|
||
const bulb = makeBulb(scene, `${id}_b_${i}`,
|
||
-1.6 + i * 0.4,
|
||
COL_H + 0.18 - Math.abs(Math.sin(i * 1.7)) * 0.3,
|
||
0,
|
||
colors[i % colors.length],
|
||
0.9
|
||
);
|
||
bulb.parent = root;
|
||
}
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', {
|
||
textColor: '#ffd76b', bgColor: '#1f1408', emissive: [0.7, 0.55, 0.15],
|
||
});
|
||
sign.position.set(0, COL_H - 0.4, 0.21);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archForestRustic(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.5, GAP = 2.8;
|
||
// кривые колонны (две box со слегка разным масштабом)
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.6, shape: 'box',
|
||
diffuse: [0.42, 0.28, 0.16], emissive: [0.10, 0.07, 0.04] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.6, shape: 'box',
|
||
diffuse: [0.42, 0.28, 0.16], emissive: [0.10, 0.07, 0.04] });
|
||
colL.position.x = -GAP / 2;
|
||
colL.rotation.z = -0.05;
|
||
colR.position.x = GAP / 2;
|
||
colR.rotation.z = 0.05;
|
||
colL.parent = root; colR.parent = root;
|
||
// балка — две, на разной высоте (как ферма)
|
||
const beam1 = makeBeam(scene, `${id}_beam1`, {
|
||
width: GAP + 0.5, height: 0.35, depth: 0.35,
|
||
diffuse: [0.45, 0.30, 0.18], emissive: [0.10, 0.07, 0.04],
|
||
});
|
||
beam1.position.y = COL_H + 0.175;
|
||
beam1.parent = root;
|
||
const beam2 = makeBeam(scene, `${id}_beam2`, {
|
||
width: GAP, height: 0.25, depth: 0.25,
|
||
diffuse: [0.45, 0.30, 0.18], emissive: [0.10, 0.07, 0.04],
|
||
});
|
||
beam2.position.y = COL_H - 0.5;
|
||
beam2.parent = root;
|
||
// X-перекрестье из досок
|
||
const dr1 = makeBeam(scene, `${id}_dr1`, {
|
||
width: 2.6, height: 0.18, depth: 0.18,
|
||
diffuse: [0.40, 0.25, 0.13], emissive: [0.08, 0.05, 0.03],
|
||
});
|
||
dr1.position.y = COL_H * 0.5;
|
||
dr1.rotation.z = Math.PI / 5;
|
||
dr1.parent = root;
|
||
const dr2 = dr1.clone(`${id}_dr2`);
|
||
dr2.rotation.z = -Math.PI / 5;
|
||
dr2.parent = root;
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#ffe44a' });
|
||
sign.position.set(0, COL_H + 0.5, 0.26);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
// ----------------------------------------- Эпоха II — Горы -----
|
||
function archMountainStone(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.4, GAP = 3.0;
|
||
const colDif = [0.50, 0.50, 0.55];
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.75, shape: 'box', diffuse: colDif, emissive: [0.10, 0.10, 0.11] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.75, shape: 'box', diffuse: colDif, emissive: [0.10, 0.10, 0.11] });
|
||
colL.position.x = -GAP / 2; colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, { width: GAP + 1.0, height: 0.55, depth: 0.55, diffuse: colDif, emissive: [0.10, 0.10, 0.11] });
|
||
beam.position.y = COL_H + 0.275; beam.parent = root;
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#aaccff' });
|
||
sign.position.set(0, COL_H - 0.3, 0.31);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archMountainCrystal(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.6, GAP = 3.0;
|
||
const xtaldif = [0.5, 0.7, 0.95], xtalem = [0.20, 0.35, 0.55];
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.55, diffuse: xtaldif, emissive: xtalem, specPower: 96 });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.55, diffuse: xtaldif, emissive: xtalem, specPower: 96 });
|
||
colL.position.x = -GAP / 2; colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, { width: GAP + 0.6, height: 0.35, depth: 0.35, diffuse: xtaldif, emissive: xtalem });
|
||
beam.position.y = COL_H + 0.175; beam.parent = root;
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#aaeeff' });
|
||
sign.position.set(0, COL_H - 0.4, 0.21);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archMountainIce(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.5, GAP = 3.0;
|
||
const icedif = [0.80, 0.92, 1.0], iceem = [0.25, 0.35, 0.50];
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.55, diffuse: icedif, emissive: iceem, specPower: 128 });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.55, diffuse: icedif, emissive: iceem, specPower: 128 });
|
||
colL.position.x = -GAP / 2; colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, { width: GAP + 0.6, height: 0.35, depth: 0.35, diffuse: icedif, emissive: iceem });
|
||
beam.position.y = COL_H + 0.175; beam.parent = root;
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#ddffff' });
|
||
sign.position.set(0, COL_H - 0.4, 0.21);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archMountainPeak(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.8, GAP = 3.0;
|
||
const colDif = [0.40, 0.45, 0.50];
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.6, shape: 'box', diffuse: colDif, emissive: [0.05, 0.06, 0.07] });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.6, shape: 'box', diffuse: colDif, emissive: [0.05, 0.06, 0.07] });
|
||
colL.position.x = -GAP / 2; colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
// вместо балки — пирамидальная крыша из 2-х наклонённых box
|
||
const sideA = makeBeam(scene, `${id}_a`, { width: GAP * 0.7, height: 0.4, depth: 0.4, diffuse: colDif, emissive: [0.05, 0.06, 0.07] });
|
||
sideA.position.set(-GAP * 0.27, COL_H + GAP * 0.27, 0);
|
||
sideA.rotation.z = -Math.PI / 4;
|
||
sideA.parent = root;
|
||
const sideB = sideA.clone(`${id}_b`);
|
||
sideB.position.set(GAP * 0.27, COL_H + GAP * 0.27, 0);
|
||
sideB.rotation.z = Math.PI / 4;
|
||
sideB.parent = root;
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#88aacc' });
|
||
sign.position.set(0, COL_H - 0.6, 0.31);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
function archMountainBronze(scene, id) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = 3.6, GAP = 3.0;
|
||
const brz = [0.55, 0.38, 0.18];
|
||
const colL = makeColumn(scene, `${id}_colL`, { height: COL_H, diameter: 0.5, diffuse: brz, emissive: [0.15, 0.08, 0.03], specPower: 96 });
|
||
const colR = makeColumn(scene, `${id}_colR`, { height: COL_H, diameter: 0.5, diffuse: brz, emissive: [0.15, 0.08, 0.03], specPower: 96 });
|
||
colL.position.x = -GAP / 2; colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, { width: GAP + 0.8, height: 0.45, depth: 0.45, diffuse: brz, emissive: [0.15, 0.08, 0.03] });
|
||
beam.position.y = COL_H + 0.225; beam.parent = root;
|
||
const sign = makeSignText(scene, `${id}_sign`, 'СТАРТ', { textColor: '#ffd76b' });
|
||
sign.position.set(0, COL_H - 0.3, 0.26);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
// ----------------------------------------- Эпохи III-X — упрощённые шаблоны -----
|
||
function archGeneric(scene, id, palette) {
|
||
const root = new TransformNode(`arch_${id}`, scene);
|
||
const COL_H = palette.colH || 3.5, GAP = palette.gap || 3.0;
|
||
const colL = makeColumn(scene, `${id}_colL`, {
|
||
height: COL_H, diameter: palette.colDia || 0.55,
|
||
shape: palette.shape || 'cylinder',
|
||
diffuse: palette.colDif, emissive: palette.colEm,
|
||
specPower: palette.specPower,
|
||
});
|
||
const colR = makeColumn(scene, `${id}_colR`, {
|
||
height: COL_H, diameter: palette.colDia || 0.55,
|
||
shape: palette.shape || 'cylinder',
|
||
diffuse: palette.colDif, emissive: palette.colEm,
|
||
specPower: palette.specPower,
|
||
});
|
||
colL.position.x = -GAP / 2; colR.position.x = GAP / 2;
|
||
colL.parent = root; colR.parent = root;
|
||
const beam = makeBeam(scene, `${id}_beam`, {
|
||
width: GAP + 0.6, height: palette.beamH || 0.35, depth: 0.35,
|
||
diffuse: palette.beamDif || palette.colDif, emissive: palette.beamEm || palette.colEm,
|
||
});
|
||
beam.position.y = COL_H + (palette.beamH || 0.35) / 2;
|
||
beam.parent = root;
|
||
// бонус — например лампочки или сферки
|
||
if (palette.bulbs) {
|
||
for (let i = 0; i < palette.bulbs.count; i++) {
|
||
const b = makeBulb(scene, `${id}_bulb_${i}`,
|
||
-GAP / 2 + (i + 0.5) * (GAP / palette.bulbs.count),
|
||
COL_H + 0.5,
|
||
0,
|
||
palette.bulbs.color
|
||
);
|
||
b.parent = root;
|
||
}
|
||
}
|
||
const sign = makeSignText(scene, `${id}_sign`, palette.signText || 'СТАРТ', {
|
||
textColor: palette.signColor || '#ffffff',
|
||
bgColor: palette.signBg || '#0a1428',
|
||
emissive: palette.signEm || [0.5, 0.5, 0.5],
|
||
});
|
||
sign.position.set(0, COL_H - 0.4, 0.21);
|
||
sign.parent = root;
|
||
return { root, dispose: () => { for (const ch of root.getChildMeshes()) ch.dispose(); root.dispose(); } };
|
||
}
|
||
|
||
// =========================================================================
|
||
// КАТАЛОГ
|
||
// =========================================================================
|
||
|
||
const GENERIC_EPOCHS = {
|
||
3: [
|
||
{ name: 'Город хром', colDif:[0.65,0.65,0.7], colEm:[0.15,0.15,0.18], signColor:'#22ccff', specPower:128 },
|
||
{ name: 'Город неон', colDif:[0.10,0.10,0.20], colEm:[0.10,0.30,0.60], signColor:'#22ddff', signEm:[0.1,0.6,1.0] },
|
||
{ name: 'Город бетон', colDif:[0.55,0.55,0.55], colEm:[0.10,0.10,0.10], signColor:'#cccccc' },
|
||
{ name: 'Граффити', colDif:[0.20,0.20,0.25], colEm:[0.05,0.05,0.07], signColor:'#ff44aa', signBg:'#2a0a1a', signEm:[0.8,0.2,0.5] },
|
||
{ name: 'Город жёлтый', colDif:[0.85,0.65,0.10], colEm:[0.30,0.20,0.05], signColor:'#000', signBg:'#ffe44a', signEm:[1,0.85,0.1] },
|
||
],
|
||
4: [
|
||
{ name: 'Ночной фиолет', colDif:[0.35,0.10,0.50], colEm:[0.35,0.10,0.60], signColor:'#ff88ff', signEm:[0.7,0.3,0.9] },
|
||
{ name: 'Ночной циан', colDif:[0.10,0.40,0.50], colEm:[0.10,0.50,0.65], signColor:'#88eeff', signEm:[0.3,0.8,1.0] },
|
||
{ name: 'Ночной розовый', colDif:[0.80,0.20,0.45], colEm:[0.55,0.15,0.30], signColor:'#ff88aa', signEm:[1.0,0.4,0.7] },
|
||
{ name: 'Ночной жёлтый', colDif:[0.85,0.65,0.10], colEm:[0.55,0.40,0.05], signColor:'#ffee88', signEm:[1,0.85,0.2] },
|
||
{ name: 'Голограмма', colDif:[0.30,0.50,1.0], colEm:[0.40,0.70,1.0], signColor:'#ffffff', signEm:[0.6,0.8,1.0] },
|
||
],
|
||
5: [
|
||
{ name: 'Пустыня песчаник', colDif:[0.75,0.62,0.38], colEm:[0.18,0.13,0.07], signColor:'#5a3a18' },
|
||
{ name: 'Пустыня терракота', colDif:[0.65,0.30,0.15], colEm:[0.20,0.07,0.03], signColor:'#ffd76b' },
|
||
{ name: 'Пустыня кость', colDif:[0.85,0.80,0.65], colEm:[0.25,0.20,0.12], signColor:'#3a2a1a' },
|
||
{ name: 'Пустыня кактус', colDif:[0.30,0.50,0.25], colEm:[0.07,0.15,0.07], signColor:'#ffffff' },
|
||
{ name: 'Пустыня закат', colDif:[0.85,0.40,0.20], colEm:[0.45,0.15,0.05], signColor:'#ffe44a', signEm:[1,0.5,0.1] },
|
||
],
|
||
6: [
|
||
{ name: 'Океан коралл', colDif:[0.90,0.40,0.50], colEm:[0.30,0.10,0.15], signColor:'#ffffff' },
|
||
{ name: 'Океан синий', colDif:[0.10,0.40,0.65], colEm:[0.05,0.20,0.35], signColor:'#aaeeff' },
|
||
{ name: 'Океан жемчуг', colDif:[0.85,0.85,0.95], colEm:[0.20,0.20,0.30], signColor:'#3a5a7a', specPower:128 },
|
||
{ name: 'Океан водоросль', colDif:[0.20,0.50,0.30], colEm:[0.07,0.20,0.10], signColor:'#aaffaa' },
|
||
{ name: 'Океан бирюза', colDif:[0.10,0.65,0.65], colEm:[0.10,0.40,0.40], signColor:'#ffffff' },
|
||
],
|
||
7: [
|
||
{ name: 'Пещеры камень', colDif:[0.35,0.30,0.25], colEm:[0.07,0.05,0.04], signColor:'#aa8855' },
|
||
{ name: 'Пещеры кварц', colDif:[0.55,0.45,0.75], colEm:[0.20,0.15,0.40], signColor:'#ddccff' },
|
||
{ name: 'Пещеры гриб', colDif:[0.55,0.20,0.20], colEm:[0.25,0.08,0.08], signColor:'#ffffff' },
|
||
{ name: 'Пещеры мох', colDif:[0.20,0.50,0.30], colEm:[0.20,0.55,0.30], signColor:'#aaff88', signEm:[0.3,0.8,0.4] },
|
||
{ name: 'Пещеры алмаз', colDif:[0.75,0.85,0.95], colEm:[0.35,0.40,0.45], signColor:'#ffffff', specPower:128 },
|
||
],
|
||
8: [
|
||
{ name: 'Вулкан лава', colDif:[0.30,0.10,0.05], colEm:[0.50,0.15,0.02], signColor:'#ffaa00', signEm:[1,0.5,0.05] },
|
||
{ name: 'Вулкан обсидиан', colDif:[0.10,0.05,0.10], colEm:[0.05,0.02,0.05], signColor:'#aa44aa', specPower:128 },
|
||
{ name: 'Вулкан магма', colDif:[0.55,0.25,0.05], colEm:[0.85,0.40,0.08], signColor:'#ffff55', signEm:[1,1,0.3] },
|
||
{ name: 'Вулкан пепел', colDif:[0.25,0.22,0.22], colEm:[0.05,0.04,0.04], signColor:'#ff5555' },
|
||
{ name: 'Вулкан огонь', colDif:[0.45,0.15,0.05], colEm:[0.85,0.30,0.05], signColor:'#ffee00', signEm:[1,0.9,0.1] },
|
||
],
|
||
9: [
|
||
{ name: 'Космос звезда', colDif:[0.45,0.40,0.10], colEm:[0.65,0.55,0.15], signColor:'#ffffaa' },
|
||
{ name: 'Космос плазма', colDif:[0.30,0.55,0.95], colEm:[0.40,0.80,1.00], signColor:'#aaffff' },
|
||
{ name: 'Космос лазер', colDif:[0.80,0.10,0.50], colEm:[0.95,0.20,0.80], signColor:'#ffaaff', signEm:[1,0.4,1] },
|
||
{ name: 'Космос туманность', colDif:[0.55,0.25,0.85], colEm:[0.50,0.20,0.85], signColor:'#ffccff' },
|
||
{ name: 'Космос луна', colDif:[0.65,0.65,0.65], colEm:[0.35,0.35,0.35], signColor:'#bbbbbb' },
|
||
],
|
||
10: [
|
||
{ name: 'Кибер зелёный', colDif:[0.10,0.80,0.30], colEm:[0.20,1.00,0.40], signColor:'#aaffaa', signEm:[0.2,1.0,0.4] },
|
||
{ name: 'Кибер фиолет', colDif:[0.55,0.15,0.85], colEm:[0.70,0.25,1.00], signColor:'#ffaaff' },
|
||
{ name: 'Кибер красный', colDif:[0.85,0.10,0.30], colEm:[0.90,0.20,0.40], signColor:'#ffaaaa' },
|
||
{ name: 'Кибер голограмма', colDif:[0.30,0.50,1.0], colEm:[0.50,0.85,1.0], signColor:'#ffffff' },
|
||
{ name: 'Кибер жёлтый', colDif:[0.85,0.85,0.10], colEm:[0.85,0.85,0.15], signColor:'#000', signBg:'#ffff44', signEm:[1,1,0.3] },
|
||
],
|
||
};
|
||
|
||
export const ARCH_CATALOG = [];
|
||
const epoch1 = [
|
||
{ id: 'a1_v1', title: 'Деревянная', make: archForestWood },
|
||
{ id: 'a1_v2', title: 'Каменная', make: archForestStone },
|
||
{ id: 'a1_v3', title: 'С лианами', make: archForestVine },
|
||
{ id: 'a1_v4', title: 'С фонариками', make: archForestLanterns },
|
||
{ id: 'a1_v5', title: 'Рустик', make: archForestRustic },
|
||
];
|
||
for (const a of epoch1) ARCH_CATALOG.push({ ...a, epoch: 1 });
|
||
|
||
const epoch2 = [
|
||
{ id: 'a2_v1', title: 'Каменная', make: archMountainStone },
|
||
{ id: 'a2_v2', title: 'Кристалл', make: archMountainCrystal },
|
||
{ id: 'a2_v3', title: 'Ледяная', make: archMountainIce },
|
||
{ id: 'a2_v4', title: 'Пиковая', make: archMountainPeak },
|
||
{ id: 'a2_v5', title: 'Бронзовая', make: archMountainBronze },
|
||
];
|
||
for (const a of epoch2) ARCH_CATALOG.push({ ...a, epoch: 2 });
|
||
|
||
for (let epoch = 3; epoch <= 10; epoch++) {
|
||
const palettes = GENERIC_EPOCHS[epoch] || [];
|
||
for (let i = 0; i < palettes.length; i++) {
|
||
const palette = palettes[i];
|
||
ARCH_CATALOG.push({
|
||
id: `a${epoch}_v${i + 1}`,
|
||
epoch,
|
||
title: palette.name,
|
||
make: (scene, id) => archGeneric(scene, id, palette),
|
||
});
|
||
}
|
||
}
|
||
|
||
export const EPOCH_INFO = [
|
||
{ n: 1, name: 'Лес', emoji: '🌲', color: '#3a7a4a' },
|
||
{ n: 2, name: 'Горы', emoji: '🏔', color: '#aaccff' },
|
||
{ n: 3, name: 'Город днём', emoji: '🏙', color: '#7a8aaa' },
|
||
{ n: 4, name: 'Город ночью', emoji: '🌃', color: '#1a1a3a' },
|
||
{ n: 5, name: 'Пустыня', emoji: '🏜', color: '#c8a575' },
|
||
{ n: 6, name: 'Океан', emoji: '🌊', color: '#3a8aaa' },
|
||
{ n: 7, name: 'Пещеры', emoji: '🕳', color: '#3a3a3a' },
|
||
{ n: 8, name: 'Вулкан', emoji: '🌋', color: '#8a2a2a' },
|
||
{ n: 9, name: 'Космос', emoji: '🚀', color: '#3a1a5a' },
|
||
{ n: 10, name: 'Кибер', emoji: '🤖', color: '#ff00ff' },
|
||
];
|
||
|
||
export function getArchesByEpoch(epoch) {
|
||
return ARCH_CATALOG.filter(a => a.epoch === epoch);
|
||
}
|