Showcase

One Portal test chamber, built by Gemini 4 Argon

Hostedvia DaBenchOpenRouter
Running live in your browserNew tab
Cost to build
$0.24
Show the code the model wrote
HTML92,256 characters
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Aperture Test Chamber 01</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: #0d1117;
      font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
      color: #f0f4f8;
    }
    #game-canvas {
      display: block;
      width: 100vw;
      height: 100vh;
      cursor: pointer;
    }

    /* Crosshair & Portal Indicators */
    #hud {
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 10;
      opacity: 0;
      transition: opacity 0.3s ease;
    }
    #hud.visible {
      opacity: 1;
    }
    #crosshair {
      position: absolute;
      top: 50%;
      left: 50%;
      width: 48px;
      height: 48px;
      transform: translate(-50%, -50%);
    }
    #hud-top-left {
      position: absolute;
      top: 24px;
      left: 28px;
      background: rgba(14, 20, 28, 0.72);
      border-left: 3px solid #00a8ff;
      padding: 10px 16px;
      backdrop-filter: blur(6px);
      border-radius: 0 6px 6px 0;
      letter-spacing: 0.08em;
      font-size: 12px;
      text-transform: uppercase;
      color: #c8d6e5;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
    }
    #hud-top-left strong {
      display: block;
      font-size: 15px;
      color: #ffffff;
      margin-bottom: 2px;
      letter-spacing: 0.12em;
    }
    #hud-controls {
      position: absolute;
      bottom: 24px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(14, 20, 28, 0.75);
      border: 1px solid rgba(255, 255, 255, 0.12);
      padding: 8px 20px;
      border-radius: 999px;
      font-size: 12px;
      letter-spacing: 0.06em;
      color: #d2dae2;
      backdrop-filter: blur(6px);
      display: flex;
      gap: 18px;
      align-items: center;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
      white-space: nowrap;
    }
    .key-badge {
      display: inline-block;
      background: rgba(255, 255, 255, 0.14);
      border: 1px solid rgba(255, 255, 255, 0.24);
      border-radius: 4px;
      padding: 2px 7px;
      font-weight: 600;
      font-size: 11px;
      color: #fff;
      margin-right: 4px;
    }
    .dot-blue {
      display: inline-block;
      width: 9px;
      height: 9px;
      border-radius: 50%;
      background: #00a8ff;
      box-shadow: 0 0 8px #00a8ff;
      margin-right: 5px;
    }
    .dot-orange {
      display: inline-block;
      width: 9px;
      height: 9px;
      border-radius: 50%;
      background: #ff7700;
      box-shadow: 0 0 8px #ff7700;
      margin-right: 5px;
    }

    /* Demo Overlay (Shown before first click) */
    #demo-overlay {
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 20;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding: 34px 42px;
      background: radial-gradient(circle at center, transparent 55%, rgba(6, 10, 15, 0.45) 100%);
      transition: opacity 0.35s ease;
    }
    #demo-overlay.hidden {
      opacity: 0;
      visibility: hidden;
    }
    .demo-header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
    }
    .chamber-brand {
      background: rgba(12, 17, 24, 0.82);
      border: 1px solid rgba(255, 255, 255, 0.15);
      border-left: 4px solid #ff7700;
      padding: 16px 24px;
      border-radius: 0 10px 10px 0;
      backdrop-filter: blur(10px);
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
    }
    .chamber-brand .sub {
      font-size: 11px;
      text-transform: uppercase;
      letter-spacing: 0.2em;
      color: #8fa4b8;
      margin-bottom: 4px;
    }
    .chamber-brand h1 {
      font-size: 26px;
      font-weight: 800;
      letter-spacing: 0.08em;
      color: #ffffff;
    }
    .demo-pill {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      background: rgba(12, 17, 24, 0.82);
      border: 1px solid rgba(0, 168, 255, 0.4);
      padding: 8px 16px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: #70d6ff;
      backdrop-filter: blur(8px);
    }
    .pulse-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: #00a8ff;
      box-shadow: 0 0 10px #00a8ff;
      animation: pulseGlow 1.4s infinite ease-in-out;
    }
    @keyframes pulseGlow {
      0%, 100% { transform: scale(1); opacity: 1; }
      50% { transform: scale(1.35); opacity: 0.55; }
    }
    .demo-footer {
      align-self: center;
      text-align: center;
      background: rgba(12, 17, 24, 0.86);
      border: 1px solid rgba(255, 255, 255, 0.16);
      padding: 20px 36px;
      border-radius: 14px;
      backdrop-filter: blur(12px);
      box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55);
    }
    .click-prompt {
      font-size: 18px;
      font-weight: 700;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: #ffffff;
      margin-bottom: 10px;
    }
    .demo-controls-row {
      display: flex;
      gap: 20px;
      justify-content: center;
      font-size: 13px;
      color: #b8c7d6;
    }

    /* Pause Banner when pointer lock is exited during gameplay */
    #pause-banner {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(12, 18, 26, 0.9);
      border: 1px solid rgba(255, 255, 255, 0.2);
      padding: 24px 38px;
      border-radius: 12px;
      text-align: center;
      pointer-events: none;
      z-index: 25;
      backdrop-filter: blur(10px);
      box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6);
      opacity: 0;
      visibility: hidden;
      transition: opacity 0.2s ease;
    }
    #pause-banner.visible {
      opacity: 1;
      visibility: visible;
    }
    #pause-banner h2 {
      font-size: 20px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      margin-bottom: 8px;
    }
    #pause-banner p {
      font-size: 13px;
      color: #9fb3c8;
    }

    /* Chamber Complete Modal Card */
    #complete-modal {
      position: fixed;
      inset: 0;
      z-index: 30;
      display: flex;
      align-items: center;
      justify-content: center;
      background: rgba(6, 10, 16, 0.72);
      backdrop-filter: blur(10px);
      opacity: 0;
      visibility: hidden;
      transition: opacity 0.35s ease;
    }
    #complete-modal.visible {
      opacity: 1;
      visibility: visible;
    }
    .complete-card {
      background: linear-gradient(145deg, rgba(20, 28, 38, 0.96), rgba(12, 17, 24, 0.98));
      border: 1px solid rgba(255, 255, 255, 0.18);
      border-top: 4px solid #00a8ff;
      border-radius: 14px;
      padding: 36px 44px;
      width: 440px;
      max-width: 90vw;
      text-align: center;
      box-shadow: 0 24px 64px rgba(0, 0, 0, 0.7);
      transform: translateY(12px);
      transition: transform 0.35s ease;
    }
    #complete-modal.visible .complete-card {
      transform: translateY(0);
    }
    .complete-badge {
      display: inline-block;
      padding: 4px 12px;
      border-radius: 999px;
      background: rgba(0, 168, 255, 0.15);
      border: 1px solid rgba(0, 168, 255, 0.4);
      color: #5cd1ff;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      margin-bottom: 14px;
    }
    .complete-card h2 {
      font-size: 30px;
      font-weight: 800;
      letter-spacing: 0.06em;
      color: #ffffff;
      margin-bottom: 8px;
    }
    .complete-card p.desc {
      font-size: 14px;
      color: #a5b8cc;
      margin-bottom: 24px;
    }
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      margin-bottom: 28px;
    }
    .stat-box {
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 8px;
      padding: 12px 8px;
    }
    .stat-val {
      font-size: 20px;
      font-weight: 700;
      color: #ffffff;
    }
    .stat-lbl {
      font-size: 10px;
      text-transform: uppercase;
      letter-spacing: 0.1em;
      color: #8395a7;
      margin-top: 3px;
    }
    #restart-btn {
      width: 100%;
      padding: 14px 24px;
      font-size: 15px;
      font-weight: 700;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: #ffffff;
      background: linear-gradient(90deg, #0097e6, #00a8ff);
      border: none;
      border-radius: 8px;
      cursor: pointer;
      box-shadow: 0 6px 20px rgba(0, 168, 255, 0.4);
      transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
    }
    #restart-btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 10px 26px rgba(0, 168, 255, 0.6);
      background: linear-gradient(90deg, #00a8ff, #25baff);
    }
    #restart-btn:active {
      transform: translateY(0);
    }
  </style>
