From d43bb67a5feb33912b91df6d5937fe8fd38de712 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:41:13 -0400 Subject: [PATCH] feat: add animated energy shields around Batcave terminal (#112) - Builds Batcave terminal group at z=-12: raised floor platform, console desk, main monitor with flickering emissive screen, four corner pillars with point lights - Adds three-layer GLSL shader domes (ShaderMaterial, additive blending) featuring hex-grid pattern, Fresnel rim, scrolling scan-line, and expanding ripple; each dome breathes at a different phase/speed - Three equatorial torus rings at platform/mid/upper heights pulsing opacity and scale - All elements animated in the main loop: shield opacity/scale breathing, torus ring pulses, pillar light flicker, monitor screen flicker Fixes #112 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 200 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/app.js b/app.js index 23cc7d3..e051217 100644 --- a/app.js +++ b/app.js @@ -575,6 +575,177 @@ async function loadSovereigntyStatus() { loadSovereigntyStatus(); +// === BATCAVE TERMINAL === +// A dark command post behind the main platform, defended by animated energy shield domes. + +const batcaveGroup = new THREE.Group(); +batcaveGroup.position.set(0, 0, -12); +scene.add(batcaveGroup); + +// Raised floor platform +const batcaveFloorMat = new THREE.MeshStandardMaterial({ + color: 0x080e1c, + metalness: 0.85, + roughness: 0.2, + emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.04), +}); +batcaveGroup.add(Object.assign( + new THREE.Mesh(new THREE.BoxGeometry(7, 0.25, 5), batcaveFloorMat), + { position: new THREE.Vector3(0, -0.125, 0) } +)); + +// Console desk +const deskMat = new THREE.MeshStandardMaterial({ color: 0x0c1830, metalness: 0.92, roughness: 0.18 }); +batcaveGroup.add(Object.assign( + new THREE.Mesh(new THREE.BoxGeometry(3.8, 0.45, 1.1), deskMat), + { position: new THREE.Vector3(0, 0.225, 0.6) } +)); + +// Main monitor — emissive cyan screen +const batcaveMonitorMesh = new THREE.Mesh( + new THREE.BoxGeometry(2.6, 1.5, 0.07), + new THREE.MeshStandardMaterial({ + color: 0x000d22, + emissive: new THREE.Color(NEXUS.colors.accent), + emissiveIntensity: 0.55, + roughness: 1, + }) +); +batcaveMonitorMesh.position.set(0, 1.2, 0.24); +batcaveGroup.add(batcaveMonitorMesh); + +// Monitor point light +const batcaveMonitorLight = new THREE.PointLight(NEXUS.colors.accent, 0.9, 6); +batcaveMonitorLight.position.set(0, 1.2, 1.0); +batcaveGroup.add(batcaveMonitorLight); + +// Corner pillars with glow +const pillarGeo = new THREE.CylinderGeometry(0.1, 0.14, 2.8, 8); +const pillarMat = new THREE.MeshStandardMaterial({ + color: 0x0d1830, metalness: 0.95, roughness: 0.12, + emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.1), +}); +const PILLAR_XZ = [[-2.8, -2.0], [2.8, -2.0], [-2.8, 2.0], [2.8, 2.0]]; +/** @type {THREE.PointLight[]} */ +const batcavePillarLights = []; +for (const [px, pz] of PILLAR_XZ) { + const pillar = new THREE.Mesh(pillarGeo, pillarMat); + pillar.position.set(px, 1.4, pz); + batcaveGroup.add(pillar); + const pl = new THREE.PointLight(NEXUS.colors.accent, 0.35, 3.5); + pl.position.set(px, 2.4, pz); + batcaveGroup.add(pl); + batcavePillarLights.push(pl); +} + +// === ENERGY SHIELD SHADERS === +const shieldVertexShader = /* glsl */` + varying vec3 vNormal; + varying vec3 vViewDir; + varying vec2 vUv; + + void main() { + vNormal = normalize(normalMatrix * normal); + vec4 worldPos = modelMatrix * vec4(position, 1.0); + vViewDir = normalize(cameraPosition - worldPos.xyz); + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const shieldFragmentShader = /* glsl */` + uniform float uTime; + uniform vec3 uColor; + uniform float uOpacity; + varying vec3 vNormal; + varying vec3 vViewDir; + varying vec2 vUv; + + // Simple hex pattern via offset-row grid + float hexPattern(vec2 p) { + vec2 r = vec2(1.0, 1.732); + vec2 h = r * 0.5; + vec2 a = mod(p, r) - h; + vec2 b = mod(p - h, r) - h; + float da = length(a); + float db = length(b); + return min(da, db); + } + + void main() { + // Fresnel rim — brighter at glancing angles + float fresnel = 1.0 - abs(dot(vViewDir, vNormal)); + fresnel = pow(fresnel, 2.2); + + // Hex grid lines + float hd = hexPattern(vUv * 14.0); + float hLine = 1.0 - smoothstep(0.38, 0.46, hd); + + // Scrolling scan line + float scan = pow(sin(vUv.y * 90.0 - uTime * 3.5) * 0.5 + 0.5, 8.0) * 0.45; + + // Expanding ripple from center + float dist = length(vUv - 0.5); + float ripple = pow(sin(dist * 18.0 - uTime * 2.8) * 0.5 + 0.5, 5.0) * 0.3; + + float alpha = (fresnel * 0.55 + hLine * 0.5 + scan + ripple) * uOpacity; + gl_FragColor = vec4(uColor, clamp(alpha, 0.0, 0.95)); + } +`; + +// Three concentric shield domes — different sizes, phases, and tints +const SHIELD_CONFIGS = [ + { sx: 4.0, sy: 3.2, sz: 3.8, color: new THREE.Color(NEXUS.colors.accent), phase: 0.0, speed: 1.0 }, + { sx: 4.7, sy: 3.8, sz: 4.4, color: new THREE.Color(0x22aaff), phase: Math.PI * 0.667, speed: 0.72 }, + { sx: 5.4, sy: 4.4, sz: 5.0, color: new THREE.Color(0x88ffff), phase: Math.PI * 1.333, speed: 0.48 }, +]; + +/** @type {THREE.Mesh[]} */ +const shieldMeshes = []; +for (const cfg of SHIELD_CONFIGS) { + // Upper hemisphere dome (phi 0 → π * 0.6 covers top + sides) + const geo = new THREE.SphereGeometry(1, 48, 32, 0, Math.PI * 2, 0, Math.PI * 0.62); + const mat = new THREE.ShaderMaterial({ + vertexShader: shieldVertexShader, + fragmentShader: shieldFragmentShader, + uniforms: { + uTime: { value: 0.0 }, + uColor: { value: cfg.color }, + uOpacity: { value: 0.0 }, + }, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.scale.set(cfg.sx, cfg.sy, cfg.sz); + // Raise slightly so dome sits on the floor + mesh.position.set(0, cfg.sy * 0.42, 0); + mesh.userData = { phase: cfg.phase, speed: cfg.speed, sx: cfg.sx, sy: cfg.sy, sz: cfg.sz }; + batcaveGroup.add(mesh); + shieldMeshes.push(mesh); +} + +// Equatorial torus rings that pulse at three heights +/** @type {THREE.Mesh[]} */ +const shieldTorusMeshes = []; +for (const yh of [0.18, 1.6, 3.1]) { + const torusMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.accent, + transparent: true, + opacity: 0.0, + blending: THREE.AdditiveBlending, + depthWrite: false, + }); + const torus = new THREE.Mesh(new THREE.TorusGeometry(4.0, 0.045, 8, 64), torusMat); + torus.position.y = yh; + torus.rotation.x = Math.PI / 2; + torus.userData = { baseY: yh }; + batcaveGroup.add(torus); + shieldTorusMeshes.push(torus); +} + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -656,6 +827,35 @@ function animate() { sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; } + // Animate Batcave energy shields + for (const sm of shieldMeshes) { + sm.material.uniforms.uTime.value = elapsed; + const pulse = Math.sin(elapsed * sm.userData.speed + sm.userData.phase) * 0.5 + 0.5; + sm.material.uniforms.uOpacity.value = 0.22 + pulse * 0.32; + // Subtle breathing scale + const breathe = 1.0 + Math.sin(elapsed * sm.userData.speed * 0.45 + sm.userData.phase) * 0.018; + sm.scale.set( + sm.userData.sx * breathe, + sm.userData.sy * breathe, + sm.userData.sz * breathe + ); + } + + // Pulse torus rings + for (let i = 0; i < shieldTorusMeshes.length; i++) { + const torus = shieldTorusMeshes[i]; + const p = Math.sin(elapsed * 1.3 + i * Math.PI * 0.667) * 0.5 + 0.5; + torus.material.opacity = 0.25 + p * 0.55; + const rs = 1.0 + Math.sin(elapsed * 0.9 + i) * 0.025; + torus.scale.set(rs, rs, 1); + } + + // Flicker pillar lights and monitor + for (let i = 0; i < batcavePillarLights.length; i++) { + batcavePillarLights[i].intensity = 0.28 + Math.sin(elapsed * 1.6 + i * Math.PI * 0.5) * 0.12; + } + batcaveMonitorLight.intensity = 0.7 + Math.sin(elapsed * 11.7) * 0.07 + Math.sin(elapsed * 7.1) * 0.05; + composer.render(); } -- 2.43.0