Split the monolithic 5393-line app.js into 32 focused ES modules under modules/ with a thin ~330-line orchestrator. No bundler required — runs in-browser via import maps. Module structure: core/ — scene, ticker, state, theme, audio data/ — gitea, weather, bitcoin, loaders terrain/ — stars, clouds, island effects/ — matrix-rain, energy-beam, lightning, shockwave, rune-ring, gravity-zones panels/ — heatmap, sigil, sovereignty, dual-brain, batcave, earth, agent-board, lora-panel portals/ — portal-system, commit-banners narrative/ — bookshelves, oath, chat utils/ — perlin All files pass node --check. No new dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
222 lines
8.8 KiB
JavaScript
222 lines
8.8 KiB
JavaScript
// modules/terrain/island.js — Floating island + glass platform + crystals
|
|
import * as THREE from 'three';
|
|
import { THEME } from '../core/theme.js';
|
|
import { perlin } from '../utils/perlin.js';
|
|
|
|
const GLASS_RADIUS = 4.55;
|
|
const GLASS_TILE_SIZE = 0.85;
|
|
const GLASS_TILE_GAP = 0.14;
|
|
const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP;
|
|
|
|
export { GLASS_RADIUS };
|
|
|
|
const glassEdgeMaterials = [];
|
|
let voidLight;
|
|
|
|
export function init(scene) {
|
|
// --- Glass Platform ---
|
|
const glassPlatformGroup = new THREE.Group();
|
|
|
|
const platformFrameMat = new THREE.MeshStandardMaterial({
|
|
color: 0x0a1828, metalness: 0.9, roughness: 0.1,
|
|
emissive: new THREE.Color(THEME.colors.accent).multiplyScalar(0.06),
|
|
});
|
|
|
|
const platformRim = new THREE.Mesh(new THREE.RingGeometry(4.7, 5.3, 64), platformFrameMat);
|
|
platformRim.rotation.x = -Math.PI / 2;
|
|
platformRim.castShadow = true;
|
|
platformRim.receiveShadow = true;
|
|
glassPlatformGroup.add(platformRim);
|
|
|
|
const borderTorus = new THREE.Mesh(new THREE.TorusGeometry(5.0, 0.1, 6, 64), platformFrameMat);
|
|
borderTorus.rotation.x = Math.PI / 2;
|
|
borderTorus.castShadow = true;
|
|
borderTorus.receiveShadow = true;
|
|
glassPlatformGroup.add(borderTorus);
|
|
|
|
const glassTileMat = new THREE.MeshPhysicalMaterial({
|
|
color: new THREE.Color(THEME.colors.accent), transparent: true, opacity: 0.09,
|
|
roughness: 0.0, metalness: 0.0, transmission: 0.92, thickness: 0.06,
|
|
side: THREE.DoubleSide, depthWrite: false,
|
|
});
|
|
|
|
const glassEdgeBaseMat = new THREE.LineBasicMaterial({
|
|
color: THEME.colors.accent, transparent: true, opacity: 0.55,
|
|
});
|
|
|
|
const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE);
|
|
const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo);
|
|
|
|
for (let row = -5; row <= 5; row++) {
|
|
for (let col = -5; col <= 5; col++) {
|
|
const x = col * GLASS_TILE_STEP;
|
|
const z = row * GLASS_TILE_STEP;
|
|
const distFromCenter = Math.sqrt(x * x + z * z);
|
|
if (distFromCenter > GLASS_RADIUS) continue;
|
|
|
|
const tile = new THREE.Mesh(tileGeo, glassTileMat.clone());
|
|
tile.rotation.x = -Math.PI / 2;
|
|
tile.position.set(x, 0, z);
|
|
glassPlatformGroup.add(tile);
|
|
|
|
const mat = glassEdgeBaseMat.clone();
|
|
const edges = new THREE.LineSegments(tileEdgeGeo, mat);
|
|
edges.rotation.x = -Math.PI / 2;
|
|
edges.position.set(x, 0.002, z);
|
|
glassPlatformGroup.add(edges);
|
|
glassEdgeMaterials.push({ mat, distFromCenter });
|
|
}
|
|
}
|
|
|
|
voidLight = new THREE.PointLight(THEME.colors.accent, 0.5, 14);
|
|
voidLight.position.set(0, -3.5, 0);
|
|
glassPlatformGroup.add(voidLight);
|
|
|
|
scene.add(glassPlatformGroup);
|
|
glassPlatformGroup.traverse(obj => {
|
|
if (obj.isMesh) obj.userData.zoomLabel = 'Glass Platform';
|
|
});
|
|
|
|
// --- Floating Island Terrain ---
|
|
const ISLAND_RADIUS = 9.5;
|
|
const SEGMENTS = 96;
|
|
const SIZE = ISLAND_RADIUS * 2;
|
|
|
|
function islandFBm(nx, nz) {
|
|
const wx = perlin(nx * 0.5 + 3.7, nz * 0.5 + 1.2) * 0.55;
|
|
const wz = perlin(nx * 0.5 + 8.3, nz * 0.5 + 5.9) * 0.55;
|
|
const px = nx + wx, pz = nz + wz;
|
|
let h = 0;
|
|
h += perlin(px, pz) * 1.000;
|
|
h += perlin(px * 2, pz * 2) * 0.500;
|
|
h += perlin(px * 4, pz * 4) * 0.250;
|
|
h += perlin(px * 8, pz * 8) * 0.125;
|
|
h += perlin(px * 16, pz * 16) * 0.063;
|
|
h /= 1.938;
|
|
const ridge = 1.0 - Math.abs(perlin(px * 3.1 + 5.0, pz * 3.1 + 7.0));
|
|
return h * 0.78 + ridge * 0.22;
|
|
}
|
|
|
|
const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEGMENTS, SEGMENTS);
|
|
geo.rotateX(-Math.PI / 2);
|
|
const pos = geo.attributes.position;
|
|
const vCount = pos.count;
|
|
const rawHeights = new Float32Array(vCount);
|
|
|
|
for (let i = 0; i < vCount; i++) {
|
|
const x = pos.getX(i);
|
|
const z = pos.getZ(i);
|
|
const dist = Math.sqrt(x * x + z * z) / ISLAND_RADIUS;
|
|
const rimNoise = perlin(x * 0.38 + 10, z * 0.38 + 10) * 0.10;
|
|
const edgeFactor = Math.max(0, 1 - Math.pow(Math.max(0, dist - rimNoise), 2.4));
|
|
const h = islandFBm(x * 0.15, z * 0.15);
|
|
const height = ((h + 1) * 0.5) * edgeFactor * 3.2;
|
|
pos.setY(i, height);
|
|
rawHeights[i] = height;
|
|
}
|
|
geo.computeVertexNormals();
|
|
|
|
const colBuf = new Float32Array(vCount * 3);
|
|
for (let i = 0; i < vCount; i++) {
|
|
const h = rawHeights[i];
|
|
let r, g, b;
|
|
if (h < 0.25) { r = 0.11; g = 0.09; b = 0.07; }
|
|
else if (h < 0.75) { const t = (h - 0.25) / 0.50; r = 0.11 + t * 0.13; g = 0.09 + t * 0.09; b = 0.07 + t * 0.06; }
|
|
else if (h < 1.4) { const t = (h - 0.75) / 0.65; r = 0.24 + t * 0.12; g = 0.18 + t * 0.10; b = 0.13 + t * 0.10; }
|
|
else if (h < 2.2) { const t = (h - 1.4) / 0.80; r = 0.36 + t * 0.14; g = 0.28 + t * 0.11; b = 0.23 + t * 0.13; }
|
|
else { const t = Math.min(1, (h - 2.2) / 0.9); r = 0.50 + t * 0.05; g = 0.39 + t * 0.10; b = 0.36 + t * 0.28; }
|
|
colBuf[i * 3] = r; colBuf[i * 3 + 1] = g; colBuf[i * 3 + 2] = b;
|
|
}
|
|
geo.setAttribute('color', new THREE.BufferAttribute(colBuf, 3));
|
|
|
|
const topMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.86, metalness: 0.05 });
|
|
const topMesh = new THREE.Mesh(geo, topMat);
|
|
topMesh.castShadow = true;
|
|
topMesh.receiveShadow = true;
|
|
|
|
// Crystal spires
|
|
const crystalMat = new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color(THEME.colors.accent).multiplyScalar(0.55),
|
|
emissive: new THREE.Color(THEME.colors.accent), emissiveIntensity: 0.5,
|
|
roughness: 0.08, metalness: 0.25, transparent: true, opacity: 0.80,
|
|
});
|
|
const CRYSTAL_MIN_H = 2.05;
|
|
const crystalGroup = new THREE.Group();
|
|
|
|
for (let row = -5; row <= 5; row++) {
|
|
for (let col = -5; col <= 5; col++) {
|
|
const bx = col * 1.75, bz = row * 1.75;
|
|
if (Math.sqrt(bx * bx + bz * bz) > ISLAND_RADIUS * 0.72) continue;
|
|
const edF = Math.max(0, 1 - Math.pow(Math.sqrt(bx * bx + bz * bz) / ISLAND_RADIUS, 2.4));
|
|
const candidateH = ((islandFBm(bx * 0.15, bz * 0.15) + 1) * 0.5) * edF * 3.2;
|
|
if (candidateH < CRYSTAL_MIN_H) continue;
|
|
const jx = bx + perlin(bx * 0.7 + 20, bz * 0.7 + 20) * 0.55;
|
|
const jz = bz + perlin(bx * 0.7 + 30, bz * 0.7 + 30) * 0.55;
|
|
if (Math.sqrt(jx * jx + jz * jz) > ISLAND_RADIUS * 0.68) continue;
|
|
const clusterSize = 2 + Math.floor(Math.abs(perlin(bx * 0.5 + 40, bz * 0.5 + 40)) * 3);
|
|
for (let c = 0; c < clusterSize; c++) {
|
|
const angle = (c / clusterSize) * Math.PI * 2 + perlin(bx + c, bz + c) * 1.4;
|
|
const spread = 0.08 + Math.abs(perlin(bx + c * 5, bz + c * 5)) * 0.22;
|
|
const sx = jx + Math.cos(angle) * spread;
|
|
const sz = jz + Math.sin(angle) * spread;
|
|
const spireScale = 0.14 + (candidateH - CRYSTAL_MIN_H) * 0.11;
|
|
const spireH = spireScale * (0.8 + Math.abs(perlin(sx, sz)) * 0.45);
|
|
const spireR = spireH * 0.17;
|
|
const spireGeo = new THREE.ConeGeometry(spireR, spireH * 2.8, 5);
|
|
const spire = new THREE.Mesh(spireGeo, crystalMat);
|
|
spire.position.set(sx, candidateH + spireH * 0.5, sz);
|
|
spire.rotation.z = perlin(sx * 2, sz * 2) * 0.28;
|
|
spire.rotation.x = perlin(sx * 3 + 1, sz * 3 + 1) * 0.18;
|
|
spire.castShadow = true;
|
|
crystalGroup.add(spire);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rocky underside
|
|
const BOTTOM_SEGS_R = 52, BOTTOM_SEGS_V = 10, BOTTOM_HEIGHT = 2.6;
|
|
const bottomGeo = new THREE.CylinderGeometry(
|
|
ISLAND_RADIUS * 0.80, ISLAND_RADIUS * 0.28, BOTTOM_HEIGHT, BOTTOM_SEGS_R, BOTTOM_SEGS_V, true
|
|
);
|
|
const bPos = bottomGeo.attributes.position;
|
|
for (let i = 0; i < bPos.count; i++) {
|
|
const bx = bPos.getX(i), bz = bPos.getZ(i), by = bPos.getY(i);
|
|
const bAngle = Math.atan2(bz, bx);
|
|
const r = Math.sqrt(bx * bx + bz * bz);
|
|
const radDisp = perlin(Math.cos(bAngle) * 1.6 + 50, Math.sin(bAngle) * 1.6 + 50) * 0.65;
|
|
const vNorm = (by + BOTTOM_HEIGHT * 0.5) / BOTTOM_HEIGHT;
|
|
const stalDisp = (1 - vNorm) * Math.abs(perlin(bx * 0.35 + 70, by * 0.7 + bz * 0.35)) * 0.9;
|
|
const newR = r + radDisp;
|
|
bPos.setX(i, (bx / r) * newR);
|
|
bPos.setZ(i, (bz / r) * newR);
|
|
bPos.setY(i, by - stalDisp);
|
|
}
|
|
bottomGeo.computeVertexNormals();
|
|
|
|
const bottomMat = new THREE.MeshStandardMaterial({ color: 0x0c0a08, roughness: 0.93, metalness: 0.02 });
|
|
const bottomMesh = new THREE.Mesh(bottomGeo, bottomMat);
|
|
bottomMesh.position.y = -BOTTOM_HEIGHT * 0.5;
|
|
bottomMesh.castShadow = true;
|
|
|
|
const capGeo = new THREE.CircleGeometry(ISLAND_RADIUS * 0.28, 48);
|
|
capGeo.rotateX(Math.PI / 2);
|
|
const capMesh = new THREE.Mesh(capGeo, bottomMat);
|
|
capMesh.position.y = -(BOTTOM_HEIGHT + 0.1);
|
|
|
|
const islandGroup = new THREE.Group();
|
|
islandGroup.add(topMesh);
|
|
islandGroup.add(crystalGroup);
|
|
islandGroup.add(bottomMesh);
|
|
islandGroup.add(capMesh);
|
|
islandGroup.position.y = -2.8;
|
|
scene.add(islandGroup);
|
|
}
|
|
|
|
export function update(elapsed) {
|
|
for (const { mat, distFromCenter } of glassEdgeMaterials) {
|
|
const phase = elapsed * 1.1 - distFromCenter * 0.18;
|
|
mat.opacity = 0.25 + Math.sin(phase) * 0.22;
|
|
}
|
|
if (voidLight) voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2;
|
|
}
|