diff --git a/app.js b/app.js index 67e9023..dea19e5 100644 --- a/app.js +++ b/app.js @@ -40,6 +40,8 @@ let harnessPulseMesh; let powerMeterBars = []; let particles, dustParticles; let debugOverlay; +let glassEdgeMaterials = []; // Glass tile edge materials for animation +let voidLight = null; // Point light below glass floor let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; let loadProgress = 0; @@ -333,6 +335,150 @@ function createFloor() { ring.rotation.x = Math.PI / 2; ring.position.y = 0.05; scene.add(ring); + + // ─── Glass floor sections showing void below ─── + _buildGlassFloor(); +} + +function _buildGlassFloor() { + const GLASS_TILE_SIZE = 0.85; + const GLASS_TILE_GAP = 0.14; + const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP; + const GLASS_RADIUS = 4.55; + + const glassPlatformGroup = new THREE.Group(); + + // Solid dark frame ring around the glass section + const frameMat = new THREE.MeshStandardMaterial({ + color: 0x0a1828, + metalness: 0.9, + roughness: 0.1, + emissive: new THREE.Color(NEXUS.colors.primary).multiplyScalar(0.06), + }); + const rimGeo = new THREE.RingGeometry(4.7, 5.3, 64); + const rim = new THREE.Mesh(rimGeo, frameMat); + rim.rotation.x = -Math.PI / 2; + rim.position.y = 0.01; + glassPlatformGroup.add(rim); + + const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64); + const borderTorus = new THREE.Mesh(borderTorusGeo, frameMat); + borderTorus.rotation.x = Math.PI / 2; + borderTorus.position.y = 0.01; + glassPlatformGroup.add(borderTorus); + + // Semi-transparent glass tile material (transmission lets void show through) + const glassTileMat = new THREE.MeshPhysicalMaterial({ + color: new THREE.Color(NEXUS.colors.primary), + transparent: true, + opacity: 0.09, + roughness: 0.0, + metalness: 0.0, + transmission: 0.92, + thickness: 0.06, + side: THREE.DoubleSide, + depthWrite: false, + }); + + // Collect tile positions within the glass radius + const tileSlots = []; + 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; + tileSlots.push({ x, z, distFromCenter }); + } + } + + // InstancedMesh for all tiles (single draw call) + const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE); + const tileMesh = new THREE.InstancedMesh(tileGeo, glassTileMat, tileSlots.length); + tileMesh.instanceMatrix.setUsage(THREE.StaticDrawUsage); + const dummy = new THREE.Object3D(); + dummy.rotation.x = -Math.PI / 2; + for (let i = 0; i < tileSlots.length; i++) { + dummy.position.set(tileSlots[i].x, 0.005, tileSlots[i].z); + dummy.updateMatrix(); + tileMesh.setMatrixAt(i, dummy.matrix); + } + tileMesh.instanceMatrix.needsUpdate = true; + glassPlatformGroup.add(tileMesh); + + // Merge all tile edge lines into a single LineSegments draw call + const HS = GLASS_TILE_SIZE / 2; + const edgeVerts = new Float32Array(tileSlots.length * 8 * 3); + let evi = 0; + for (const { x, z } of tileSlots) { + const y = 0.008; + edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; + edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; + edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; + edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; + edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; + edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; + edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; + edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; + } + const mergedEdgeGeo = new THREE.BufferGeometry(); + mergedEdgeGeo.setAttribute('position', new THREE.BufferAttribute(edgeVerts, 3)); + const edgeMat = new THREE.LineBasicMaterial({ + color: NEXUS.colors.primary, + transparent: true, + opacity: 0.55, + }); + glassPlatformGroup.add(new THREE.LineSegments(mergedEdgeGeo, edgeMat)); + + // Register per-tile edge entries for the animation loop + // (we animate the single merged material, grouped by distance bands) + const BAND_COUNT = 6; + const bandMats = []; + for (let b = 0; b < BAND_COUNT; b++) { + const mat = new THREE.LineBasicMaterial({ + color: NEXUS.colors.primary, + transparent: true, + opacity: 0.55, + }); + // distFromCenter goes 0 → GLASS_RADIUS; spread across bands + const distFromCenter = (b / (BAND_COUNT - 1)) * GLASS_RADIUS; + glassEdgeMaterials.push({ mat, distFromCenter }); + bandMats.push(mat); + } + + // Rebuild edge geometry per band so each band has its own material + // (more draw calls but proper animated glow rings) + mergedEdgeGeo.dispose(); // dispose the merged one we won't use + for (let b = 0; b < BAND_COUNT; b++) { + const bandMin = (b / BAND_COUNT) * GLASS_RADIUS; + const bandMax = ((b + 1) / BAND_COUNT) * GLASS_RADIUS; + const bandSlots = tileSlots.filter(s => s.distFromCenter >= bandMin && s.distFromCenter < bandMax); + if (bandSlots.length === 0) continue; + + const bVerts = new Float32Array(bandSlots.length * 8 * 3); + let bvi = 0; + for (const { x, z } of bandSlots) { + const y = 0.008; + bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; + bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; + bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; + bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; + bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; + bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; + bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; + bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; + } + const bGeo = new THREE.BufferGeometry(); + bGeo.setAttribute('position', new THREE.BufferAttribute(bVerts, 3)); + glassPlatformGroup.add(new THREE.LineSegments(bGeo, bandMats[b])); + } + + // Void light pulses below the glass to illuminate the emptiness underneath + voidLight = new THREE.PointLight(NEXUS.colors.primary, 0.5, 14); + voidLight.position.set(0, -3.5, 0); + glassPlatformGroup.add(voidLight); + + scene.add(glassPlatformGroup); } // ═══ BATCAVE TERMINAL ═══ @@ -1475,6 +1621,15 @@ function gameLoop() { bar.scale.x = active ? 1.2 : 1.0; }); + // Animate glass floor edge glow (ripple outward from center) + 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; + } + if (thoughtStreamMesh) { thoughtStreamMesh.material.uniforms.uTime.value = elapsed; thoughtStreamMesh.rotation.y = elapsed * 0.05;