feat: add animated energy shields around Batcave terminal (#112)
Some checks failed
CI / validate (pull_request) Failing after 12s
CI / auto-merge (pull_request) Has been skipped

- 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 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-24 00:41:13 -04:00
parent eadc104842
commit d43bb67a5f

200
app.js
View File

@@ -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();
}