← back to Dw War Room

src/scene/CommandTable.ts

271 lines

// ═══════════════════════════════════════════════
// DW War Room 3D — Command Table
// Central holographic table in the war room
// ═══════════════════════════════════════════════

import * as THREE from 'three';

// ─── Types ───────────────────────────────────────────────────────────────────

export interface CommandTableHandle {
  animate(elapsed: number): void;
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Build a flat torus lying horizontal (rotated -90° on X). */
function horizontalTorus(
  radius: number,
  tube: number,
  radialSeg: number,
  tubularSeg: number,
  color: number,
  opacity: number,
  additive = false,
): THREE.Mesh {
  const geo = new THREE.TorusGeometry(radius, tube, radialSeg, tubularSeg);
  const mat = new THREE.MeshBasicMaterial({
    color,
    transparent: true,
    opacity,
    blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending,
    depthWrite: false,
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.rotation.x = -Math.PI / 2;
  return mesh;
}

// ─── Main factory ────────────────────────────────────────────────────────────

export function createCommandTable(scene: THREE.Scene): CommandTableHandle {
  const group = new THREE.Group();
  group.name = 'command-table';

  // ── 1. Table base — dark cylinder ────────────────────────────────────────
  const baseGeo = new THREE.CylinderGeometry(6, 7, 0.3, 64);
  const baseMat = new THREE.MeshPhongMaterial({
    color: 0x0a0a1e,
    emissive: 0x1a1a3e,
    emissiveIntensity: 0.4,
    transparent: true,
    opacity: 0.92,
  });
  const base = new THREE.Mesh(baseGeo, baseMat);
  base.position.y = 0.15;
  group.add(base);

  // ── 2. Animated outer ring (purple) ──────────────────────────────────────
  const outerRing = horizontalTorus(6.2, 0.08, 8, 96, 0x8b5cf6, 0.75, true);
  outerRing.position.y = 0.35;
  group.add(outerRing);

  // ── 3. Secondary outer glow ring (same radius, wider tube, very faint) ───
  const outerGlow = horizontalTorus(6.2, 0.22, 8, 64, 0x8b5cf6, 0.12, true);
  outerGlow.position.y = 0.35;
  group.add(outerGlow);

  // ── 4. Inner ring (blue) ─────────────────────────────────────────────────
  const innerRing = horizontalTorus(4, 0.05, 8, 64, 0x3b82f6, 0.5, true);
  innerRing.position.y = 0.36;
  group.add(innerRing);

  // ── 5. Mid ring (blue, thin) ─────────────────────────────────────────────
  const midRing = horizontalTorus(5.1, 0.035, 8, 64, 0x3b82f6, 0.3, true);
  midRing.position.y = 0.36;
  group.add(midRing);

  // ── 6. Edge glow ring — bright emissive at table edge ────────────────────
  const edgeGlowGeo = new THREE.TorusGeometry(6.85, 0.06, 8, 96);
  const edgeGlowMat = new THREE.MeshStandardMaterial({
    color: 0x8b5cf6,
    emissive: 0x8b5cf6,
    emissiveIntensity: 2.5,
    transparent: true,
    opacity: 0.55,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
  });
  const edgeGlow = new THREE.Mesh(edgeGlowGeo, edgeGlowMat);
  edgeGlow.rotation.x = -Math.PI / 2;
  edgeGlow.position.y = 0.33;
  group.add(edgeGlow);

  // ── 7. Animated grid ─────────────────────────────────────────────────────
  const gridHelper = new THREE.GridHelper(12, 20, 0x3b3366, 0x1a1a3e);
  gridHelper.position.y = 0.32;
  // GridHelper material is transparent by default — boost it slightly
  const gridMats = Array.isArray(gridHelper.material)
    ? gridHelper.material
    : [gridHelper.material];
  gridMats.forEach((m) => {
    (m as THREE.LineBasicMaterial).transparent = true;
    (m as THREE.LineBasicMaterial).opacity = 0.55;
    (m as THREE.LineBasicMaterial).blending = THREE.AdditiveBlending;
  });
  group.add(gridHelper);

  // ── 8. Holographic projection cone ───────────────────────────────────────
  // Open-ended cone pointing upward, wide base at table level, tip at top
  const coneGeo = new THREE.ConeGeometry(5.5, 9, 64, 1, true);
  const coneMat = new THREE.MeshBasicMaterial({
    color: 0x8b5cf6,
    transparent: true,
    opacity: 0.06,
    side: THREE.DoubleSide,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
  });
  const cone = new THREE.Mesh(coneGeo, coneMat);
  // Base of cone sits at table surface (y=0.35), tip points up to y≈9.35
  cone.position.y = 0.35 + 9 / 2; // y = 4.85
  group.add(cone);

  // Second cone pass — slightly smaller, more opaque for depth
  const coneInnerGeo = new THREE.ConeGeometry(3.2, 7, 48, 1, true);
  const coneInnerMat = new THREE.MeshBasicMaterial({
    color: 0x3b82f6,
    transparent: true,
    opacity: 0.045,
    side: THREE.DoubleSide,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
  });
  const coneInner = new THREE.Mesh(coneInnerGeo, coneInnerMat);
  coneInner.position.y = 0.35 + 7 / 2; // y = 3.85
  group.add(coneInner);

  // ── 9. Pulsing energy core — sphere at y=2 ───────────────────────────────
  const coreGeo = new THREE.SphereGeometry(0.22, 32, 32);
  const coreMat = new THREE.MeshStandardMaterial({
    color: 0xffffff,
    emissive: 0x8b5cf6,
    emissiveIntensity: 3.0,
    transparent: true,
    opacity: 0.9,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
  });
  const core = new THREE.Mesh(coreGeo, coreMat);
  core.position.y = 2;
  group.add(core);

  // Core halo — larger diffuse sphere around the core
  const haloGeo = new THREE.SphereGeometry(0.55, 24, 24);
  const haloMat = new THREE.MeshBasicMaterial({
    color: 0x8b5cf6,
    transparent: true,
    opacity: 0.12,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
  });
  const halo = new THREE.Mesh(haloGeo, haloMat);
  halo.position.y = 2;
  group.add(halo);

  // ── 10. Data particle column ─────────────────────────────────────────────
  const PARTICLE_COUNT = 200;
  const particlePositions = new Float32Array(PARTICLE_COUNT * 3);
  // Random speeds per particle (stored separately; not in geometry)
  const particleSpeeds = new Float32Array(PARTICLE_COUNT);
  const particleAngles = new Float32Array(PARTICLE_COUNT);
  const particleRadii = new Float32Array(PARTICLE_COUNT);

  for (let i = 0; i < PARTICLE_COUNT; i++) {
    const angle = Math.random() * Math.PI * 2;
    const radius = Math.random() * 4.5 + 0.5; // 0.5 – 5.0 from center
    const startY = Math.random() * 8;          // scattered initial heights

    particleAngles[i] = angle;
    particleRadii[i] = radius;
    particleSpeeds[i] = 0.4 + Math.random() * 1.2; // units/s

    particlePositions[i * 3]     = Math.cos(angle) * radius;
    particlePositions[i * 3 + 1] = startY;
    particlePositions[i * 3 + 2] = Math.sin(angle) * radius;
  }

  const particleGeo = new THREE.BufferGeometry();
  particleGeo.setAttribute(
    'position',
    new THREE.BufferAttribute(particlePositions, 3),
  );

  const particleMat = new THREE.PointsMaterial({
    color: 0x8b5cf6,
    size: 0.05,
    transparent: true,
    opacity: 0.7,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    sizeAttenuation: true,
  });

  const particles = new THREE.Points(particleGeo, particleMat);
  group.add(particles);

  // ── Finalize group ────────────────────────────────────────────────────────
  scene.add(group);

  // ─── Animate closure ────────────────────────────────────────────────────
  // elapsed is in milliseconds (matching the convention used in WarRoom.ts)
  let lastElapsed = 0;

  return {
    animate(elapsed: number): void {
      const t = elapsed / 1000; // seconds
      const dt = (elapsed - lastElapsed) / 1000;
      lastElapsed = elapsed;

      // — Outer ring: slow clockwise rotation on Y axis —
      outerRing.rotation.z += 0.004;
      outerGlow.rotation.z -= 0.002; // counter-rotate for layered feel

      // — Inner/mid rings: gentle counter-rotation —
      innerRing.rotation.z -= 0.006;
      midRing.rotation.z += 0.003;

      // — Animated grid: very slow rotation —
      gridHelper.rotation.y += 0.001;

      // — Edge glow: pulse brightness via emissiveIntensity —
      const edgePulse = 1.8 + Math.sin(t * 1.8) * 0.7;
      (edgeGlowMat as THREE.MeshStandardMaterial).emissiveIntensity = edgePulse;

      // — Energy core: scale + emissive pulse —
      const corePulse = 0.85 + Math.sin(t * 2.4) * 0.25;
      core.scale.setScalar(corePulse);
      (coreMat as THREE.MeshStandardMaterial).emissiveIntensity =
        2.5 + Math.sin(t * 2.4) * 1.5;
      halo.scale.setScalar(0.9 + Math.sin(t * 2.4 + 0.5) * 0.35);
      (haloMat as THREE.MeshBasicMaterial).opacity =
        0.08 + Math.sin(t * 2.4) * 0.06;

      // — Holographic cones: very slow counter-rotations on Y —
      cone.rotation.y += 0.0015;
      coneInner.rotation.y -= 0.002;

      // — Data particles: rise upward, respawn at bottom —
      const posAttr = particleGeo.attributes['position'] as THREE.BufferAttribute;
      const posArray = posAttr.array as Float32Array;
      const COLUMN_TOP = 8.5;

      for (let i = 0; i < PARTICLE_COUNT; i++) {
        posArray[i * 3 + 1] += particleSpeeds[i] * dt;

        if (posArray[i * 3 + 1] > COLUMN_TOP) {
          // Respawn at table surface with fresh random position
          const newAngle = Math.random() * Math.PI * 2;
          const newRadius = Math.random() * 4.5 + 0.5;
          particleAngles[i] = newAngle;
          particleRadii[i] = newRadius;
          posArray[i * 3]     = Math.cos(newAngle) * newRadius;
          posArray[i * 3 + 1] = 0.35 + Math.random() * 0.5; // just above table
          posArray[i * 3 + 2] = Math.sin(newAngle) * newRadius;
        }
      }
      posAttr.needsUpdate = true;
    },
  };
}