</head>
<body>
  <canvas id="game-canvas"></canvas>

  <!-- In-Game HUD -->
  <div id="hud">
    <div id="hud-top-left">
      <strong>CHAMBER 01</strong>
      <span>Objective: Reach Upper Exit</span>
    </div>

    <svg id="crosshair" viewBox="0 0 48 48">
      <!-- Left Arc (Blue Portal State) -->
      <path id="xhair-blue" d="M 19 11 A 14 14 0 0 0 19 37" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="2.6" stroke-linecap="round" />
      <!-- Right Arc (Orange Portal State) -->
      <path id="xhair-orange" d="M 29 11 A 14 14 0 0 1 29 37" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="2.6" stroke-linecap="round" />
      <!-- Center Reticle Dot -->
      <circle cx="24" cy="24" r="2.2" fill="#ffffff" />
    </svg>

    <div id="hud-controls">
      <span><span class="dot-blue"></span><span class="key-badge">L-Click</span>Blue Portal</span>
      <span><span class="dot-orange"></span><span class="key-badge">R-Click</span>Orange Portal</span>
      <span><span class="key-badge">WASD</span>Move</span>
      <span><span class="key-badge">SPACE</span>Jump</span>
      <span><span class="key-badge">R</span>Reset Portals</span>
    </div>
  </div>

  <!-- Attract / Demo Overlay -->
  <div id="demo-overlay">
    <div class="demo-header">
      <div class="chamber-brand">
        <div class="sub">Aperture Laboratories // Enrichment Center</div>
        <h1>TEST CHAMBER 01</h1>
      </div>
      <div class="demo-pill">
        <span class="pulse-dot"></span>
        <span>Live Portal Demo Loop</span>
      </div>
    </div>

    <div class="demo-footer">
      <div class="click-prompt">Click Anywhere to Start Chamber</div>
      <div class="demo-controls-row">
        <span><span class="key-badge">Mouse</span>Look</span>
        <span><span class="key-badge">WASD</span>Move</span>
        <span><span class="key-badge">Space</span>Jump</span>
        <span><span class="dot-blue"></span><span class="key-badge">LMB</span>Blue</span>
        <span><span class="dot-orange"></span><span class="key-badge">RMB</span>Orange</span>
      </div>
    </div>
  </div>

  <!-- Pause Banner -->
  <div id="pause-banner">
    <h2>Simulation Paused</h2>
    <p>Click anywhere on the chamber view to lock mouse and resume</p>
  </div>

  <!-- Chamber Complete Card -->
  <div id="complete-modal">
    <div class="complete-card">
      <div class="complete-badge">Testing Protocol Complete</div>
      <h2>Chamber complete</h2>
      <p class="desc">Exemplary portal placement and spatial traversal recorded.</p>
      <div class="stats-grid">
        <div class="stat-box">
          <div class="stat-val" id="stat-time">0.0s</div>
          <div class="stat-lbl">Time</div>
        </div>
        <div class="stat-box">
          <div class="stat-val" id="stat-shots">0</div>
          <div class="stat-lbl">Portals Placed</div>
        </div>
        <div class="stat-box">
          <div class="stat-val" id="stat-steps">0</div>
          <div class="stat-lbl">Traversals</div>
        </div>
      </div>
      <button id="restart-btn" type="button">Restart Chamber</button>
    </div>
  </div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';

    // =========================================================================
    // CONSTANTS & CONFIGURATION
    // =========================================================================
    const PORTAL_RX = 0.68;        // Horizontal semi-axis of portal (1.36m wide)
    const PORTAL_RY = 1.18;        // Vertical semi-axis of portal (2.36m tall)
    const PLAYER_EYE_HEIGHT = 1.55;
    const PLAYER_RADIUS = 0.36;
    const GRAVITY = 15.0;
    const JUMP_SPEED = 5.4;
    const MOVE_SPEED = 5.5;
    const CAMERA_NEAR = 0.035;
    const CAMERA_FAR = 65.0;
    const CAMERA_FOV = 72;

    const COLOR_BLUE = new THREE.Color(0x00a8ff);
    const COLOR_ORANGE = new THREE.Color(0xff7700);

    // =========================================================================
    // PROCEDURAL WEB AUDIO SYNTHESIZER
    // =========================================================================
    class SoundSystem {
      constructor() {
        this.ctx = null;
      }

      init() {
        if (!this.ctx) {
          const AudioCtx = window.AudioContext || window.webkitAudioContext;
          if (AudioCtx) this.ctx = new AudioCtx();
        }
        if (this.ctx && this.ctx.state === 'suspended') {
          this.ctx.resume().catch(() => {});
        }
      }

      playShoot(isBlue) {
        if (!this.ctx) return;
        try {
          const now = this.ctx.currentTime;
          const osc = this.ctx.createOscillator();
          const gain = this.ctx.createGain();
          osc.type = 'sawtooth';
          const baseFreq = isBlue ? 520 : 420;
          osc.frequency.setValueAtTime(baseFreq, now);
          osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.9, now + 0.06);
          osc.frequency.exponentialRampToValueAtTime(140, now + 0.22);

          gain.gain.setValueAtTime(0.16, now);
          gain.gain.exponentialRampToValueAtTime(0.001, now + 0.23);

          osc.connect(gain);
          gain.connect(this.ctx.destination);
          osc.start(now);
          osc.stop(now + 0.24);
        } catch (e) {}
      }

      playPortalOpen(isBlue) {
        if (!this.ctx) return;
        try {
          const now = this.ctx.currentTime;
          const freqs = isBlue ? [330, 495, 660] : [293.66, 440, 587.33];
          freqs.forEach((f, idx) => {
            const osc = this.ctx.createOscillator();
            const gain = this.ctx.createGain();
            osc.type = 'sine';
            osc.frequency.setValueAtTime(f * 0.85, now);
            osc.frequency.exponentialRampToValueAtTime(f, now + 0.14);
            gain.gain.setValueAtTime(0.05 / (idx + 1), now);
            gain.gain.exponentialRampToValueAtTime(0.001, now + 0.26);
            osc.connect(gain);
            gain.connect(this.ctx.destination);
            osc.start(now);
            osc.stop(now + 0.27);
          });
        } catch (e) {}
      }

      playFizzle() {
        if (!this.ctx) return;
        try {
          const now = this.ctx.currentTime;
          const bufferSize = this.ctx.sampleRate * 0.2;
          const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
          const data = buffer.getChannelData(0);
          for (let i = 0; i < bufferSize; i++) {
            data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.35));
          }
          const noise = this.ctx.createBufferSource();
          noise.buffer = buffer;

          const filter = this.ctx.createBiquadFilter();
          filter.type = 'bandpass';
          filter.frequency.setValueAtTime(1600, now);
          filter.frequency.exponentialRampToValueAtTime(420, now + 0.19);
          filter.Q.value = 2.5;

          const gain = this.ctx.createGain();
          gain.gain.setValueAtTime(0.22, now);
          gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);

          noise.connect(filter);
          filter.connect(gain);
          gain.connect(this.ctx.destination);
          noise.start(now);
        } catch (e) {}
      }

      playTraverse() {
        if (!this.ctx) return;
        try {
          const now = this.ctx.currentTime;
          const osc = this.ctx.createOscillator();
          const gain = this.ctx.createGain();
          osc.type = 'sine';
          osc.frequency.setValueAtTime(160, now);
          osc.frequency.exponentialRampToValueAtTime(320, now + 0.08);
          osc.frequency.exponentialRampToValueAtTime(110, now + 0.22);

          gain.gain.setValueAtTime(0.14, now);
          gain.gain.exponentialRampToValueAtTime(0.001, now + 0.23);

          osc.connect(gain);
          gain.connect(this.ctx.destination);
          osc.start(now);
          osc.stop(now + 0.24);
        } catch (e) {}
      }

      playFootstep() {
        if (!this.ctx) return;
        try {
          const now = this.ctx.currentTime;
          const osc = this.ctx.createOscillator();
          const gain = this.ctx.createGain();
          osc.type = 'triangle';
          osc.frequency.setValueAtTime(115 + Math.random() * 25, now);
          osc.frequency.exponentialRampToValueAtTime(45, now + 0.055);
          gain.gain.setValueAtTime(0.045, now);
          gain.gain.exponentialRampToValueAtTime(0.001, now + 0.06);
          osc.connect(gain);
          gain.connect(this.ctx.destination);
          osc.start(now);
          osc.stop(now + 0.065);
        } catch (e) {}
      }

      playVictory() {
        if (!this.ctx) return;
        try {
          const now = this.ctx.currentTime;
          const notes = [523.25, 659.25, 783.99, 1046.5];
          notes.forEach((freq, i) => {
            const osc = this.ctx.createOscillator();
            const gain = this.ctx.createGain();
            osc.type = 'triangle';
            const t = now + i * 0.11;
            osc.frequency.setValueAtTime(freq, t);
            gain.gain.setValueAtTime(0.0001, now);
            gain.gain.setValueAtTime(0.12, t);
            gain.gain.exponentialRampToValueAtTime(0.001, t + 0.55);
            osc.connect(gain);
            gain.connect(this.ctx.destination);
            osc.start(t);
            osc.stop(t + 0.56);
          });
        } catch (e) {}
      }
    }

    const sounds = new SoundSystem();

    // =========================================================================
    // RENDERER, SCENE & CAMERAS
    // =========================================================================
    const canvas = document.getElementById('game-canvas');
    const renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      powerPreference: 'high-performance'
    });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.NoToneMapping;
    renderer.localClippingEnabled = true;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x161c24);
    scene.fog = new THREE.FogExp2(0x161c24, 0.015);

    const mainCamera = new THREE.PerspectiveCamera(
      CAMERA_FOV,
      window.innerWidth / window.innerHeight,
      CAMERA_NEAR,
      CAMERA_FAR
    );
    const virtualCamera = new THREE.PerspectiveCamera(
      CAMERA_FOV,
      window.innerWidth / window.innerHeight,
      CAMERA_NEAR,
      CAMERA_FAR
    );
    const recursiveCamera = new THREE.PerspectiveCamera(
      CAMERA_FOV,
      window.innerWidth / window.innerHeight,
      CAMERA_NEAR,
      CAMERA_FAR
    );

    // First-person Portal Gun overlay scene so gun never clips into walls
    const weaponScene = new THREE.Scene();
    const weaponCamera = new THREE.PerspectiveCamera(
      50,
      window.innerWidth / window.innerHeight,
      0.05,
      10
    );

    // =========================================================================
    // PROCEDURAL TEXTURE GENERATORS
    // =========================================================================
    function createWhitePanelTexture() {
      const size = 512;
      const c = document.createElement('canvas');
      c.width = size;
      c.height = size;
      const ctx = c.getContext('2d');

      // Matte off-white ceramic base
      ctx.fillStyle = '#eaeeef';
      ctx.fillRect(0, 0, size, size);

      // Subtle panel variation
      const tiles = 2;
      const step = size / tiles;
      for (let y = 0; y < tiles; y++) {
        for (let x = 0; x < tiles; x++) {
          const shade = 232 + ((x + y) % 2) * 5;
          ctx.fillStyle = `rgb(${shade}, ${shade + 3}, ${shade + 5})`;
          ctx.fillRect(x * step + 4, y * step + 4, step - 8, step - 8);

          // Inner subtle bevel highlight
          ctx.strokeStyle = 'rgba(255, 255, 255, 0.75)';
          ctx.lineWidth = 2;
          ctx.strokeRect(x * step + 5, y * step + 5, step - 10, step - 10);

          // Subtle corner pin marks
          ctx.fillStyle = 'rgba(130, 145, 160, 0.35)';
          const m = 14;
          ctx.fillRect(x * step + m, y * step + m, 3, 3);
          ctx.fillRect((x + 1) * step - m - 3, y * step + m, 3, 3);
          ctx.fillRect(x * step + m, (y + 1) * step - m - 3, 3, 3);
          ctx.fillRect((x + 1) * step - m - 3, (y + 1) * step - m - 3, 3, 3);
        }
      }

      // Crisp dark seams
      ctx.strokeStyle = '#9aa6b2';
      ctx.lineWidth = 6;
      for (let i = 0; i <= tiles; i++) {
        ctx.beginPath();
        ctx.moveTo(i * step, 0);
        ctx.lineTo(i * step, size);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(0, i * step);
        ctx.lineTo(size, i * step);
        ctx.stroke();
      }

      // Fine seam core
      ctx.strokeStyle = '#6c7885';
      ctx.lineWidth = 2;
      for (let i = 0; i <= tiles; i++) {
        ctx.beginPath();
        ctx.moveTo(i * step, 0);
        ctx.lineTo(i * step, size);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(0, i * step);
        ctx.lineTo(size, i * step);
        ctx.stroke();
      }

      const tex = new THREE.CanvasTexture(c);
      tex.wrapS = THREE.RepeatWrapping;
      tex.wrapT = THREE.RepeatWrapping;
      tex.colorSpace = THREE.SRGBColorSpace;
      tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
      return tex;
    }

    function createDarkMetalTexture() {
      const size = 512;
      const c = document.createElement('canvas');
      c.width = size;
      c.height = size;
      const ctx = c.getContext('2d');

      ctx.fillStyle = '#262d36';
      ctx.fillRect(0, 0, size, size);

      const tiles = 2;
      const step = size / tiles;
      for (let y = 0; y < tiles; y++) {
        for (let x = 0; x < tiles; x++) {
          const base = 37 + ((x + y) % 2) * 4;
          ctx.fillStyle = `rgb(${base}, ${base + 6}, ${base + 14})`;
          ctx.fillRect(x * step + 6, y * step + 6, step - 12, step - 12);

          // Recessed inner plate
          ctx.strokeStyle = '#36404d';
          ctx.lineWidth = 2;
          ctx.strokeRect(x * step + 14, y * step + 14, step - 28, step - 28);
        }
      }

      // Deep dark metal seams
      ctx.strokeStyle = '#15191f';
      ctx.lineWidth = 8;
      for (let i = 0; i <= tiles; i++) {
        ctx.beginPath();
        ctx.moveTo(i * step, 0);
        ctx.lineTo(i * step, size);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(0, i * step);
        ctx.lineTo(size, i * step);
        ctx.stroke();
      }

      const tex = new THREE.CanvasTexture(c);
      tex.wrapS = THREE.RepeatWrapping;
      tex.wrapT = THREE.RepeatWrapping;
      tex.colorSpace = THREE.SRGBColorSpace;
      tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
      return tex;
    }

    function createFloorTexture() {
      const size = 512;
      const c = document.createElement('canvas');
      c.width = size;
      c.height = size;
      const ctx = c.getContext('2d');

      ctx.fillStyle = '#1f252d';
      ctx.fillRect(0, 0, size, size);

      const tiles = 4;
      const step = size / tiles;
      for (let y = 0; y < tiles; y++) {
        for (let x = 0; x < tiles; x++) {
          const v = 32 + ((x + y) % 2) * 3;
          ctx.fillStyle = `rgb(${v}, ${v + 6}, ${v + 12})`;
          ctx.fillRect(x * step + 3, y * step + 3, step - 6, step - 6);
        }
      }

      ctx.strokeStyle = '#13171c';
      ctx.lineWidth = 4;
      for (let i = 0; i <= tiles; i++) {
        ctx.beginPath();
        ctx.moveTo(i * step, 0);
        ctx.lineTo(i * step, size);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(0, i * step);
        ctx.lineTo(size, i * step);
        ctx.stroke();
      }

      const tex = new THREE.CanvasTexture(c);
      tex.wrapS = THREE.RepeatWrapping;
      tex.wrapT = THREE.RepeatWrapping;
      tex.colorSpace = THREE.SRGBColorSpace;
      tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
      return tex;
    }

    function createChamberSignTexture() {
      const c = document.createElement('canvas');
      c.width = 512;
      c.height = 768;
      const ctx = c.getContext('2d');

      // Backlit white display background
      const grad = ctx.createLinearGradient(0, 0, 0, 768);
      grad.addColorStop(0, '#f4f7f9');
      grad.addColorStop(1, '#dce4ec');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, 512, 768);

      // Outer border
      ctx.strokeStyle = '#222a34';
      ctx.lineWidth = 18;
      ctx.strokeRect(9, 9, 494, 750);

      // Giant "01"
      ctx.fillStyle = '#182028';
      ctx.font = 'bold 250px "Segoe UI", Arial, sans-serif';
      ctx.fillText('01', 42, 265);

      // Divider line & progress ticks
      ctx.fillRect(46, 310, 420, 8);
      for (let i = 0; i < 10; i++) {
        ctx.fillStyle = i < 3 ? '#182028' : 'rgba(24, 32, 40, 0.22)';
        ctx.fillRect(46 + i * 43, 334, 30, 16);
      }

      ctx.fillStyle = '#182028';
      ctx.font = 'bold 28px "Segoe UI", Arial, sans-serif';
      ctx.fillText('PORTAL TRAVERSAL', 46, 405);
      ctx.font = '600 20px "Segoe UI", Arial, sans-serif';
      ctx.fillStyle = '#485665';
      ctx.fillText('CHAMBER 01 // HIGH LEDGE EXIT', 46, 438);

      // Icon boxes at bottom
      const iconsY = 500;
      const boxS = 124;
      for (let i = 0; i < 3; i++) {
        const bx = 46 + i * 148;
        ctx.strokeStyle = '#182028';
        ctx.lineWidth = 6;
        ctx.strokeRect(bx, iconsY, boxS, boxS);
      }

      // Icon 1: Portal entry
      ctx.strokeStyle = '#0088dd';
      ctx.lineWidth = 8;
      ctx.beginPath();
      ctx.ellipse(46 + 62, iconsY + 62, 26, 42, 0, 0, Math.PI * 2);
      ctx.stroke();

      // Icon 2: Portal exit
      ctx.strokeStyle = '#ee6600';
      ctx.lineWidth = 8;
      ctx.beginPath();
      ctx.ellipse(46 + 148 + 62, iconsY + 62, 26, 42, 0, 0, Math.PI * 2);
      ctx.stroke();

      // Icon 3: High ledge person reaching door
      ctx.fillStyle = '#182028';
      ctx.fillRect(46 + 296 + 20, iconsY + 78, 84, 10);
      ctx.beginPath();
      ctx.arc(46 + 296 + 62, iconsY + 38, 11, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillRect(46 + 296 + 54, iconsY + 52, 16, 24);

      const tex = new THREE.CanvasTexture(c);
      tex.colorSpace = THREE.SRGBColorSpace;
      return tex;
    }

    const whitePanelBaseTex = createWhitePanelTexture();
    const darkMetalBaseTex = createDarkMetalTexture();
    const floorBaseTex = createFloorTexture();

    // =========================================================================
    // LEVEL / TEST CHAMBER CONSTRUCTION
    // =========================================================================
    const shootableMeshes = [];
    const boxColliders = [];
    const whitePanelRegistry = {};

    function addWallBox({
      x, y, z, w, h, d,
      material,
      collider = true,
      shootable = true,
      castShadow = true,
      receiveShadow = true
    }) {
      const geo = new THREE.BoxGeometry(w, h, d);
      const mesh = new THREE.Mesh(geo, material);
      mesh.position.set(x, y, z);
      mesh.castShadow = castShadow;
      mesh.receiveShadow = receiveShadow;
      mesh.userData.portalable = false;
      scene.add(mesh);

      if (shootable) shootableMeshes.push(mesh);
      if (collider) {
        boxColliders.push({
          minX: x - w / 2,
          maxX: x + w / 2,
          minY: y - h / 2,
          maxY: y + h / 2,
          minZ: z - d / 2,
          maxZ: z + d / 2,
          panelId: null
        });
      }
      return mesh;
    }

    function makeTiledMaterial(baseTex, repeatX, repeatY, roughness = 0.55, metalness = 0.25) {
      const tex = baseTex.clone();
      tex.needsUpdate = true;
      tex.repeat.set(repeatX, repeatY);
      return new THREE.MeshStandardMaterial({
        map: tex,
        roughness,
        metalness
      });
    }

    // Add a white portalable panel flush on a wall surface
    function addWhitePanel({
      id,
      center,
      normal,
      width,
      height,
      hostColliderIndex = -1
    }) {
      const thickness = 0.08;
      const geo = new THREE.BoxGeometry(width, height, thickness);
      const mat = makeTiledMaterial(whitePanelBaseTex, width / 1.9, height / 1.9, 0.35, 0.06);
      const mesh = new THREE.Mesh(geo, mat);

      // Orient mesh so local +Z points along `normal`
      const up = new THREE.Vector3(0, 1, 0);
      const right = new THREE.Vector3().crossVectors(up, normal).normalize();
      const rotMat = new THREE.Matrix4().makeBasis(right, up, normal);
      mesh.quaternion.setFromRotationMatrix(rotMat);

      // Position slightly recessed so front face is at `center`
      mesh.position.copy(center).addScaledVector(normal, -thickness * 0.5);
      mesh.castShadow = true;
      mesh.receiveShadow = true;

      // Dark metallic border frame around the white panel
      const frameMat = new THREE.MeshStandardMaterial({
        color: 0x1a2028,
        roughness: 0.4,
        metalness: 0.7
      });
      const borderT = 0.08;
      const topBar = new THREE.Mesh(new THREE.BoxGeometry(width + borderT * 2, borderT, thickness + 0.02), frameMat);
      topBar.position.set(0, height / 2 + borderT / 2, 0);
      const botBar = new THREE.Mesh(new THREE.BoxGeometry(width + borderT * 2, borderT, thickness + 0.02), frameMat);
      botBar.position.set(0, -height / 2 - borderT / 2, 0);
      const leftBar = new THREE.Mesh(new THREE.BoxGeometry(borderT, height, thickness + 0.02), frameMat);
      leftBar.position.set(-width / 2 - borderT / 2, 0, 0);
      const rightBar = new THREE.Mesh(new THREE.BoxGeometry(borderT, height, thickness + 0.02), frameMat);
      rightBar.position.set(width / 2 + borderT / 2, 0, 0);
      mesh.add(topBar, botBar, leftBar, rightBar);

      const panelInfo = {
        id,
        center: center.clone(),
        normal: normal.clone(),
        up: up.clone(),
        right: right.clone(),
        halfWidth: width * 0.5,
        halfHeight: height * 0.5,
        hostColliderIndex
      };

      mesh.userData.portalable = true;
      mesh.userData.panelInfo = panelInfo;
      whitePanelRegistry[id] = panelInfo;

      scene.add(mesh);
      shootableMeshes.push(mesh);
      return mesh;
    }

    // Build Test Chamber 01
    let exitDoorLeft, exitDoorRight, exitLightMat;
    let doorOpenAmount = 0;

    function buildTestChamber() {
      // 1. Lower Floor (z from -3.5 to +9.0, y = 0)
      addWallBox({
        x: 0, y: -0.25, z: 2.75,
        w: 14, h: 0.5, d: 12.5,
        material: makeTiledMaterial(floorBaseTex, 7, 6.25, 0.45, 0.35)
      });

      // 2. High Ledge Floor (z from -9.0 to -3.5, y = 4.4)
      addWallBox({
        x: 0, y: 4.15, z: -6.25,
        w: 14, h: 0.5, d: 5.5,
        material: makeTiledMaterial(floorBaseTex, 7, 2.75, 0.45, 0.35)
      });

      // 3. High Ledge Cliff Wall (at z = -3.5, rising from y = 0 to y = 4.4)
      const cliffColliderIdx = boxColliders.length;
      addWallBox({
        x: 0, y: 2.2, z: -3.75,
        w: 14, h: 4.4, d: 0.5,
        material: makeTiledMaterial(darkMetalBaseTex, 7, 2.2, 0.5, 0.45)
      });

      // Yellow/black industrial edge trim along the top of the high ledge (z = -3.5, y = 4.4)
      const edgeTrimMat = new THREE.MeshStandardMaterial({
        color: 0xd49b27,
        roughness: 0.4,
        metalness: 0.5
      });
      addWallBox({
        x: 0, y: 4.38, z: -3.48,
        w: 14, h: 0.08, d: 0.08,
        material: edgeTrimMat,
        collider: false,
        shootable: false
      });

      // Glass safety railings on the left and right edges of the High Ledge (leaving center open)
      const glassMat = new THREE.MeshPhysicalMaterial({
        color: 0x99d8ff,
        transparent: true,
        opacity: 0.26,
        roughness: 0.1,
        metalness: 0.1,
        transmission: 0.5
      });
      const postMat = new THREE.MeshStandardMaterial({ color: 0x485460, metalness: 0.7, roughness: 0.3 });
      // Left railing
      addWallBox({ x: -5.3, y: 4.95, z: -3.52, w: 3.2, h: 1.05, d: 0.04, material: glassMat });
      addWallBox({ x: -3.7, y: 4.95, z: -3.52, w: 0.08, h: 1.1, d: 0.08, material: postMat });
      // Right railing
      addWallBox({ x: 5.3, y: 4.95, z: -3.52, w: 3.2, h: 1.05, d: 0.04, material: glassMat });
      addWallBox({ x: 3.7, y: 4.95, z: -3.52, w: 0.08, h: 1.1, d: 0.08, material: postMat });

      // 4. Outer Room Walls (x = -7, x = +7, z = +9, z = -9, ceiling y = 9.2)
      const westWallIdx = boxColliders.length;
      addWallBox({
        x: -7.25, y: 4.6, z: 0,
        w: 0.5, h: 9.2, d: 18,
        material: makeTiledMaterial(darkMetalBaseTex, 9, 4.6, 0.5, 0.45)
      });

      const eastWallIdx = boxColliders.length;
      addWallBox({
        x: 7.25, y: 4.6, z: 0,
        w: 0.5, h: 9.2, d: 18,
        material: makeTiledMaterial(darkMetalBaseTex, 9, 4.6, 0.5, 0.45)
      });

      // South wall (behind player start)
      addWallBox({
        x: 0, y: 4.6, z: 9.25,
        w: 14, h: 9.2, d: 0.5,
        material: makeTiledMaterial(darkMetalBaseTex, 7, 4.6, 0.5, 0.45)
      });

      // North wall on the High Ledge (with doorway opening around x = 3.2, y = 4.4..7.0)
      // Left section of North Wall (x from -7 to +1.85)
      const northWallLeftIdx = boxColliders.length;
      addWallBox({
        x: -2.575, y: 6.8, z: -9.25,
        w: 8.85, h: 4.8, d: 0.5,
        material: makeTiledMaterial(darkMetalBaseTex, 4.4, 2.4, 0.5, 0.45)
      });
      // Right section of North Wall (x from +4.55 to +7)
      addWallBox({
        x: 5.775, y: 6.8, z: -9.25,
        w: 2.45, h: 4.8, d: 0.5,
        material: makeTiledMaterial(darkMetalBaseTex, 1.2, 2.4, 0.5, 0.45)
      });
      // Top section above Exit Door (x from +1.85 to +4.55, y from 7.0 to 9.2)
      addWallBox({
        x: 3.2, y: 8.1, z: -9.25,
        w: 2.7, h: 2.2, d: 0.5,
        material: makeTiledMaterial(darkMetalBaseTex, 1.35, 1.1, 0.5, 0.45)
      });

      // Ceiling (y = 9.2)
      addWallBox({
        x: 0, y: 9.45, z: 0,
        w: 14, h: 0.5, d: 18,
        material: makeTiledMaterial(darkMetalBaseTex, 7, 9, 0.6, 0.4)
      });

      // 5. White Portalable Wall Panels
      // Panel A: Lower Cliff Wall Panel (used in Demo & convenient for player)
      addWhitePanel({
        id: 'lower_cliff',
        center: new THREE.Vector3(-3.0, 1.44, -3.45),
        normal: new THREE.Vector3(0, 0, 1),
        width: 3.8,
        height: 2.72,
        hostColliderIndex: cliffColliderIdx
      });

      // Panel B: Lower West Wall Panel (used in Demo & convenient for player)
      addWhitePanel({
        id: 'lower_west',
        center: new THREE.Vector3(-6.95, 1.44, 2.4),
        normal: new THREE.Vector3(1, 0, 0),
        width: 3.8,
        height: 2.72,
        hostColliderIndex: westWallIdx
      });

      // Panel C: Lower East Wall Panel (faces West panel for fun portal recursion!)
      addWhitePanel({
        id: 'lower_east',
        center: new THREE.Vector3(6.95, 1.44, 2.4),
        normal: new THREE.Vector3(-1, 0, 0),
        width: 3.8,
        height: 2.72,
        hostColliderIndex: eastWallIdx
      });

      // Panel D: High Ledge North Wall Panel (The Puzzle Target!)
      addWhitePanel({
        id: 'upper_ledge',
        center: new THREE.Vector3(-3.0, 5.84, -8.95),
        normal: new THREE.Vector3(0, 0, 1),
        width: 4.2,
        height: 2.72,
        hostColliderIndex: northWallLeftIdx
      });

      // 6. Exit Door & Exit Vestibule on the High Ledge (x = 3.2, y = 4.4, z = -9.0)
      buildExitDoorAndVestibule();

      // 7. Architectural Details: Observation Window, Chamber Sign, Indicator Lights, Ceiling Strips
      buildArchitecturalDetails();
    }

    function buildExitDoorAndVestibule() {
      const doorX = 3.2;
      const doorY = 4.4;
      const doorZ = -9.0;

      // Vestibule floor, ceiling, walls beyond z = -9.0
      const vestMat = new THREE.MeshStandardMaterial({
        color: 0xdce4ec,
        roughness: 0.35,
        metalness: 0.15
      });
      // Floor
      addWallBox({
        x: doorX, y: 4.15, z: -11.0,
        w: 3.6, h: 0.5, d: 4.0,
        material: makeTiledMaterial(floorBaseTex, 2, 2, 0.35, 0.3)
      });
      // Ceiling
      addWallBox({ x: doorX, y: 7.25, z: -11.0, w: 3.6, h: 0.4, d: 4.0, material: vestMat });
      // Left wall
      addWallBox({ x: doorX - 1.7, y: 5.7, z: -11.0, w: 0.3, h: 2.8, d: 4.0, material: vestMat });
      // Right wall
      addWallBox({ x: doorX + 1.7, y: 5.7, z: -11.0, w: 0.3, h: 2.8, d: 4.0, material: vestMat });
      // Back wall of vestibule
      addWallBox({ x: doorX, y: 5.7, z: -12.9, w: 3.6, h: 2.8, d: 0.3, material: vestMat });

      // Glowing cyan exit portal pad at back of vestibule
      const padMat = new THREE.MeshBasicMaterial({ color: 0x5cd1ff });
      const backGlow = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 2.2), padMat);
      backGlow.position.set(doorX, 5.6, -12.72);
      scene.add(backGlow);

      const vestLight = new THREE.PointLight(0x66d9ff, 3.5, 7);
      vestLight.position.set(doorX, 6.4, -10.8);
      scene.add(vestLight);

      // Heavy Aperture-style Door Frame
      const frameMat = new THREE.MeshStandardMaterial({
        color: 0x181e26,
        roughness: 0.35,
        metalness: 0.75
      });
      addWallBox({ x: doorX - 1.35, y: doorY + 1.35, z: doorZ + 0.05, w: 0.22, h: 2.7, d: 0.55, material: frameMat });
      addWallBox({ x: doorX + 1.35, y: doorY + 1.35, z: doorZ + 0.05, w: 0.22, h: 2.7, d: 0.55, material: frameMat });
      addWallBox({ x: doorX, y: doorY + 2.65, z: doorZ + 0.05, w: 2.9, h: 0.24, d: 0.55, material: frameMat });

      // Overhead Exit Sign Badge
      exitLightMat = new THREE.MeshBasicMaterial({ color: 0x00e676 });
      const signBadge = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.28, 0.1), exitLightMat);
      signBadge.position.set(doorX, doorY + 2.95, doorZ + 0.26);
      scene.add(signBadge);

      // Sliding Door Halves
      const doorLeafMat = new THREE.MeshStandardMaterial({
        color: 0x3b4654,
        roughness: 0.35,
        metalness: 0.65
      });
      const trimMat = new THREE.MeshBasicMaterial({ color: 0x00d8ff });

      exitDoorLeft = new THREE.Group();
      const leftMesh = new THREE.Mesh(new THREE.BoxGeometry(1.25, 2.55, 0.18), doorLeafMat);
      leftMesh.castShadow = true;
      leftMesh.receiveShadow = true;
      const leftStripe = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.2, 0.2), trimMat);
      leftStripe.position.set(0.56, 0, 0);
      exitDoorLeft.add(leftMesh, leftStripe);
      exitDoorLeft.position.set(doorX - 0.62, doorY + 1.27, doorZ);
      scene.add(exitDoorLeft);

      exitDoorRight = new THREE.Group();
      const rightMesh = new THREE.Mesh(new THREE.BoxGeometry(1.25, 2.55, 0.18), doorLeafMat);
      rightMesh.castShadow = true;
      rightMesh.receiveShadow = true;
      const rightStripe = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.2, 0.2), trimMat);
      rightStripe.position.set(-0.56, 0, 0);
      exitDoorRight.add(rightMesh, rightStripe);
      exitDoorRight.position.set(doorX + 0.62, doorY + 1.27, doorZ);
      scene.add(exitDoorRight);
    }

    function buildArchitecturalDetails() {
      // 1. Illuminated Test Chamber 01 Sign on the right side of the Cliff Wall
      const signTex = createChamberSignTexture();
      const signMat = new THREE.MeshBasicMaterial({ map: signTex });
      const signMesh = new THREE.Mesh(new THREE.PlaneGeometry(1.7, 2.55), signMat);
      signMesh.position.set(2.9, 2.2, -3.47);
      scene.add(signMesh);

      // Frame around sign
      const signFrameMat = new THREE.MeshStandardMaterial({ color: 0x151a21, metalness: 0.7, roughness: 0.4 });
      addWallBox({
        x: 2.9, y: 2.2, z: -3.5,
        w: 1.82, h: 2.67, d: 0.04,
        material: signFrameMat,
        collider: false,
        shootable: false
      });

      // 2. High Observation Window on the East Wall (x = 6.95, y = 6.4, z = 1.5)
      const obsBackMat = new THREE.MeshBasicMaterial({ color: 0xcfe9f7 });
      const obsWindow = new THREE.Mesh(new THREE.PlaneGeometry(6.8, 2.4), obsBackMat);
      obsWindow.rotation.y = -Math.PI / 2;
      obsWindow.position.set(6.96, 6.5, 1.5);
      scene.add(obsWindow);

      // Mullions over Observation Window
      const mullionMat = new THREE.MeshStandardMaterial({ color: 0x1a2028, metalness: 0.7, roughness: 0.35 });
      for (let i = -3; i <= 3; i++) {
        addWallBox({
          x: 6.94, y: 6.5, z: 1.5 + i * 1.1,
          w: 0.1, h: 2.5, d: 0.08,
          material: mullionMat,
          collider: false,
          shootable: false
        });
      }
      addWallBox({ x: 6.94, y: 7.72, z: 1.5, w: 0.12, h: 0.12, d: 7.0, material: mullionMat, collider: false, shootable: false });
      addWallBox({ x: 6.94, y: 5.28, z: 1.5, w: 0.12, h: 0.12, d: 7.0, material: mullionMat, collider: false, shootable: false });

      // 3. Glowing Indicator Dots on the High Ledge leading from Upper White Panel to Exit Door
      const dotGeo = new THREE.BoxGeometry(0.16, 0.02, 0.16);
      const dotMat = new THREE.MeshBasicMaterial({ color: 0x00c8ff });
      const ledgePathPoints = [
        [-3.0, -8.3], [-3.0, -7.7], [-3.0, -7.1], [-2.4, -7.1], [-1.6, -7.1],
        [-0.8, -7.1], [0.0, -7.1], [0.8, -7.1], [1.6, -7.1], [2.4, -7.1],
        [3.2, -7.1], [3.2, -7.7], [3.2, -8.3]
      ];
      ledgePathPoints.forEach(([px, pz]) => {
        const dot = new THREE.Mesh(dotGeo, dotMat);
        dot.position.set(px, 4.41, pz);
        scene.add(dot);
      });

      // 4. Recessed Ceiling Light Strips
      const lightStripMat = new THREE.MeshBasicMaterial({ color: 0xeaf6ff });
      const stripPositions = [-6.2, -1.2, 4.2];
      stripPositions.forEach((sz) => {
        const strip = new THREE.Mesh(new THREE.BoxGeometry(9.2, 0.08, 0.55), lightStripMat);
        strip.position.set(0, 9.18, sz);
        scene.add(strip);
      });

      // 5. Starting Pad Ring at Player Spawn (x = 0, z = 6.2)
      const padRingMat = new THREE.MeshStandardMaterial({
        color: 0x34404d,
        metalness: 0.65,
        roughness: 0.35
      });
      const padCyl = new THREE.Mesh(new THREE.CylinderGeometry(1.3, 1.45, 0.06, 32), padRingMat);
      padCyl.position.set(0, 0.03, 6.2);
      padCyl.receiveShadow = true;
      scene.add(padCyl);

      const padGlow = new THREE.Mesh(
        new THREE.RingGeometry(1.05, 1.16, 32),
        new THREE.MeshBasicMaterial({ color: 0x00a8ff, side: THREE.DoubleSide })
      );
      padGlow.rotation.x = -Math.PI / 2;
      padGlow.position.set(0, 0.065, 6.2);
      scene.add(padGlow);
    }

    // =========================================================================
    // LIGHTING WITH SOFT SHADOWS
    // =========================================================================
    function setupLighting() {
      const hemiLight = new THREE.HemisphereLight(0xddeeff, 0x26303d, 0.85);
      scene.add(hemiLight);

      // Main overhead directional light casting soft shadows across the chamber
      const dirLight = new THREE.DirectionalLight(0xf5faff, 1.35);
      dirLight.position.set(4.5, 8.8, 3.5);
      dirLight.target.position.set(-1.5, 0, -2.5);
      dirLight.castShadow = true;
      dirLight.shadow.mapSize.width = 2048;
      dirLight.shadow.mapSize.height = 2048;
      dirLight.shadow.camera.near = 0.5;
      dirLight.shadow.camera.far = 22;
      const d = 10.5;
      dirLight.shadow.camera.left = -d;
      dirLight.shadow.camera.right = d;
      dirLight.shadow.camera.top = d;
      dirLight.shadow.camera.bottom = -d;
      dirLight.shadow.bias = -0.0005;
      dirLight.shadow.radius = 3.5;
      scene.add(dirLight);
      scene.add(dirLight.target);

      // Secondary fill light illuminating the High Ledge & White Panel
      const ledgeLight = new THREE.SpotLight(0xeef7ff, 18.0, 16, Math.PI / 3, 0.6, 1.2);
      ledgeLight.position.set(-1.5, 8.7, -4.5);
      ledgeLight.target.position.set(-2.5, 5.2, -8.9);
      scene.add(ledgeLight);
      scene.add(ledgeLight.target);

      // Observation window warm/cyan glow fill
      const obsLight = new THREE.PointLight(0xbbe8ff, 4.5, 14);
      obsLight.position.set(5.8, 6.2, 1.5);
      scene.add(obsLight);
    }

    // =========================================================================
    // PORTAL GUN VIEWMODEL
    // =========================================================================
    let gunGroup;
    let gunCoreMat;
    let gunMuzzleLight;
    let gunRecoil = 0;

    function buildPortalGun() {
      gunGroup = new THREE.Group();

      const wHemi = new THREE.HemisphereLight(0xffffff, 0x334455, 1.5);
      weaponScene.add(wHemi);
      const wDir = new THREE.DirectionalLight(0xffffff, 1.2);
      wDir.position.set(1, 2, 2);
      weaponScene.add(wDir);

      const shellMat = new THREE.MeshStandardMaterial({
        color: 0xf2f5f8,
        roughness: 0.2,
        metalness: 0.15
      });
      const darkMetal = new THREE.MeshStandardMaterial({
        color: 0x1b2129,
        roughness: 0.4,
        metalness: 0.75
      });
      gunCoreMat = new THREE.MeshBasicMaterial({ color: COLOR_BLUE });

      // Rear bulbous white shell
      const rearShell = new THREE.Mesh(new THREE.SphereGeometry(0.11, 24, 18), shellMat);
      rearShell.scale.set(0.95, 0.85, 1.75);
      rearShell.position.set(0, 0, 0.12);
      gunGroup.add(rearShell);

      // Inner black barrel rod
      const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.038, 0.045, 0.56, 20), darkMetal);
      barrel.rotation.x = Math.PI / 2;
      barrel.position.set(0, -0.01, -0.08);
      gunGroup.add(barrel);

      // Glowing energy chamber core
      const coreTube = new THREE.Mesh(new THREE.CylinderGeometry(0.028, 0.028, 0.18, 16), gunCoreMat);
      coreTube.rotation.x = Math.PI / 2;
      coreTube.position.set(0, 0.045, -0.05);
      gunGroup.add(coreTube);

      // Front white cowling collar
      const frontCowl = new THREE.Mesh(new THREE.CylinderGeometry(0.068, 0.078, 0.15, 20), shellMat);
      frontCowl.rotation.x = Math.PI / 2;
      frontCowl.position.set(0, 0, -0.24);
      gunGroup.add(frontCowl);

      // 3 iconic articulatedemitter claws at muzzle
      for (let i = 0; i < 3; i++) {
        const angle = (i * Math.PI * 2) / 3 + Math.PI / 6;
        const claw = new THREE.Group();
        claw.rotation.z = angle;

        const arm1 = new THREE.Mesh(new THREE.BoxGeometry(0.012, 0.055, 0.09), darkMetal);
        arm1.position.set(0, 0.075, -0.33);
        arm1.rotation.x = 0.25;

        const tip = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.015, 0.08), darkMetal);
        tip.position.set(0, 0.09, -0.39);
        tip.rotation.x = -0.35;

        claw.add(arm1, tip);
        gunGroup.add(claw);
      }

      // Muzzle point light in main scene
      gunMuzzleLight = new THREE.PointLight(0x00a8ff, 0, 3.5);
      scene.add(gunMuzzleLight);

      gunGroup.position.set(0.24, -0.22, -0.42);
      weaponScene.add(gunGroup);
    }

    // =========================================================================
    // PORTAL OBJECTS, RENDER TARGETS & SHADERS
    // =========================================================================
    function createPortalRenderTarget() {
      const dpr = renderer.getPixelRatio();
      const rt = new THREE.WebGLRenderTarget(
        Math.floor(window.innerWidth * dpr),
        Math.floor(window.innerHeight * dpr),
        {
          minFilter: THREE.LinearFilter,
          magFilter: THREE.LinearFilter,
          format: THREE.RGBAFormat,
          stencilBuffer: false,
          depthBuffer: true
        }
      );
      return rt;
    }

    const rtBlue = createPortalRenderTarget();
    const rtOrange = createPortalRenderTarget();
    const rtRecursion = createPortalRenderTarget();

    // Build custom geometry for the portal screen window:
    // Front ellipse cap + rear recessed pocket so the camera's near plane NEVER clips a hole when crossing!
    function createPortalPocketGeometry(rx, ry, depth = 0.34, segments = 48) {
      const positions = [];
      const uvs = [];
      const indices = [];

      // 1. Front ellipse disk at z = +0.006 (facing +Z)
      const frontCenterIdx = 0;
      positions.push(0, 0, 0.006);
      uvs.push(0.5, 0.5);

      for (let i = 0; i <= segments; i++) {
        const theta = (i / segments) * Math.PI * 2;
        const c = Math.cos(theta);
        const s = Math.sin(theta);
        positions.push(c * rx, s * ry, 0.006);
        uvs.push(c * 0.5 + 0.5, s * 0.5 + 0.5);
      }
      for (let i = 1; i <= segments; i++) {
        indices.push(frontCenterIdx, i, i + 1);
      }

      // 2. Rear pocket walls & back cap (for seamless near-plane crossing)
      const ringStartFront = positions.length / 3;
      for (let i = 0; i <= segments; i++) {
        const theta = (i / segments) * Math.PI * 2;
        const c = Math.cos(theta);
        const s = Math.sin(theta);
        positions.push(c * rx, s * ry, 0.006);
        uvs.push(c * 0.5 + 0.5, s * 0.5 + 0.5);
      }
      const ringStartBack = positions.length / 3;
      for (let i = 0; i <= segments; i++) {
        const theta = (i / segments) * Math.PI * 2;
        const c = Math.cos(theta);
        const s = Math.sin(theta);
        positions.push(c * rx, s * ry, -depth);
        uvs.push(c * 0.5 + 0.5, s * 0.5 + 0.5);
      }
      for (let i = 0; i < segments; i++) {
        const a = ringStartFront + i;
        const b = ringStartFront + i + 1;
        const c = ringStartBack + i;
        const d = ringStartBack + i + 1;
        // Inward facing triangles
        indices.push(a, c, b);
        indices.push(b, c, d);
      }

      // Back cap at z = -depth (facing +Z)
      const backCenterIdx = positions.length / 3;
      positions.push(0, 0, -depth);
      uvs.push(0.5, 0.5);
      for (let i = 0; i < segments; i++) {
        indices.push(backCenterIdx, ringStartBack + i, ringStartBack + i + 1);
      }

      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
      geo.setIndex(indices);
      geo.computeVertexNormals();
      return geo;
    }

    // Build flat rim quad geometry around the portal ellipse
    function createPortalRimMaterial(portalColor) {
      return new THREE.ShaderMaterial({
        transparent: true,
        depthWrite: false,
        side: THREE.DoubleSide,
        clipping: true,
        uniforms: {
          uColor: { value: portalColor.clone() },
          uTime: { value: 0 }
        },
        vertexShader: `
          varying vec2 vUv;
          #include <clipping_planes_pars_vertex>
          void main() {
            vUv = uv * 2.0 - 1.0;
            vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
            gl_Position = projectionMatrix * mvPosition;
            #include <clipping_planes_vertex>
          }
        `,
        fragmentShader: `
          uniform vec3 uColor;
          uniform float uTime;
          varying vec2 vUv;
          #include <clipping_planes_pars_fragment>

          void main() {
            #include <clipping_planes_fragment>
            float r = length(vUv);
            float angle = atan(vUv.y, vUv.x);

            // Animated plasma waves along the elliptical border
            float wave1 = sin(angle * 7.0 - uTime * 4.5 + r * 6.0) * 0.025;
            float wave2 = cos(angle * 11.0 + uTime * 3.2 - r * 8.0) * 0.018;
            float dist = abs(r - 0.80 + wave1 + wave2);

            float core = smoothstep(0.085, 0.008, dist);
            float halo = smoothstep(0.22, 0.0, dist) * 0.68;

            // Mask out inner opening and outer square corners
            float innerCut = smoothstep(0.735, 0.77, r);
            float outerCut = 1.0 - smoothstep(0.92, 1.0, r);

            float alpha = (core + halo) * innerCut * outerCut;
            if (alpha < 0.01) discard;

            vec3 finalColor = mix(uColor, vec3(1.0, 0.98, 0.95), core * 0.72);
            gl_FragColor = vec4(finalColor, alpha);
          }
        `
      });
    }

    // Screen-space portal window shader (samples live virtual camera render target, or shows vortex when unlinked)
    function createPortalScreenMaterial(portalColor, renderTarget) {
      return new THREE.ShaderMaterial({
        side: THREE.DoubleSide,
        clipping: true,
        uniforms: {
          uPortalTex: { value: renderTarget.texture },
          uColor: { value: portalColor.clone() },
          uLinked: { value: 0.0 },
          uTime: { value: 0.0 }
        },
        vertexShader: `
          varying vec4 vClipPos;
          varying vec2 vUv;
          #include <clipping_planes_pars_vertex>
          void main() {
            vUv = uv * 2.0 - 1.0;
            vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
            vClipPos = projectionMatrix * mvPosition;
            gl_Position = vClipPos;
            #include <clipping_planes_vertex>
          }
        `,
        fragmentShader: `
          uniform sampler2D uPortalTex;
          uniform vec3 uColor;
          uniform float uLinked;
          uniform float uTime;
          varying vec4 vClipPos;
          varying vec2 vUv;
          #include <clipping_planes_pars_fragment>

          void main() {
            #include <clipping_planes_fragment>
            float r = length(vUv);

            if (uLinked > 0.5) {
              vec2 screenUV = (vClipPos.xy / vClipPos.w) * 0.5 + 0.5;
              vec4 viewColor = texture2D(uPortalTex, clamp(screenUV, 0.001, 0.999));

              // Subtle glowing inner edge feathering
              float edgeGlow = smoothstep(0.88, 1.02, r);
              vec3 col = mix(viewColor.rgb, uColor * 1.35, edgeGlow * 0.35);
              gl_FragColor = vec4(col, 1.0);
              #include <colorspace_fragment>
            } else {
              // Unlinked swirling vortex effect
              float angle = atan(vUv.y, vUv.x);
              float swirl = sin(r * 14.0 - uTime * 5.0 + angle * 4.0) * 0.5 + 0.5;
              float swirl2 = cos(r * 9.0 + uTime * 3.5 - angle * 3.0) * 0.5 + 0.5;
              vec3 darkCore = uColor * 0.18;
              vec3 brightCore = mix(uColor, vec3(1.0), 0.45);
              vec3 col = mix(darkCore, brightCore, (swirl * 0.6 + swirl2 * 0.4) * smoothstep(0.0, 0.95, r));
              gl_FragColor = vec4(col, 1.0);
            }
          }
        `
      });
    }

    class Portal {
      constructor(isBlue, color, renderTarget) {
        this.isBlue = isBlue;
        this.color = color;
        this.renderTarget = renderTarget;
        this.active = false;
        this.panelId = null;
        this.hostColliderIndex = -1;
        this.openScale = 1.0;

        this.position = new THREE.Vector3();
        this.normal = new THREE.Vector3(0, 0, 1);
        this.up = new THREE.Vector3(0, 1, 0);
        this.right = new THREE.Vector3(1, 0, 0);

        this.group = new THREE.Group();
        this.group.visible = false;

        // 1. Portal screen window mesh (with anti-near-clip rear pocket)
        const pocketGeo = createPortalPocketGeometry(PORTAL_RX, PORTAL_RY, 0.35, 56);
        this.screenMat = createPortalScreenMaterial(color, renderTarget);
        this.screenMesh = new THREE.Mesh(pocketGeo, this.screenMat);
        this.group.add(this.screenMesh);

        // 2. Glowing animated plasma rim mesh
        const rimGeo = new THREE.PlaneGeometry(PORTAL_RX * 2.55, PORTAL_RY * 2.55);
        this.rimMat = createPortalRimMaterial(color);
        this.rimMesh = new THREE.Mesh(rimGeo, this.rimMat);
        this.rimMesh.position.z = 0.012;
        this.group.add(this.rimMesh);

        // 3. Ambient point light emitted by the portal rim
        this.light = new THREE.PointLight(color, 2.2, 5.5);
        this.light.position.set(0, 0, 0.35);
        this.group.add(this.light);

        scene.add(this.group);
      }

      place(pos, normal, panelInfo, animateOpen = true) {
        this.active = true;
        this.panelId = panelInfo.id;
        this.hostColliderIndex = panelInfo.hostColliderIndex;

        this.position.copy(pos);
        this.normal.copy(normal).normalize();
        this.up.set(0, 1, 0);
        this.right.crossVectors(this.up, this.normal).normalize();

        const rotMat = new THREE.Matrix4().makeBasis(this.right, this.up, this.normal);
        this.group.quaternion.setFromRotationMatrix(rotMat);
        this.group.position.copy(this.position);

        this.openScale = animateOpen ? 0.08 : 1.0;
        this.group.scale.setScalar(this.openScale);
        this.group.visible = true;
        this.group.updateMatrixWorld(true);
      }

      clear() {
        this.active = false;
        this.panelId = null;
        this.hostColliderIndex = -1;
        this.group.visible = false;
      }

      update(dt, elapsed, isLinked) {
        if (!this.active) return;
        if (this.openScale < 1.0) {
          this.openScale = Math.min(1.0, this.openScale + dt * 7.5);
          const eased = 1 - Math.pow(1 - this.openScale, 3);
          this.group.scale.setScalar(eased);
          this.group.updateMatrixWorld(true);
        }
        this.rimMat.uniforms.uTime.value = elapsed;
        this.screenMat.uniforms.uTime.value = elapsed;
        this.screenMat.uniforms.uLinked.value = isLinked ? 1.0 : 0.0;
      }
    }

    const bluePortal = new Portal(true, COLOR_BLUE, rtBlue);
    const orangePortal = new Portal(false, COLOR_ORANGE, rtOrange);

    // =========================================================================
    // PORTAL TRANSFORMATION & REAL-TIME VIRTUAL CAMERA RENDERING
    // =========================================================================
    const rotY180 = new THREE.Matrix4().makeRotationY(Math.PI);
    const invSourceMat = new THREE.Matrix4();
    const destMat = new THREE.Matrix4();
    const portalTransformMat = new THREE.Matrix4();

    // Compute 4x4 matrix that maps a pose relative to `srcPortal` (+Z in, -Z into wall)
    // to the corresponding pose emerging out of `dstPortal` (+Z out of wall)
    function computePortalTransform(srcPortal, dstPortal, targetMat) {
      // Build unscaled world matrices for src and dst so opening scale animation never warps perspective
      const srcBasis = new THREE.Matrix4().makeBasis(srcPortal.right, srcPortal.up, srcPortal.normal);
      srcBasis.setPosition(srcPortal.position);
      invSourceMat.copy(srcBasis).invert();

      const dstBasis = new THREE.Matrix4().makeBasis(dstPortal.right, dstPortal.up, dstPortal.normal);
      dstBasis.setPosition(dstPortal.position);
      destMat.copy(dstBasis);

      targetMat.multiplyMatrices(destMat, rotY180).multiply(invSourceMat);
      return targetMat;
    }

    function renderSinglePortalPass(srcPortal, dstPortal, eyeCamera, outRenderTarget, camObj) {
      computePortalTransform(srcPortal, dstPortal, portalTransformMat);

      // Virtual camera world matrix = T_src_to_dst * eyeCamera.matrixWorld
      camObj.matrixWorld.multiplyMatrices(portalTransformMat, eyeCamera.matrixWorld);
      camObj.matrixWorld.decompose(camObj.position, camObj.quaternion, camObj.scale);
      camObj.matrixWorldInverse.copy(camObj.matrixWorld).invert();
      camObj.projectionMatrix.copy(eyeCamera.projectionMatrix);
      camObj.projectionMatrixInverse.copy(eyeCamera.projectionMatrixInverse);

      // Hardware clipping plane at dstPortal's wall surface so walls behind dstPortal never block the view
      const clipPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(
        dstPortal.normal,
        dstPortal.position
      );
      // Slight offset so the host white panel at dstPortal is cleanly clipped away
      clipPlane.constant -= 0.015;

      const prevScreenVis = dstPortal.screenMesh.visible;
      const prevRimVis = dstPortal.rimMesh.visible;
      dstPortal.screenMesh.visible = false;
      dstPortal.rimMesh.visible = false;

      renderer.clippingPlanes = [clipPlane];
      renderer.setRenderTarget(outRenderTarget);
      renderer.clear();
      renderer.render(scene, camObj);

      dstPortal.screenMesh.visible = prevScreenVis;
      dstPortal.rimMesh.visible = prevRimVis;
    }

    function renderPortals(activeEyeCamera) {
      if (!bluePortal.active || !orangePortal.active) {
        renderer.clippingPlanes = [];
        renderer.setRenderTarget(null);
        return;
      }

      // 1. Render Blue Portal's live view (looking out of Orange Portal)
      //    First do 1 recursive bounce in case Orange can see Blue (e.g. facing walls)
      renderSinglePortalPass(bluePortal, orangePortal, activeEyeCamera, rtRecursion, virtualCamera);
      bluePortal.screenMat.uniforms.uPortalTex.value = rtRecursion.texture;
      renderSinglePortalPass(bluePortal, orangePortal, activeEyeCamera, rtBlue, recursiveCamera);
      bluePortal.screenMat.uniforms.uPortalTex.value = rtBlue.texture;

      // 2. Render Orange Portal's live view (looking out of Blue Portal)
      renderSinglePortalPass(orangePortal, bluePortal, activeEyeCamera, rtRecursion, virtualCamera);
      orangePortal.screenMat.uniforms.uPortalTex.value = rtRecursion.texture;
      renderSinglePortalPass(orangePortal, bluePortal, activeEyeCamera, rtOrange, recursiveCamera);
      orangePortal.screenMat.uniforms.uPortalTex.value = rtOrange.texture;

      renderer.clippingPlanes = [];
      renderer.setRenderTarget(null);
    }

    // =========================================================================
    // PROJECTILE TRACERS & DARK-METAL FIZZLE PARTICLES
    // =========================================================================
    const activeEffects = [];

    function spawnBeamAndImpact(startPos, endPos, normal, isBlue, didStick) {
      const color = isBlue ? COLOR_BLUE : COLOR_ORANGE;

      // 1. Energy tracer beam from portal gun to target
      const dist = startPos.distanceTo(endPos);
      const beamGeo = new THREE.CylinderGeometry(0.018, 0.028, dist, 8);
      beamGeo.rotateX(Math.PI / 2);
      const beamMat = new THREE.MeshBasicMaterial({
        color,
        transparent: true,
        opacity: 0.9
      });
      const beamMesh = new THREE.Mesh(beamGeo, beamMat);
      beamMesh.position.lerpVectors(startPos, endPos, 0.5);
      beamMesh.lookAt(endPos);
      scene.add(beamMesh);

      activeEffects.push({
        type: 'beam',
        mesh: beamMesh,
        age: 0,
        maxAge: 0.14
      });

      // 2. If shot hit dark metal / non-portalable surface, spawn Fizzle Particle Burst!
      if (!didStick) {
        const count = 34;
        const group = new THREE.Group();
        group.position.copy(endPos).addScaledVector(normal, 0.03);

        // Fizzle shockwave ring on the dark metal surface
        const ringGeo = new THREE.RingGeometry(0.04, 0.12, 24);
        const ringMat = new THREE.MeshBasicMaterial({
          color,
          side: THREE.DoubleSide,
          transparent: true,
          opacity: 0.95
        });
        const ringMesh = new THREE.Mesh(ringGeo, ringMat);
        const targetPt = endPos.clone().add(normal);
        ringMesh. position.copy(endPos).addScaledVector(normal, 0.02);
        ringMesh.lookAt(targetPt);
        scene.add(ringMesh);
        activeEffects.push({
          type: 'ring',
          mesh: ringMesh,
          age: 0,
          maxAge: 0.32
        });

        // Sparks scattering off the dark metal surface
        const sparkGeo = new THREE.SphereGeometry(0.032, 6, 6);
        const particles = [];
        for (let i = 0; i < count; i++) {
          const pMat = new THREE.MeshBasicMaterial({
            color: i % 3 === 0 ? 0xffffff : color,
            transparent: true,
            opacity: 1.0
          });
          const pMesh = new THREE.Mesh(sparkGeo, pMat);
          // Random tangent + normal velocity
          const randVec = new THREE.Vector3(
            (Math.random() - 0.5) * 2,
            (Math.random() - 0.5) * 2,
            (Math.random() - 0.5) * 2
          ).normalize();
          const vel = normal.clone().multiplyScalar(1.2 + Math.random() * 2.2)
            .addScaledVector(randVec, 2.4);
          group.add(pMesh);
          particles.push({ mesh: pMesh, vel });
        }
        scene.add(group);
        activeEffects.push({
          type: 'fizzle',
          group,
          particles,
          age: 0,
          maxAge: 0.42
        });
      }
    }

    function updateEffects(dt) {
      for (let i = activeEffects.length - 1; i >= 0; i--) {
        const fx = activeEffects[i];
        fx.age += dt;
        const t = fx.age / fx.maxAge;
        if (t >= 1) {
          if (fx.mesh) {
            scene.remove(fx.mesh);
            fx.mesh.geometry.dispose();
            fx.mesh.material.dispose();
          }
          if (fx.group) {
            scene.remove(fx.group);
          }
          activeEffects.splice(i, 1);
          continue;
        }

        if (fx.type === 'beam') {
          fx.mesh.material.opacity = (1 - t) * 0.9;
          fx.mesh.scale.set(1 - t * 0.7, 1 - t * 0.7, 1);
        } else if (fx.type === 'ring') {
          const s = 1 + t * 5.2;
          fx.mesh.scale.set(s, s, 1);
          fx.mesh.material.opacity = (1 - t) * 0.9;
        } else if (fx.type === 'fizzle') {
          fx.particles.forEach((p) => {
            p.vel.y -= 7.5 * dt;
            p.mesh.position.addScaledVector(p.vel, dt);
            p.mesh.material.opacity = 1 - t;
            p.mesh.scale.setScalar(1 - t * 0.6);
          });
        }
      }
    }

    // =========================================================================
    // PORTAL FIRING & PLACEMENT LOGIC
    // =========================================================================
    const raycaster = new THREE.Raycaster();

    function firePortal(isBlue) {
      const targetPortal = isBlue ? bluePortal : orangePortal;
      const otherPortal = isBlue ? orangePortal : bluePortal;
      const color = isBlue ? COLOR_BLUE : COLOR_ORANGE;

      // Update Portal Gun visual state
      gunCoreMat.color.copy(color);
      gunMuzzleLight.color.copy(color);
      gunMuzzleLight.intensity = 4.0;
      gunRecoil = 1.0;

      // Muzzle world origin for beam visual
      const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(mainCamera.quaternion);
      const right = new THREE.Vector3(1, 0, 0).applyQuaternion(mainCamera.quaternion);
      const up = new THREE.Vector3(0, 1, 0).applyQuaternion(mainCamera.quaternion);
      const muzzlePos = mainCamera.position.clone()
        .addScaledVector(forward, 0.45)
        .addScaledVector(right, 0.22)
        .addScaledVector(up, -0.18);

      raycaster.setFromCamera(new THREE.Vector2(0, 0), mainCamera);
      const hits = raycaster.intersectObjects(shootableMeshes, false);

      if (hits.length === 0) return;
      const hit = hits[0];
      const hitMesh = hit.object;
      const hitNormal = hit.face.normal.clone().transformDirection(hitMesh.matrixWorld).normalize();

      if (hitMesh.userData.portalable && hitMesh.userData.panelInfo) {
        const panel = hitMesh.userData.panelInfo;

        // Ensure player shot the front face of the white panel
        if (hitNormal.dot(panel.normal) < 0.7) {
          sounds.playFizzle();
          spawnBeamAndImpact(muzzlePos, hit.point, hitNormal, isBlue, false);
          return;
        }

        // Project hit point onto the white panel's 2D plane and clamp so portal stays inside panel bounds
        const rel = hit.point.clone().sub(panel.center);
        let u = rel.dot(panel.right);
        let v = rel.dot(panel.up);

        const maxU = Math.max(0, panel.halfWidth - PORTAL_RX - 0.08);
        const maxV = Math.max(0, panel.halfHeight - PORTAL_RY - 0.06);
        u = THREE.MathUtils.clamp(u, -maxU, maxU);
        v = THREE.MathUtils.clamp(v, -maxV, maxV);

        // If the other portal is on the same white panel, prevent overlapping it
        if (otherPortal.active && otherPortal.panelId === panel.id) {
          const otherRel = otherPortal.position.clone().sub(panel.center);
          const otherU = otherRel.dot(panel.right);
          const minSep = PORTAL_RX * 2 + 0.14;
          if (Math.abs(u - otherU) < minSep) {
            const shiftedRight = otherU + minSep;
            const shiftedLeft = otherU - minSep;
            if (Math.abs(u - shiftedRight) < Math.abs(u - shiftedLeft) && shiftedRight <= maxU) {
              u = shiftedRight;
            } else if (shiftedLeft >= -maxU) {
              u = shiftedLeft;
            } else if (shiftedRight <= maxU) {
              u = shiftedRight;
            } else {
              // No room on this panel without overlapping the other portal -> fizzle
              sounds.playFizzle();
              spawnBeamAndImpact(muzzlePos, hit.point, panel.normal, isBlue, false);
              return;
            }
          }
        }

        const finalPos = panel.center.clone()
          .addScaledVector(panel.right, u)
          .addScaledVector(panel.up, v);

        targetPortal.place(finalPos, panel.normal, panel, true);
        sounds.playShoot(isBlue);
        sounds.playPortalOpen(isBlue);
        spawnBeamAndImpact(muzzlePos, finalPos, panel.normal, isBlue, true);
        gameStats.shots++;
        updateCrosshairHUD();
      } else {
        // Hit dark metal or glass -> fizzle!
        sounds.playShoot(isBlue);
        sounds.playFizzle();
        spawnBeamAndImpact(muzzlePos, hit.point, hitNormal, isBlue, false);
      }
    }

    function updateCrosshairHUD() {
      const xBlue = document.getElementById('xhair-blue');
      const xOrange = document.getElementById('xhair-orange');
      xBlue.setAttribute('stroke', bluePortal.active ? '#00a8ff' : 'rgba(255,255,255,0.28)');
      xOrange.setAttribute('stroke', orangePortal.active ? '#ff7700' : 'rgba(255,255,255,0.28)');
    }

    // =========================================================================
    // PLAYER STATE, PHYSICS & SEAMLESS PORTAL TRAVERSAL
    // =========================================================================
    const GAME_MODE = {
      DEMO: 'DEMO',
      PLAYING: 'PLAYING',
      COMPLETE: 'COMPLETE'
    };
    let currentMode = GAME_MODE.DEMO;

    const player = {
      eyePos: new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 6.2),
      vel: new THREE.Vector3(),
      yaw: 0,
      pitch: 0,
      onGround: true,
      stepTimer: 0
    };

    const keys = {
      KeyW: false,
      KeyS: false,
      KeyA: false,
      KeyD: false,
      ArrowUp: false,
      ArrowDown: false,
      ArrowLeft: false,
      ArrowRight: false,
      Space: false
    };

    const gameStats = {
      startTime: 0,
      shots: 0,
      traversals: 0
    };

    // Check if a point `eyePos` is aligned within the walkable opening of `portal`
    function getPortalLocalCoords(eyePos, portal) {
      const rel = eyePos.clone().sub(portal.position);
      return {
        x: rel.dot(portal.right),
        y: rel.dot(portal.up),
        z: rel.dot(portal.normal)
      };
    }

    function isInsidePortalAperture(localCoords, marginX = 0.16, marginY = 0.45) {
      const nx = localCoords.x / (PORTAL_RX - marginX);
      const ny = localCoords.y / (PORTAL_RY - marginY);
      return (nx * nx + ny * ny) <= 1.0;
    }

    // Transform player pose and velocity seamlessly across `srcPortal` -> `dstPortal`
    function traversePortal(srcPortal, dstPortal) {
      computePortalTransform(srcPortal, dstPortal, portalTransformMat);

      // 1. Transform exact eye position so camera matches virtual camera with 0-pixel jump
      player.eyePos.applyMatrix4(portalTransformMat);

      // 2. Transform velocity vector by the portal rotation
      const rotMat3 = new THREE.Matrix4().extractRotation(portalTransformMat);
      player.vel.applyMatrix4(rotMat3);

      // Ensure minimum outward velocity so player smoothly exits the destination portal
      const outDot = player.vel.dot(dstPortal.normal);
      if (outDot < 1.8) {
        player.vel.addScaledVector(dstPortal.normal, 1.8 - outDot);
      }

      // 3. Transform horizontal look direction (yaw)
      const forward = new THREE.Vector3(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
      forward.applyMatrix4(rotMat3);
      player.yaw = Math.atan2(-forward.x, -forward.z);

      sounds.playTraverse();
      gameStats.traversals++;
    }

    function getFloorHeightAt(x, y, z) {
      // Check if in high ledge region (z <= -3.5) and high enough to be on or stepping onto the ledge
      if (z <= -3.45 && y >= 4.0 + PLAYER_EYE_HEIGHT - 0.55) {
        return 4.4;
      }
      return 0.0;
    }

    function updatePlayerPhysics(dt) {
      // 1. Horizontal movement input
      const moveForward = (keys.KeyW || keys.ArrowUp ? 1 : 0) - (keys.KeyS || keys.ArrowDown ? 1 : 0);
      const moveRight = (keys.KeyD || keys.ArrowRight ? 1 : 0) - (keys.KeyA || keys.ArrowLeft ? 1 : 0);

      const forwardDir = new THREE.Vector3(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
      const rightDir = new THREE.Vector3(Math.cos(player.yaw), 0, -Math.sin(player.yaw));

      const wishDir = new THREE.Vector3()
        .addScaledVector(forwardDir, moveForward)
        .addScaledVector(rightDir, moveRight);
      if (wishDir.lengthSq() > 0) wishDir.normalize();

      const accel = player.onGround ? 14.0 : 4.5;
      const targetVx = wishDir.x * MOVE_SPEED;
      const targetVz = wishDir.z * MOVE_SPEED;
      player.vel.x = THREE.MathUtils.lerp(player.vel.x, targetVx, Math.min(1, accel * dt));
      player.vel.z = THREE.MathUtils.lerp(player.vel.z, targetVz, Math.min(1, accel * dt));

      // Jump & Gravity
      if (player.onGround && keys.Space) {
        player.vel.y = JUMP_SPEED;
        player.onGround = false;
        sounds.playFootstep();
      }
      player.vel.y -= GRAVITY * dt;

      // Save previous signed distance to each portal before stepping position
      const bothPortalsOpen = bluePortal.active && orangePortal.active;
      const prevBlueLocal = bothPortalsOpen ? getPortalLocalCoords(player.eyePos, bluePortal) : null;
      const prevOrangeLocal = bothPortalsOpen ? getPortalLocalCoords(player.eyePos, orangePortal) : null;

      // Advance position
      player.eyePos.addScaledVector(player.vel, dt);

      // 2. Check seamless portal plane crossing BEFORE wall collision pushback!
      if (bothPortalsOpen) {
        const currBlueLocal = getPortalLocalCoords(player.eyePos, bluePortal);
        if (
          prevBlueLocal.z >= 0 &&
          currBlueLocal.z < 0 &&
          player.vel.dot(bluePortal.normal) < 0 &&
          isInsidePortalAperture(currBlueLocal, 0.12, 0.48)
        ) {
          traversePortal(bluePortal, orangePortal);
        } else {
          const currOrangeLocal = getPortalLocalCoords(player.eyePos, orangePortal);
          if (
            prevOrangeLocal.z >= 0 &&
            currOrangeLocal.z < 0 &&
            player.vel.dot(orangePortal.normal) < 0 &&
            isInsidePortalAperture(currOrangeLocal, 0.12, 0.48)
          ) {
            traversePortal(orangePortal, bluePortal);
          }
        }
      }

      // 3. Floor & Ceiling collision
      const floorY = getFloorHeightAt(player.eyePos.x, player.eyePos.y, player.eyePos.z);
      const minEyeY = floorY + PLAYER_EYE_HEIGHT;
      if (player.eyePos.y <= minEyeY) {
        // Smoothly settle eye height if emerging slightly below/above floor from a portal
        if (minEyeY - player.eyePos.y > 0.02 && minEyeY - player.eyePos.y < 0.35) {
          player.eyePos.y = THREE.MathUtils.lerp(player.eyePos.y, minEyeY, Math.min(1, dt * 18));
        } else {
          player.eyePos.y = minEyeY;
        }
        if (player.vel.y < 0) player.vel.y = 0;
        player.onGround = true;
      } else {
        player.onGround = false;
      }

      // Ceiling clamp
      if (player.eyePos.y > 8.95) {
        player.eyePos.y = 8.95;
        if (player.vel.y > 0) player.vel.y = 0;
      }

      // 4. Horizontal Wall Collisions (with automatic pass-through when aligned with an active portal)
      let bypassColliderIdx = -1;
      if (bothPortalsOpen) {
        for (const p of [bluePortal, orangePortal]) {
          const loc = getPortalLocalCoords(player.eyePos, p);
          if (loc.z > -0.45 && loc.z < PLAYER_RADIUS + 0.25 && isInsidePortalAperture(loc, 0.14, 0.48)) {
            bypassColliderIdx = p.hostColliderIndex;
            // Guide player cleanly inside the portal rim horizontally when inside the threshold
            if (loc.z < PLAYER_RADIUS) {
              const maxLocalX = PORTAL_RX - 0.20;
              if (Math.abs(loc.x) > maxLocalX) {
                const clampedX = Math.sign(loc.x) * maxLocalX;
                player.eyePos.addScaledVector(p.right, clampedX - loc.x);
              }
            }
            break;
          }
        }
      }

      const feetY = player.eyePos.y - PLAYER_EYE_HEIGHT + 0.15;
      const headY = player.eyePos.y + 0.15;

      for (let i = 0; i < boxColliders.length; i++) {
        if (i === bypassColliderIdx) continue;
        const b = boxColliders[i];
        if (headY < b.minY || feetY > b.maxY) continue;

        const closestX = THREE.MathUtils.clamp(player.eyePos.x, b.minX, b.maxX);
        const closestZ = THREE.MathUtils.clamp(player.eyePos.z, b.minZ, b.maxZ);
        const dx = player.eyePos.x - closestX;
        const dz = player.eyePos.z - closestZ;
        const distSq = dx * dx + dz * dz;

        if (distSq < PLAYER_RADIUS * PLAYER_RADIUS) {
          const dist = Math.sqrt(distSq);
          if (dist > 0.0001) {
            const push = (PLAYER_RADIUS - dist) / dist;
            player.eyePos.x += dx * push;
            player.eyePos.z += dz * push;
          }
        }
      }

      // Keep player blocked by closed Exit Door until it opens
      if (doorOpenAmount < 0.75 && player.eyePos.y > 4.0 && Math.abs(player.eyePos.x - 3.2) < 1.3) {
        if (player.eyePos.z < -8.62) {
          player.eyePos.z = -8.62;
        }
      }

      // Footsteps cadence
      const horizSpeed = Math.hypot(player.vel.x, player.vel.z);
      if (player.onGround && horizSpeed > 1.2) {
        player.stepTimer += dt * horizSpeed * 0.42;
        if (player.stepTimer >= 1.0) {
          player.stepTimer = 0;
          sounds.playFootstep();
        }
      }

      // 5. Update Exit Door proximity & Check Chamber Completion
      const distToDoor = Math.hypot(player.eyePos.x - 3.2, player.eyePos.z - (-9.0));
      const targetDoorOpen = (player.eyePos.y > 4.0 && distToDoor < 4.6) ? 1.0 : 0.0;
      doorOpenAmount = THREE.MathUtils.lerp(doorOpenAmount, targetDoorOpen, Math.min(1, dt * 6.0));
      exitDoorLeft.position.x = 3.2 - 0.62 - doorOpenAmount * 1.15;
      exitDoorRight.position.x = 3.2 + 0.62 + doorOpenAmount * 1.15;

      // Trigger "Chamber complete" when player walks through the Exit Door on the high ledge
      if (
        player.eyePos.y > 4.0 &&
        player.eyePos.z < -9.35 &&
        Math.abs(player.eyePos.x - 3.2) < 1.45
      ) {
        completeChamber();
      }

      // Apply player pose to mainCamera
      mainCamera.position.copy(player.eyePos);
      mainCamera.rotation.order = 'YXZ';
      mainCamera.rotation.y = player.yaw;
      mainCamera.rotation.x = player.pitch;
      mainCamera.rotation.z = 0;
      mainCamera.updateMatrixWorld(true);
    }

    // =========================================================================
    // SEAMLESS INFINITE DEMO LOOP (Before First Click)
    // =========================================================================
    // In Demo Mode:
    // - Blue Portal is placed on the Lower Cliff White Panel at (-3.0, 1.38, -3.45), normal (0, 0, +1)
    // - Orange Portal is placed on the Lower West White Panel at (-6.95, 1.38, 2.40), normal (+1, 0, 0)
    // The demo camera walks along a smooth closed path from the Orange Portal exit (-6.95, 2.40) facing +X,
    // curves across the chamber floor toward (-3.0, -3.45) facing -Z, and walks straight through the Blue Portal,
    // where T_Blue_to_Orange maps (-3.0, y, -3.45) facing -Z to (-6.95, y, 2.40) facing +X with C2 continuity!
    let demoProgress = 0.68;

    function setupDemoPortals() {
      const cliffPanel = whitePanelRegistry['lower_cliff'];
      const westPanel = whitePanelRegistry['lower_west'];
      bluePortal.place(
        new THREE.Vector3(-3.0, 1.38, -3.45),
        cliffPanel.normal,
        cliffPanel,
        false
      );
      orangePortal.place(
        new THREE.Vector3(-6.95, 1.38, 2.40),
        westPanel.normal,
        westPanel,
        false
      );
      updateCrosshairHUD();
    }

    function evaluateDemoSpline(u) {
      // u in [0, 1):
      // Segment 1 (u in [0, 0.16]): straight exit out of Orange Portal along +X from x = -6.95 to x = -5.55
      // Segment 2 (u in [0.16, 0.84]): smooth cubic Hermite curve from (-5.55, 2.40) tangent (+1, 0)
      //                                 to (-3.00, -2.05) tangent (0, -1)
      // Segment 3 (u in [0.84, 1.00]): straight approach into Blue Portal along -Z from z = -2.05 to z = -3.45
      const straightLen = 1.40;
      const x0 = -6.95;
      const z0 = 2.40;
      const x1 = -3.00;
      const z1 = -3.45;

      let px, pz, tx, tz;
      if (u < 0.16) {
        const s = u / 0.16;
        px = x0 + s * straightLen;
        pz = z0;
        tx = 1;
        tz = 0;
      } else if (u > 0.84) {
        const s = (u - 0.84) / 0.16;
        px = x1;
        pz = (z1 + straightLen) - s * straightLen;
        tx = 0;
        tz = -1;
      } else {
        const t = (u - 0.16) / 0.68;
        const p0x = x0 + straightLen, p0z = z0;
        const p1x = x1, p1z = z1 + straightLen;
        const mScale = 5.95; // Matches arc speed of straight segments
        const m0x = mScale, m0z = 0;
        const m1x = 0, m1z = -mScale;

        const t2 = t * t;
        const t3 = t2 * t;
        const h00 = 2 * t3 - 3 * t2 + 1;
        const h10 = t3 - 2 * t2 + t;
        const h01 = -2 * t3 + 3 * t2;
        const h11 = t3 - t2;

        px = h00 * p0x + h10 * m0x + h01 * p1x + h11 * m1x;
        pz = h00 * p0z + h10 * m0z + h01 * p1z + h11 * m1z;

        // Derivative for tangent look direction
        const dh00 = 6 * t2 - 6 * t;
        const dh10 = 3 * t2 - 4 * t + 1;
        const dh01 = -6 * t2 + 6 * t;
        const dh11 = 3 * t2 - 2 * t;

        tx = dh00 * p0x + dh10 * m0x + dh01 * p1x + dh11 * m1x;
        tz = dh00 * p0z + dh10 * m0z + dh01 * p1z + dh11 * m1z;
      }

      const yaw = Math.atan2(-tx, -tz);
      // Gentle head bob that has an integer number of cycles (4 cycles) over [0, 1] so bob is also continuous at u = 0/1!
      const bob = Math.sin(u * Math.PI * 8) * 0.028;
      // Subtle upward glance toward the high ledge in the middle of the room, level when entering/exiting portal
      const midRoomFactor = Math.pow(Math.sin(u * Math.PI), 2);
      const pitch = midRoomFactor * 0.14;

      return {
        x: px,
        y: 1.50 + bob,
        z: pz,
        yaw,
        pitch
      };
    }

    function updateDemoLoop(dt) {
      demoProgress = (demoProgress + dt * 0.115) % 1.0;
      const pose = evaluateDemoSpline(demoProgress);

      mainCamera.position.set(pose.x, pose.y, pose.z);
      mainCamera.rotation.order = 'YXZ';
      mainCamera.rotation.y = pose.yaw;
      mainCamera.rotation.x = pose.pitch;
      mainCamera.rotation.z = 0;
      mainCamera.updateMatrixWorld(true);
    }

    // =========================================================================
    // GAME FLOW, POINTER LOCK & INPUT HANDLERS
    // =========================================================================
    const demoOverlay = document.getElementById('demo-overlay');
    const hudEl = document.getElementById('hud');
    const pauseBanner = document.getElementById('pause-banner');
    const completeModal = document.getElementById('complete-modal');
    const restartBtn = document.getElementById('restart-btn');

    function startPlayerChamber() {
      currentMode = GAME_MODE.PLAYING;
      demoOverlay.classList.add('hidden');
      completeModal.classList.remove('visible');
      hudEl.classList.add('visible');

      // Reset player at chamber entrance pad facing the High Ledge & Exit Door
      player.eyePos.set(0, PLAYER_EYE_HEIGHT, 6.2);
      player.vel.set(0, 0, 0);
      player.yaw = 0;
      player.pitch = 0.08;
      player.onGround = true;

      // Clear demo portals so the player solves the chamber
      bluePortal.clear();
      orangePortal.clear();
      updateCrosshairHUD();

      doorOpenAmount = 0;
      exitDoorLeft.position.x = 3.2 - 0.62;
      exitDoorRight.position.x = 3.2 + 0.62;

      gameStats.startTime = performance.now();
      gameStats.shots = 0;
      gameStats.traversals = 0;
    }

    function completeChamber() {
      if (currentMode === GAME_MODE.COMPLETE) return;
      currentMode = GAME_MODE.COMPLETE;
      sounds.playVictory();

      const elapsedSec = ((performance.now() - gameStats.startTime) / 1000).toFixed(1);
      document.getElementById('stat-time').textContent = `${elapsedSec}s`;
      document.getElementById('stat-shots').textContent = `${gameStats.shots}`;
      document.getElementById('stat-steps').textContent = `${gameStats.traversals}`;

      hudEl.classList.remove('visible');
      pauseBanner.classList.remove('visible');
      completeModal.classList.add('visible');

      if (document.pointerLockElement === canvas) {
        document.exitPointerLock();
      }
    }

    canvas.addEventListener('mousedown', (e) => {
      sounds.init();

      if (currentMode === GAME_MODE.DEMO) {
        startPlayerChamber();
        canvas.requestPointerLock();
        return;
      }

      if (currentMode === GAME_MODE.PLAYING) {
        if (document.pointerLockElement !== canvas) {
          canvas.requestPointerLock();
          return;
        }
        if (e.button === 0) {
          firePortal(true); // Left click -> Blue Portal
        } else if (e.button === 2) {
          firePortal(false); // Right click -> Orange Portal
        }
      }
    });

    window.addEventListener('contextmenu', (e) => e.preventDefault());

    document.addEventListener('pointerlockchange', () => {
      if (currentMode === GAME_MODE.PLAYING) {
        if (document.pointerLockElement === canvas) {
          pauseBanner.classList.remove('visible');
        } else {
          pauseBanner.classList.add('visible');
          // Clear held movement keys when unlocked
          Object.keys(keys).forEach((k) => { keys[k] = false; });
        }
      }
    });

    document.addEventListener('mousemove', (e) => {
      if (currentMode !== GAME_MODE.PLAYING || document.pointerLockElement !== canvas) return;
      const sensitivity = 0.0022;
      player.yaw -= e.movementX * sensitivity;
      player.pitch -= e.movementY * sensitivity;
      const maxPitch = Math.PI / 2 - 0.04;
      player.pitch = THREE.MathUtils.clamp(player.pitch, -maxPitch, maxPitch);
    });

    window.addEventListener('keydown', (e) => {
      if (e.code in keys) {
        keys[e.code] = true;
      }
      if (e.code === 'KeyR' && currentMode === GAME_MODE.PLAYING) {
        bluePortal.clear();
        orangePortal.clear();
        sounds.playFizzle();
        updateCrosshairHUD();
      }
    });

    window.addEventListener('keyup', (e) => {
      if (e.code in keys) {
        keys[e.code] = false;
      }
    });

    restartBtn.addEventListener('click', () => {
      sounds.init();
      startPlayerChamber();
      canvas.requestPointerLock();
    });

    window.addEventListener('resize', () => {
      const w = window.innerWidth;
      const h = window.innerHeight;
      const dpr = renderer.getPixelRatio();

      renderer.setSize(w, h);
      mainCamera.aspect = w / h;
      mainCamera.updateProjectionMatrix();

      virtualCamera.aspect = w / h;
      virtualCamera.updateProjectionMatrix();

      recursiveCamera.aspect = w / h;
      recursiveCamera.updateProjectionMatrix();

      weaponCamera.aspect = w / h;
      weaponCamera.updateProjectionMatrix();

      rtBlue.setSize(Math.floor(w * dpr), Math.floor(h * dpr));
      rtOrange.setSize(Math.floor(w * dpr), Math.floor(h * dpr));
      rtRecursion.setSize(Math.floor(w * dpr), Math.floor(h * dpr));
    });

    // =========================================================================
    // INITIALIZE SCENE & MAIN ANIMATION LOOP
    // =========================================================================
    setupLighting();
    buildTestChamber();
    buildPortalGun();
    setupDemoPortals();

    let lastTime = performance.now();

    function animate(now) {
      requestAnimationFrame(animate);

      const dt = Math.min(0.05, (now - lastTime) / 1000);
      lastTime = now;
      const elapsed = now * 0.001;

      // 1. Update camera / player movement BEFORE portal rendering so portal views have zero lag
      if (currentMode === GAME_MODE.DEMO) {
        updateDemoLoop(dt);
      } else if (currentMode === GAME_MODE.PLAYING) {
        updatePlayerPhysics(dt);
      }

      // 2. Update portals & particle effects
      const isLinked = bluePortal.active && orangePortal.active;
      bluePortal.update(dt, elapsed, isLinked);
      orangePortal.update(dt, elapsed, isLinked);
      updateEffects(dt);

      // 3. Update first-person Portal Gun sway & recoil
      if (gunGroup) {
        gunRecoil = Math.max(0, gunRecoil - dt * 6.5);
        gunMuzzleLight.intensity = THREE.MathUtils.lerp(gunMuzzleLight.intensity, 0, dt * 10);
        gunMuzzleLight.position.copy(mainCamera.position);

        const moveSpeed = currentMode === GAME_MODE.PLAYING
          ? Math.hypot(player.vel.x, player.vel.z)
          : 2.5;
        const bobX = Math.cos(elapsed * 7.0) * 0.006 * (moveSpeed / MOVE_SPEED);
        const bobY = Math.sin(elapsed * 14.0) * 0.005 * (moveSpeed / MOVE_SPEED);

        gunGroup.position.set(
          0.24 + bobX,
          -0.22 + bobY + gunRecoil * 0.018,
          -0.42 + gunRecoil * 0.065
        );
        gunGroup.rotation.x = gunRecoil * 0.16;
      }

      // 4. Render live portal windows to offscreen render targets
      renderPortals(mainCamera);

      // 5. Render main chamber view
      renderer.autoClear = true;
      renderer.render(scene, mainCamera);

      // 6. Render first-person Portal Gun overlay on top of depth buffer
      renderer.autoClear = false;
      renderer.clearDepth();
      renderer.render(weaponScene, weaponCamera);
      renderer.autoClear = true;
    }

    requestAnimationFrame(animate);
  </script>
</body>
</html>

Same prompt, other models

2 more
Compare side by side

One email when a new model lands on the board or in the showcase. Unsubscribe anytime.