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 <noreply@anthropic.com>
This commit is contained in:
200
app.js
200
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user