Showcase

Boat game, built by Gemini 4 Argon

Hostedvia DaBenchOpenRouter
Running live in your browserNew tab
Cost to build
$0.23
Show the code the model wrote
HTML103,228 characters
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TEMPEST VANGUARD — Realistic Naval Wave Physics & Combat</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: #040b14;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    color: #e2f1ff;
    cursor: crosshair;
  }
  #glCanvas {
    position: absolute;
     inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }
  #fxCanvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 2;
  }
  /* HUD OVERLAY */
  #hud {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 3;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 18px 24px;
  }
  .hud-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 16px;
    flex-wrap: wrap;
  }
  .panel {
    background: rgba(6, 18, 32, 0.72);
    border: 1px solid rgba(90, 190, 255, 0.28);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), inset 0 0 18px rgba(40, 150, 255, 0.08);
    backdrop-filter: blur(8px);
    border-radius: 8px;
    padding: 12px 16px;
    pointer-events: auto;
  }
  .status-block {
    min-width: 280px;
  }
  .title-tag {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: #6bc5ff;
    margin-bottom: 6px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .bar-wrap {
    width: 100%;
    height: 12px;
    background: rgba(0, 0, 0, 0.6);
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.12);
    margin-bottom: 8px;
  }
  .bar-fill {
    height: 100%;
    width: 100%;
    background: linear-gradient(90deg, #00e676, #69f0ae);
    transition: width 0.15s ease-out, background 0.3s;
    box-shadow: 0 0 10px rgba(0, 230, 118, 0.6);
  }
  .stats-row {
    display: flex;
    gap: 18px;
    font-size: 13px;
    font-weight: 600;
    color: #b8dcff;
  }
  .stats-row span b {
    color: #fff;
    font-size: 15px;
    font-variant-numeric: tabular-nums;
  }
  .controls-bar {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
  }
  .btn {
    background: rgba(18, 48, 82, 0.78);
    border: 1px solid rgba(100, 195, 255, 0.35);
    color: #d6eeff;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.8px;
    text-transform: uppercase;
    padding: 7px 11px;
    border-radius: 5px;
    cursor: pointer;
    transition: all 0.15s;
  }
  .btn:hover {
    background: rgba(36, 94, 156, 0.9);
    border-color: #7cd4ff;
    color: #fff;
    transform: translateY(-1px);
  }
  .btn.active {
    background: linear-gradient(135deg, #0088ff, #00c6ff);
    color: #02101f;
    border-color: #a6e9ff;
    box-shadow: 0 0 12px rgba(0, 174, 255, 0.5);
  }
  .btn.warn {
    background: rgba(140, 35, 25, 0.8);
    border-color: rgba(255, 110, 90, 0.55);
    color: #ffd4cc;
  }
  .btn.warn:hover {
    background: rgba(200, 45, 30, 0.95);
    box-shadow: 0 0 14px rgba(255, 70, 50, 0.6);
  }

  /* CENTER ALERTS */
  #alertBanner {
    align-self: center;
    background: rgba(160, 20, 20, 0.82);
    border: 1px solid #ff6b6b;
    box-shadow: 0 0 30px rgba(255, 40, 40, 0.6);
    padding: 10px 26px;
    border-radius: 6px;
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 2.5px;
    text-transform: uppercase;
    color: #fff;
    opacity: 0;
    transform: translateY(-10px);
    transition: opacity 0.3s, transform 0.3s;
    pointer-events: none;
    text-align: center;
  }
  #alertBanner.visible {
    opacity: 1;
    transform: translateY(0);
    animation: pulseAlert 1s infinite alternate;
  }
  @keyframes pulseAlert {
    from { box-shadow: 0 0 15px rgba(255, 50, 50, 0.45); }
    to { box-shadow: 0 0 35px rgba(255, 80, 50, 0.95); }
  }

  /* BOTTOM HUD */
  .hud-bottom {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 16px;
  }
  .radar-box {
    display: flex;
    align-items: center;
    gap: 14px;
  }
  #radarCanvas {
    width: 136px;
    height: 136px;
    border-radius: 50%;
    border: 2px solid rgba(100, 205, 255, 0.45);
    background: radial-gradient(circle, rgba(6, 38, 60, 0.9) 0%, rgba(2, 14, 26, 0.95) 100%);
    box-shadow: 0 0 20px rgba(0, 150, 255, 0.25), inset 0 0 15px rgba(0, 200, 255, 0.2);
  }
  .key-hints {
    font-size: 12px;
    line-height: 1.65;
    color: #a6cde8;
  }
  .key {
    display: inline-block;
    background: rgba(255, 255, 255, 0.12);
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 4px;
    padding: 1px 6px;
    font-size: 11px;
    font-weight: 700;
    color: #fff;
    margin-right: 4px;
  }
  .telemetry-panel {
    min-width: 270px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .weapons-row {
    display: flex;
    gap: 10px;
  }
  .weapon-slot {
    flex: 1;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(100, 190, 255, 0.22);
    border-radius: 6px;
    padding: 6px 8px;
  }
  .weapon-name {
    font-size: 10px;
    font-weight: 700;
    color: #7ccbff;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    margin-bottom: 4px;
    display: flex;
    justify-content: space-between;
  }
  .cooldown-bar {
    height: 5px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 3px;
    overflow: hidden;
  }
  .cooldown-fill {
    height: 100%;
    width: 100%;
    background: #00d2ff;
    transition: width 0.05s linear;
  }
  .gauges-row {
    display: flex;
    justify-content: space-between;
    font-size: 12px;
    color: #9ec8e8;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
    padding-top: 6px;
    font-variant-numeric: tabular-nums;
  }
  .gauges-row b {
    color: #fff;
  }

  /* GAME OVER MODAL */
  #modalOverlay {
    position: absolute;
    inset: 0;
    background: rgba(2, 8, 18, 0.82);
    backdrop-filter: blur(10px);
    z-index: 10;
    display: none;
    align-items: center;
    justify-content: center;
  }
  #modalOverlay.show {
    display: flex;
  }
  .modal-card {
    background: rgba(8, 24, 44, 0.95);
    border: 1px solid rgba(100, 200, 255, 0.45);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
    border-radius: 12px;
    padding: 32px 42px;
    text-align: center;
    max-width: 440px;
  }
  .modal-card h1 {
    font-size: 28px;
    letter-spacing: 3px;
    color: #ff5e5e;
    margin-bottom: 10px;
    text-transform: uppercase;
  }
  .modal-card p {
    font-size: 15px;
    color: #b8dcff;
    margin-bottom: 24px;
    line-height: 1.5;
  }
  .modal-card .btn {
    font-size: 14px;
    padding: 12px 28px;
  }
</style>
</head>
<body>

<canvas id="glCanvas"></canvas>
<canvas id="fxCanvas"></canvas>

<div id="hud">
  <div class="hud-top">
    <div class="panel status-block">
      <div class="title-tag">
        <span>HULL INTEGRITY</span>
        <span id="hullPctText">100%</span>
      </div>
      <div class="bar-wrap">
        <div class="bar-fill" id="hullBar"></div>
      </div>
      <div class="stats-row">
        <span>SCORE: <b id="scoreVal">0</b></span>
        <span>FLEET WAVE: <b id="waveVal">1</b></span>
        <span>HOSTILES: <b id="enemiesVal">0</b></span>
      </div>
    </div>

    <div class="panel controls-bar">
      <span style="font-size:11px;font-weight:700;color:#7ccbff;letter-spacing:1px;margin-right:4px;">SEA STATE:</span>
      <button class="btn active" data-preset="sunset">Golden Swell</button>
      <button class="btn" data-preset="noon">Tropical Noon</button>
      <button class="btn" data-preset="storm">North Sea Storm</button>
      <button class="btn" data-preset="night">Moonlit Raid</button>
      <button class="btn warn" id="rogueBtn" title="Trigger an immediate towering Rogue Wave">⚡ Summon Rogue Wave</button>
      <button class="btn" id="audioBtn">🔊 Audio: ON</button>
    </div>
  </div>

  <div id="alertBanner">⚠️ MASSIVE ROGUE WAVE APPROACHING — TURN BOW INTO WAVE! ⚠️</div>

  <div class="hud-bottom">
    <div class="panel radar-box">
      <canvas id="radarCanvas" width="160" height="160"></canvas>
      <div class="key-hints">
        <div><span class="key">W</span><span class="key">S</span> Throttle Forward / Reverse</div>
        <div><span class="key">A</span><span class="key">D</span> Rudder Port / Starboard</div>
        <div><span class="key">SHIFT</span> Turbo Hydro-Boost</div>
        <div><span class="key">MOUSE</span> Aim Deck Turret (360°)</div>
        <div><span class="key">L-CLICK</span> Twin 120mm Cannons</div>
        <div><span class="key">R-CLICK</span> Broadside Depth Salvo</div>
        <div><span class="key">SCROLL</span> Zoom Camera</div>
      </div>
    </div>

    <div class="panel telemetry-panel">
      <div class="weapons-row">
        <div class="weapon-slot">
          <div class="weapon-name"><span>120mm Turret</span><span id="w1Status">READY</span></div>
          <div class="cooldown-bar"><div class="cooldown-fill" id="w1Bar"></div></div>
        </div>
        <div class="weapon-slot">
          <div class="weapon-name"><span>Salvo Mortar</span><span id="w2Status">READY</span></div>
          <div class="cooldown-bar"><div class="cooldown-fill" id="w2Bar"></div></div>
        </div>
        <div class="weapon-slot">
          <div class="weapon-name"><span>Turbo Boost</span><span id="boostStatus">100%</span></div>
          <div class="cooldown-bar"><div class="cooldown-fill" id="boostBar"></div></div>
        </div>
      </div>
      <div class="gauges-row">
        <span>SPEED: <b id="speedVal">0.0</b> kts</span>
        <span>PITCH: <b id="pitchVal">0°</b></span>
        <span>ROLL: <b id="rollVal">0°</b></span>
        <span>WAVE HT: <b id="waveHtVal">0.0m</b></span>
      </div>
    </div>
  </div>
</div>

<div id="modalOverlay">
  <div class="modal-card">
    <h1>VESSEL SUNK</h1>
    <p id="finalStatsText">Your warship was claimed by the sea after destroying 0 hostile vessels.</p>
    <button class="btn active" id="restartBtn">DEPLOY NEW DESTROYER</button>
  </div>
</div>

<script>
// ============================================================================
// SELF-CONTAINED THREE.JS-INSPIRED WEBGL2 PBR & GERSTNER OCEAN ENGINE
// ============================================================================
// Provides a clean Three.js-style scene graph & math architecture with bespoke
// custom GLSL shaders for Gerstner + Rogue wave hydrodynamics, subsurface
// scattering, Kelvin wakes, multi-point buoyancy physics, and naval combat.

const THREE = (() => {
  class Vector3 {
    constructor(x = 0, y = 0, z = 0) { this.x = x; this.y = y; this.z = z; }
    set(x, y, z) { this.x = x; this.y = y; this.z = z; return this; }
    copy(v) { this.x = v.x; this.y = v.y; this.z = v.z; return this; }
    clone() { return new Vector3(this.x, this.y, this.z); }
    add(v) { this.x += v.x; this.y += v.y; this.z += v.z; return this; }
    sub(v) { this.x -= v.x; this.y -= v.y; this.z -= v.z; return this; }
     addScaledVector(v, s) { this.x += v.x * s; this.y += v.y * s; this.z += v.z * s; return this; }
    multiplyScalar(s) { this.x *= s; this.y *= s; this.z *= s; return this; }
    dot(v) { return this.x * v.x + this.y * v.y + this.z * v.z; }
    cross(v) {
      const ax = this.x, ay = this.y, az = this.z;
      this.x = ay * v.z - az * v.y;
      this.y = az * v.x - ax * v.z;
      this.z = ax * v.y - ay * v.x;
      return this;
    }
    lengthSq() { return this.x * this.x + this.y * this.y + this.z * this.z; }
    length() { return Math.hypot(this.x, this.y, this.z); }
    normalize() {
      const l = this.length() || 1;
      return this.multiplyScalar(1 / l);
    }
    distanceTo(v) { return Math.hypot(this.x - v.x, this.y - v.y, this.z - v.z); }
    lerp(v, a) {
      this.x += (v.x - this.x) * a;
      this.y += (v.y - this.y) * a;
      this.z += (v.z - this.z) * a;
      return this;
    }
  }

  class Matrix4 {
    constructor() {
      this.elements = new Float32Array([
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
      ]);
    }
    identity() {
      const e = this.elements;
      e.fill(0); e[0] = e[5] = e[10] = e[15] = 1;
      return this;
    }
    copy(m) { this.elements.set(m.elements); return this; }
    multiplyMatrices(a, b) {
      const ae = a.elements, be = b.elements, te = this.elements;
      const a11 = ae[0], a12 = ae[4], a13 = ae[8], a14 = ae[12];
      const a21 = ae[1], a22 = ae[5], a23 = ae[9], a24 = ae[13];
      const a31 = ae[2], a32 = ae[6], a33 = ae[10], a34 = ae[14];
      const a41 = ae[3], a42 = ae[7], a43 = ae[11], a44 = ae[15];

      const b11 = be[0], b12 = be[4], b13 = be[8], b14 = be[12];
      const b21 = be[1], b22 = be[5], b23 = be[9], b24 = be[13];
      const b31 = be[2], b32 = be[6], b33 = be[10], b34 = be[14];
      const b41 = be[3], b42 = be[7], b43 = be[11], b44 = be[15];

      te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
      te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
      te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
      te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;

      te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
      te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
      te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
      te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;

      te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
      te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
      te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
      te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;

      te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
      te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
      te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
      te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;
      return this;
    }
    makePerspective(fovDeg, aspect, near, far) {
      const f = 1.0 / Math.tan((fovDeg * Math.PI / 180) * 0.5);
      const nf = 1 / (near - far);
      const e = this.elements;
      e.fill(0);
      e[0] = f / aspect;
      e[5] = f;
      e[10] = (far + near) * nf;
      e[11] = -1;
      e[14] = (2 * far * near) * nf;
      return this;
    }
    makeLookAt(eye, target, up) {
      let zx = eye.x - target.x, zy = eye.y - target.y, zz = eye.z - target.z;
      let len = Math.hypot(zx, zy, zz) || 1;
      zx /= len; zy /= len; zz /= len;

      let xx = up.y * zz - up.z * zy;
      let xy = up.z * zx - up.x * zz;
      let xz = up.x * zy - up.y * zx;
      len = Math.hypot(xx, xy, xz) || 1;
      xx /= len; xy /= len; xz /= len;

      const yx = zy * xz - zz * xy;
      const yy = zz * xx - zx * xz;
      const yz = zx * xy - zy * xx;

      const e = this.elements;
      e[0] = xx; e[4] = xy; e[8] = xz; e[12] = -(xx * eye.x + xy * eye.y + xz * eye.z);
      e[1] = yx; e[5] = yy; e[9] = yz; e[13] = -(yx * eye.x + yy * eye.y + yz * eye.z);
      e[2] = zx; e[6] = zy; e[10] = zz; e[14] = -(zx * eye.x + zy * eye.y + zz * eye.z);
      e[3] = 0;  e[7] = 0;  e[11] = 0;  e[15] = 1;
      return this;
    }
    composeYPR(pos, yaw, pitch, roll, scale) {
      // Intrinsic YXZ (Yaw around Y, Pitch around X, Roll around Z)
      const cy = Math.cos(yaw), sy = Math.sin(yaw);
      const cp = Math.cos(pitch), sp = Math.sin(pitch);
      const cr = Math.cos(roll), sr = Math.sin(roll);
      const sx = scale.x, syS = scale.y, sz = scale.z;
      const e = this.elements;

      e[0] = (cy * cr + sy * sp * sr) * sx;
      e[1] = (cp * sr) * sx;
      e[2] = (-sy * cr + cy * sp * sr) * sx;
      e[3] = 0;

      e[4] = (cy * (-sr) + sy * sp * cr) * syS;
      e[5] = (cp * cr) * syS;
      e[6] = (-sy * (-sr) + cy * sp * cr) * syS;
      e[7] = 0;

      e[8] = (sy * cp) * sz;
      e[9] = (-sp) * sz;
      e[10] = (cy * cp) * sz;
      e[11] = 0;

      e[12] = pos.x;
      e[13] = pos.y;
      e[14] = pos.z;
      e[15] = 1;
      return this;
    }
    transformPoint(v) {
      const e = this.elements;
      return new Vector3(
        e[0] * v.x + e[4] * v.y + e[8] * v.z + e[12],
        e[1] * v.x + e[5] * v.y + e[9] * v.z + e[13],
        e[2] * v.x + e[6] * v.y + e[10] * v.z + e[14]
      );
    }
    transformDirection(v) {
      const e = this.elements;
      return new Vector3(
        e[0] * v.x + e[4] * v.y + e[8] * v.z,
        e[1] * v.x + e[5] * v.y + e[9] * v.z,
        e[2] * v.x + e[6] * v.y + e[10] * v.z
      ).normalize();
    }
  }

  return { Vector3, Matrix4 };
})();

// ============================================================================
// SYNTHESIZED WEB AUDIO SOUND ENGINE (Zero External Assets)
// ============================================================================
class NavalAudio {
  constructor() {
    this.ctx = null;
    this.enabled = true;
    this.initialized = false;
    this.engineGain = null;
    this.engineOsc = null;
    this.oceanGain = null;
  }

  init() {
    if (this.initialized) return;
    try {
      const AudioCtx = window.AudioContext || window.webkitAudioContext;
      this.ctx = new AudioCtx();
      this.initialized = true;

      // Continuous Ocean Surf & Wind Noise
      const bufferSize = this.ctx.sampleRate * 3;
      const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
      const output = noiseBuffer.getChannelData(0);
      let lastOut = 0.0;
      for (let i = 0; i < bufferSize; i++) {
        const white = Math.random() * 2 - 1;
        output[i] = (lastOut + (0.03 * white)) / 1.03;
        lastOut = output[i];
        output[i] *= 2.5;
      }
      const whiteNoise = this.ctx.createBufferSource();
      whiteNoise.buffer = noiseBuffer;
      whiteNoise.loop = true;

      this.oceanFilter = this.ctx.createBiquadFilter();
      this.oceanFilter.type = 'lowpass';
      this.oceanFilter.frequency.value = 340;

      this.oceanGain = this.ctx.createGain();
      this.oceanGain.gain.value = 0.16;

      whiteNoise.connect(this.oceanFilter);
      this.oceanFilter.connect(this.oceanGain);
      this.oceanGain.connect(this.ctx.destination);
      whiteNoise.start(0);

      // Boat Diesel-Turbine Engine Hum
      this.engineOsc = this.ctx.createOscillator();
      this.engineOsc.type = 'sawtooth';
      this.engineOsc.frequency.value = 42;

      this.engineFilter = this.ctx.createBiquadFilter();
      this.engineFilter.type = 'lowpass';
      this.engineFilter.frequency.value = 110;

      this.engineGain = this.ctx.createGain();
      this.engineGain.gain.value = 0.04;

      this.engineOsc.connect(this.engineFilter);
      this.engineFilter.connect(this.engineGain);
      this.engineGain.connect(this.ctx.destination);
      this.engineOsc.start(0);
    } catch (e) {
      this.enabled = false;
    }
  }

  updateContinuous(speedRatio, waveRoughness, submergedAmount) {
    if (!this.initialized || !this.enabled) return;
    if (this.ctx.state === 'suspended') this.ctx.resume();
    const now = this.ctx.currentTime;
    this.engineOsc.frequency.setTargetAtTime(38 + speedRatio * 55, now, 0.1);
    this.engineFilter.frequency.setTargetAtTime(95 + speedRatio * 180, now, 0.1);
    this.engineGain.gain.setTargetAtTime(0.035 + speedRatio * 0.065, now, 0.1);

    this.oceanFilter.frequency.setTargetAtTime(280 + waveRoughness * 320 + submergedAmount * 400, now, 0.15);
    this.oceanGain.gain.setTargetAtTime(Math.min(0.38, 0.12 + waveRoughness * 0.12 + submergedAmount * 0.15), now, 0.15);
  }

  playCannon(isHeavy = true) {
    if (!this.initialized || !this.enabled) return;
    const now = this.ctx.currentTime;
    // Punchy low frequency boom + noise burst
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(isHeavy ? 125 : 180, now);
    osc.frequency.exponentialRampToValueAtTime(24, now + (isHeavy ? 0.42 : 0.25));

    gain.gain.setValueAtTime(isHeavy ? 0.42 : 0.25, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now + (isHeavy ? 0.45 : 0.28));
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(now);
    osc.stop(now + 0.46);

    // Crack noise
    this.playNoiseBurst(isHeavy ? 0.35 : 0.2, 900, 0.22);
  }

  playSplash(intensity = 1.0) {
    if (!this.initialized || !this.enabled) return;
    this.playNoiseBurst(0.35 + intensity * 0.3, 650 + intensity * 350, Math.min(0.35, 0.12 * intensity));
  }

  playExplosion() {
    if (!this.initialized || !this.enabled) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(90, now);
    osc.frequency.exponentialRampToValueAtTime(16, now + 0.85);
    gain.gain.setValueAtTime(0.5, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.9);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(now);
    osc.stop(now + 0.92);

    this.playNoiseBurst(0.8, 500, 0.38);
  }

  playRogueHorn() {
    if (!this.initialized || !this.enabled) return;
    const now = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(110, now);
    osc.frequency.linearRampToValueAtTime(82, now + 1.4);
    gain.gain.setValueAtTime(0.01, now);
    gain.gain.linearRampToValueAtTime(0.22, now + 0.2);
    gain.gain.exponentialRampToValueAtTime(0.001, now + 1.5);

    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.value = 320;
    osc.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start(now);
    osc.stop(now + 1.55);
  }

  playNoiseBurst(duration, cutoff, volume) {
    const bufferSize = Math.floor(this.ctx.sampleRate * duration);
    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.value = cutoff;
    filter.Q.value = 0.7;

    const gain = this.ctx.createGain();
    gain.gain.value = volume;

    noise.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    noise.start();
  }

  toggle() {
    this.init();
    this.enabled = !this.enabled;
    if (this.oceanGain && this.engineGain) {
      if (!this.enabled) {
        this.oceanGain.gain.setValueAtTime(0, this.ctx.currentTime);
        this.engineGain.gain.setValueAtTime(0, this.ctx.currentTime);
      }
    }
    return this.enabled;
  }
}

const audio = new NavalAudio();

// ============================================================================
// WEBGL2 CONTEXT & SHADER SOURCES
// ============================================================================
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl2', { antialias: true, alpha: false, powerPreference: 'high-performance' });
if (!gl) {
  alert('WebGL 2 is required to run this simulation.');
}

const fxCanvas = document.getElementById('fxCanvas');
const fxCtx = fxCanvas.getContext('2d');
const radarCanvas = document.getElementById('radarCanvas');
const radarCtx = radarCanvas.getContext('2d');

function compileShader(type, source) {
  const s = gl.createShader(type);
  gl.shaderSource(s, source);
  gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
    console.error('Shader compile error:', gl.getShaderInfoLog(s), source);
  }
  return s;
}

function createProgram(vsSource, fsSource) {
  const p = gl.createProgram();
  gl.attachShader(p, compileShader(gl.VERTEX_SHADER, vsSource));
  gl.attachShader(p, compileShader(gl.FRAGMENT_SHADER, fsSource));
  gl.linkProgram(p);
  if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
    console.error('Program link error:', gl.getProgramInfoLog(p));
  }
  return p;
}

// ----------------------------------------------------------------------------
// 1. SKY DOME SHADER (Rayleigh + Mie Scattering, Sun Disk, Horizon Mist)
// ----------------------------------------------------------------------------
const SKY_VS = `#version 300 es
precision highp float;
layout(location = 0) in vec2 aPos;
out vec3 vRayDir;
uniform mat4 uInvProj;
uniform mat4 uInvView;

void main() {
  vec4 clip = vec4(aPos, 0.9999, 1.0);
  vec4 eye = uInvProj * clip;
  eye = vec4(eye.xy, -1.0, 0.0);
  vRayDir = (uInvView * eye).xyz;
  gl_Position = clip;
}
`;

const SKY_FS = `#version 300 es
precision highp float;
in vec3 vRayDir;
out vec4 fragColor;

uniform vec3 uSunDir;
uniform vec3 uSunColor;
uniform vec3 uSkyZenith;
uniform vec3 uSkyHorizon;
uniform float uTime;
uniform float uStormFactor;

float hash(vec2 p) {
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  f = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1,0)), f.x),
             mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), f.x), f.y);
}
float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  mat2 rot = mat2(0.8, 0.6, -0.6, 0.8);
  for (int i = 0; i < 4; i++) {
    v += a * noise(p);
    p = rot * p * 2.03;
    a *= 0.5;
  }
  return v;
}

void main() {
  vec3 dir = normalize(vRayDir);
  float y = max(dir.y, 0.001);

  // Atmospheric gradient
  float horizonBlend = pow(1.0 - y, 2.8);
  vec3 col = mix(uSkyZenith, uSkyHorizon, horizonBlend);

  // Mie scattering glow around the sun
  float sunAmt = max(dot(dir, uSunDir), 0.0);
  col += uSunColor * pow(sunAmt, 8.0) * 0.45 * (1.0 - uStormFactor * 0.5);
  col += uSunColor * pow(sunAmt, 64.0) * 0.65;
  // Crisp solar disk
  col += uSunColor * smoothstep(0.9992, 0.9998, sunAmt) * 3.5 * (1.0 - uStormFactor * 0.7);

  // Procedural volumetric-style clouds in upper hemisphere
  if (dir.y > 0.01) {
    vec2 cloudUV = dir.xz / (dir.y + 0.18) * 1.4 + vec2(uTime * 0.012, uTime * 0.006);
    float c = fbm(cloudUV);
    float cloudMask = smoothstep(0.45 - uStormFactor * 0.18, 0.82, c) * smoothstep(0.01, 0.16, dir.y);
    vec3 cloudLit = mix(uSkyHorizon * 0.85, uSunColor * 1.15, pow(sunAmt, 3.0));
    vec3 cloudDark = uSkyZenith * 0.45;
    vec3 cloudCol = mix(cloudLit, cloudDark, c * (0.45 + uStormFactor * 0.4));
    col = mix(col, cloudCol, cloudMask * (0.55 + uStormFactor * 0.35));
  }

  // Horizon atmospheric haze
  float haze = exp(-abs(dir.y) * 9.5);
  col = mix(col, uSkyHorizon, haze * 0.65);

  // Filmic tonemapping
  col = col / (1.0 + col * 0.35);
  col = pow(col, vec3(1.0 / 2.2));
  fragColor = vec4(col, 1.0);
}
`;

// ----------------------------------------------------------------------------
// 2. ULTRA-REALISTIC GERSTNER + ROGUE WAVE WATER SHADER
// ----------------------------------------------------------------------------
const WATER_VS = `#version 300 es
precision highp float;
layout(location = 0) in vec2 aGridPos;

uniform mat4 uProj;
uniform mat4 uView;
uniform vec3 uCameraPos;
uniform float uTime;
uniform float uWaveScale;

// 8 Gerstner waves: vec4(dirX, dirZ, steepnessQ, amplitude), vec4(wavelengthK, speedOmega, 0, 0)
uniform vec4 uWavesA[8];
uniform vec4 uWavesB[8];

// Occasional Rogue Wave: vec4(dirX, dirZ, currentCenterDist, amplitude), vec4(width, activeFlag, steepness, 0)
uniform vec4 uRogueA;
uniform vec4 uRogueB;

out vec3 vWorldPos;
out vec3 vMacroNormal;
out float vJacobian;
out float vRogueCrest;

void main() {
  // Radial + adaptive grid centered on camera XZ
  vec2 worldBase = aGridPos + uCameraPos.xz;
  vec3 pos = vec3(worldBase.x, 0.0, worldBase.y);

  vec3 tangent = vec3(1.0, 0.0, 0.0);
  vec3 binormal = vec3(0.0, 0.0, 1.0);
  float jacobian = 1.0;

  for (int i = 0; i < 8; i++) {
    vec2 d = uWavesA[i].xy;
    float q = uWavesA[i].z;
    float a = uWavesA[i].w * uWaveScale;
    float k = uWavesB[i].x;
    float w = uWavesB[i].y;

    float phase = k * dot(d, worldBase) - w * uTime;
    float s = sin(phase);
    float c = cos(phase);

    pos.x -= q * a * d.x * c;
    pos.z -= q * a * d.y * c;
    pos.y += a * s;

    float ka = k * a;
    tangent.x -= q * d.x * d.x * ka * s;
    tangent.y += d.x * ka * c;
    tangent.z -= q * d.x * d.y * ka * s;

    binormal.x -= q * d.x * d.y * ka * s;
    binormal.y += d.y * ka * c;
    binormal.z -= q * d.y * d.y * ka * s;

    jacobian -= q * ka * s;
  }

  // Evaluate towering Rogue Wave if active
  float rogueCrest = 0.0;
  if (uRogueB.y > 0.001) {
    vec2 rDir = uRogueA.xy;
    float rCenter = uRogueA.z;
    float rAmp = uRogueA.w * uRogueB.y;
    float rWidth = uRogueB.x;

    float proj = dot(worldBase, rDir);
    float dist = proj - rCenter;
    float normDist = dist / rWidth;
    float env = exp(-normDist * normDist);

    // Asymmetric curling wave profile
    float curl = -sin(normDist * 1.9) * env * rWidth * 0.28 * uRogueB.y;
    pos.x += rDir.x * curl;
    pos.z += rDir.y * curl;
    pos.y += rAmp * env;

    // Derivative of Gaussian envelope
    float dEnv = (-2.0 * normDist / rWidth) * rAmp * env;
    tangent.y += rDir.x * dEnv;
    binormal.y += rDir.y * dEnv;

    rogueCrest = env * smoothstep(-0.4, 0.35, normDist);
    jacobian -= env * 0.65 * uRogueB.y;
  }

  vWorldPos = pos;
  vMacroNormal = normalize(cross(binormal, tangent));
  vJacobian = jacobian;
  vRogueCrest = rogueCrest;

  gl_Position = uProj * uView * vec4(pos, 1.0);
}
`;

const WATER_FS = `#version 300 es
precision highp float;

in vec3 vWorldPos;
in vec3 vMacroNormal;
in float vJacobian;
in float vRogueCrest;

out vec4 fragColor;

uniform vec3 uCameraPos;
uniform float uTime;
uniform vec3 uSunDir;
uniform vec3 uSunColor;
uniform vec3 uSkyZenith;
uniform vec3 uSkyHorizon;
uniform vec3 uWaterDeep;
uniform vec3 uWaterShallow;
uniform float uStormFactor;

// Wake trails: vec4(x, z, radius, age0to1)
uniform vec4 uWake[24];
// Splash & explosion rings: vec4(x, z, radius, age0to1)
uniform vec4 uSplashes[12];
// Active Boats for hull contact foam: vec4(x, z, yaw, halfLength)
uniform vec4 uBoats[10];
uniform int uNumBoats;
// Ballistic Aiming Reticle on water: vec4(x, z, radius, active)
uniform vec4 uAimReticle;

float hash(vec2 p) {
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}

float noise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  f = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
             mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y);
}

// Multi-octave capillary wave heightfield for micro-normals & foam texture
float microWaves(vec2 p, float t) {
  float h = 0.0;
  float amp = 0.5;
  mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
  vec2 shift = vec2(t * 0.75, -t * 0.55);
  for (int i = 0; i < 5; i++) {
    h += amp * noise(p + shift);
    p = rot * p * 2.18;
    shift = rot * shift * 1.3;
    amp *= 0.48;
  }
  return h;
}

vec3 sampleSkyReflection(vec3 reflDir) {
  reflDir.y = max(reflDir.y, 0.005);
  float horizonBlend = pow(1.0 - reflDir.y, 2.6);
  vec3 sky = mix(uSkyZenith, uSkyHorizon, horizonBlend);
  float sunAmt = max(dot(reflDir, uSunDir), 0.0);
  sky += uSunColor * pow(sunAmt, 10.0) * 0.45;
  return sky;
}

void main() {
  vec3 viewVec = uCameraPos - vWorldPos;
  float dist = length(viewVec);
  vec3 V = viewVec / max(dist, 0.001);

  // Distance LOD attenuation for micro-ripples to prevent aliasing at horizon
  float detailFade = exp(-dist * 0.0038);

  // Compute micro-normal via central differences on animated FBM
  vec2 uv = vWorldPos.xz * 0.42;
  float eps = 0.08;
  float h0 = microWaves(uv, uTime);
  float hx = microWaves(uv + vec2(eps, 0.0), uTime);
  float hz = microWaves(uv + vec2(0.0, eps), uTime);

  vec3 microN = normalize(vec3((h0 - hx) / eps, 1.35, (h0 - hz) / eps));
  vec3 N = normalize(mix(vMacroNormal, normalize(vMacroNormal + microN * vec3(0.42, 0.0, 0.42)), detailFade));

  // --- WAKE, SPLASHES & HULL CONTACT FOAM ---
  float foamMask = 0.0;
  float foamNoise = noise(vWorldPos.xz * 1.6 + vec2(uTime * 0.4, -uTime * 0.3));
  float fineFoamNoise = noise(vWorldPos.xz * 4.5 - vec2(uTime * 0.8));
  float cellularFoam = smoothstep(0.22, 0.75, foamNoise * 0.65 + fineFoamNoise * 0.35);

  // 1. Hull contact foam & bow wave around every boat
  for (int i = 0; i < 10; i++) {
    if (i >= uNumBoats) break;
    vec4 b = uBoats[i];
    vec2 dp = vWorldPos.xz - b.xy;
    // Quick bounding check
    if (dot(dp, dp) < 260.0) {
      float cy = cos(b.z), sy = sin(b.z);
      // Transform into boat local XZ (Z forward, X right)
      float lx = dp.x * cy - dp.y * sy;
      float lz = dp.x * sy + dp.y * cy;
      float halfL = b.w;
      float halfW = halfL * 0.34;
      // Tapered hull SDF
      float bowTaper = clamp(1.0 - max(lz, 0.0) / (halfL * 1.15), 0.25, 1.0);
      vec2 q = vec2(lx / (halfW * bowTaper), lz / halfL);
      float hullDist = length(q);
      if (hullDist > 0.85 && hullDist < 1.85) {
        float rim = smoothstep(1.85, 1.02, hullDist) * smoothstep(0.75, 0.98, hullDist);
        foamMask += rim * (0.85 + 0.35 * cellularFoam);
        // Perturb water normal away from boat hull
        vec2 pushDir = normalize(dp + vec2(0.001));
        N = normalize(N + vec3(pushDir.x, 0.0, pushDir.y) * rim * 0.35);
      }
    }
  }

  // 2. Kelvin Wake trail points behind moving boats
  for (int i = 0; i < 24; i++) {
    vec4 w = uWake[i];
    if (w.w <= 0.0 || w.w >= 1.0) continue;
    vec2 dp = vWorldPos.xz - w.xy;
    float r2 = dot(dp, dp);
    float maxR = w.z * (1.0 + w.w * 2.2);
    if (r2 < maxR * maxR) {
      float d = sqrt(r2) / maxR;
      // Double V-wake edge + center propeller wash
      float edgeRing = exp(-pow((d - 0.72) * 4.5, 2.0));
      float centerWash = (1.0 - d) * 0.75;
      float fade = (1.0 - w.w) * (1.0 - w.w);
      foamMask += (edgeRing * 0.9 + centerWash * cellularFoam) * fade * 0.75;
    }
  }

  // 3. Expanding Splash & Cannon Impact Rings
  for (int i = 0; i < 12; i++) {
    vec4 sp = uSplashes[i];
    if (sp.w <= 0.0 || sp.w >= 1.0) continue;
    vec2 dp = vWorldPos.xz - sp.xy;
    float d = length(dp);
    float ringRadius = sp.z * (0.25 + sp.w * 1.6);
    float ringDiff = d - ringRadius;
    if (abs(ringDiff) < 4.5) {
      float waveRing = sin(ringDiff * 3.14159) * exp(-abs(ringDiff) * 0.8) * (1.0 - sp.w);
      vec2 dir = dp / max(d, 0.001);
      N = normalize(N + vec3(dir.x * waveRing * 0.6, 0.0, dir.y * waveRing * 0.6));
      foamMask += exp(-abs(ringDiff) * 0.95) * (1.0 - sp.w) * 1.1;
    }
  }

  // 4. Wave Crest Whitecaps (Jacobian compression + Rogue Wave Crest)
  float crestFactor = smoothstep(0.68, 0.15, vJacobian) + vRogueCrest * 1.35;
  foamMask += crestFactor * (0.45 + 0.75 * cellularFoam);
  foamMask = clamp(foamMask * detailFade, 0.0, 1.0);

  // --- WATER OPTICS: FRESNEL, SUBSURFACE SCATTERING, GGX SPECULAR ---
  float NdotV = max(dot(N, V), 0.001);
  // Schlick Fresnel (F0 = 0.02 for water)
  float fresnel = 0.02 + 0.98 * pow(1.0 - NdotV, 5.0);

  // Sky reflection
  vec3 R = reflect(-V, N);
  vec3 skyRefl = sampleSkyReflection(R);

  // Deep vs Shallow water base color + wave elevation tint
  float heightFactor = clamp((vWorldPos.y + 2.5) * 0.16, 0.0, 1.0);
  vec3 waterBody = mix(uWaterDeep, uWaterShallow, heightFactor * 0.45 + (1.0 - NdotV) * 0.25);

  // Subsurface Scattering (SSS): emerald/turquoise glow through thin wave crests toward the sun
  float backLight = max(dot(V, -normalize(uSunDir + N * 0.45)), 0.0);
  float crestThickness = clamp((vWorldPos.y * 0.35 + crestFactor * 0.8), 0.0, 1.4);
  float upwardSun = max(uSunDir.y + 0.2, 0.1);
  vec3 sssColor = uWaterShallow * vec3(0.15, 1.25, 1.05) * (0.4 + 0.6 * uSunColor);
  vec3 sss = sssColor * (pow(backLight, 3.0) * 1.4 + (1.0 - N.y) * 0.6) * crestThickness * upwardSun;

  vec3 refracted = waterBody + sss;

  // GGX Sun Specular Highlight (glittering sun path across waves)
  vec3 H = normalize(V + uSunDir);
  float NdotH = max(dot(N, H), 0.0);
  float NdotL = max(dot(N, uSunDir), 0.0);
  float roughness = mix(0.055, 0.16, foamMask);
  float a2 = roughness * roughness * roughness * roughness;
  float denom = (NdotH * NdotH * (a2 - 1.0) + 1.0);
  float D = a2 / (3.14159 * denom * denom + 0.0001);
  vec3 sunSpec = uSunColor * D * fresnel * NdotL * 1.45 * (1.0 - foamMask * 0.75);

  // Combine Refraction, Reflection, Specular, and aerated Whitewater Foam
  vec3 color = mix(refracted, skyRefl, fresnel) + sunSpec;

  vec3 foamColor = mix(uSkyHorizon, uSunColor * 0.9 + vec3(0.45), max(uSunDir.y, 0.25)) * 0.95;
  color = mix(color, foamColor, foamMask * 0.92);

  // Aiming Reticle projected onto the 3D water surface
  if (uAimReticle.w > 0.5) {
    float rDist = length(vWorldPos.xz - uAimReticle.xy);
    float rad = uAimReticle.z;
    float outerRing = smoothstep(0.35, 0.0, abs(rDist - rad));
    float innerDot = smoothstep(0.65, 0.0, rDist);
    float crossHair = (abs(vWorldPos.x - uAimReticle.x) < 0.18 || abs(vWorldPos.z - uAimReticle.y) < 0.18)
                      && rDist < rad * 1.35 && rDist > rad * 0.45 ? 0.65 : 0.0;
    float reticleMask = clamp(outerRing + innerDot + crossHair, 0.0, 1.0);
    color = mix(color, vec3(0.0, 1.0, 0.75) * 1.8, reticleMask * 0.75);
  }

  // Distance atmospheric fog blending seamlessly into the sky horizon
  float fogFactor = 1.0 - exp(-dist * 0.00145);
  color = mix(color, uSkyHorizon, clamp(fogFactor, 0.0, 1.0));

  // Tonemapping & Gamma
  color = color / (1.0 + color * 0.35);
  color = pow(color, vec3(1.0 / 2.2));
  fragColor = vec4(color, 1.0);
}
`;

// ----------------------------------------------------------------------------
// 3. 3D MESH SHADER (Boats, Turrets, Projectiles, Crates, Wet Waterline)
// ----------------------------------------------------------------------------
const MESH_VS = `#version 300 es
precision highp float;
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec3 aColor;

uniform mat4 uProj;
uniform mat4 uView;
uniform mat4 uModel;

out vec3 vWorldPos;
out vec3 vNormal;
out vec3 vColor;
out float vLocalY;

void main() {
  vec4 wp = uModel * vec4(aPos, 1.0);
  vWorldPos = wp.xyz;
  vNormal = normalize(mat3(uModel) * aNormal);
  vColor = aColor;
  vLocalY = aPos.y;
  gl_Position = uProj * uView * wp;
}
`;

const MESH_FS = `#version 300 es
precision highp float;
in vec3 vWorldPos;
in vec3 vNormal;
in vec3 vColor;
in float vLocalY;

out vec4 fragColor;

uniform vec3 uCameraPos;
uniform vec3 uSunDir;
uniform vec3 uSunColor;
uniform vec3 uSkyZenith;
uniform vec3 uSkyHorizon;
uniform float uWaterHeightAtBoat;
uniform float uEmissive;
uniform float uDamageFlash;

void main() {
  vec3 N = normalize(vNormal);
  vec3 V = normalize(uCameraPos - vWorldPos);
  vec3 L = normalize(uSunDir);
  vec3 H = normalize(V + L);

  // Wet hull darkening near and below the wave line
  float wetLine = smoothstep(uWaterHeightAtBoat + 0.9, uWaterHeightAtBoat - 0.2, vWorldPos.y);
  vec3 baseCol = vColor * mix(1.0, 0.52, wetLine);

  // Hemispherical ambient + Sun Lambert + Blinn-Phong Specular
  float NdotL = max(dot(N, L), 0.0);
  vec3 ambient = mix(uSkyHorizon * 0.35, uSkyZenith * 0.55, N.y * 0.5 + 0.5);
  vec3 diffuse = uSunColor * NdotL * 0.95;

  float specPower = mix(32.0, 128.0, wetLine);
  float specStrength = mix(0.28, 0.85, wetLine);
  float spec = pow(max(dot(N, H), 0.0), specPower) * specStrength * NdotL;

  // Rim light from water bounce
  float rim = pow(1.0 - max(dot(N, V), 0.0), 3.0) * 0.25;

  vec3 col = baseCol * (ambient + diffuse) + uSunColor * spec + uSkyHorizon * rim;
  col += vColor * uEmissive;
  col = mix(col, vec3(2.2, 0.4, 0.2), uDamageFlash);

  float dist = length(uCameraPos - vWorldPos);
  float fogFactor = 1.0 - exp(-dist * 0.00145);
  col = mix(col, uSkyHorizon, clamp(fogFactor, 0.0, 1.0));

  col = col / (1.0 + col * 0.35);
  col = pow(col, vec3(1.0 / 2.2));
  fragColor = vec4(col, 1.0);
}
`;

// ----------------------------------------------------------------------------
// 4. 3D PARTICLE SHADER (Spray, Cannon Smoke, Fireballs, Plumes)
// ----------------------------------------------------------------------------
const PARTICLE_VS = `#version 300 es
precision highp float;
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec4 aColor;
layout(location = 2) in float aSize;

uniform mat4 uProj;
uniform mat4 uView;
uniform float uViewportHeight;

out vec4 vColor;

void main() {
  vec4 eyePos = uView * vec4(aPos, 1.0);
  gl_Position = uProj * eyePos;
  vColor = aColor;
  float dist = max(-eyePos.z, 1.0);
  gl_PointSize = clamp((aSize * uViewportHeight * 0.55) / dist, 1.0, 120.0);
}
`;

const PARTICLE_FS = `#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;

void main() {
  vec2 uv = gl_PointCoord * 2.0 - 1.0;
  float r2 = dot(uv, uv);
  if (r2 > 1.0) discard;
  float alpha = (1.0 - r2) * (1.0 - r2) * vColor.a;
  fragColor = vec4(vColor.rgb, alpha);
}
`;

// Compile all programs
const skyProg = createProgram(SKY_VS, SKY_FS);
const waterProg = createProgram(WATER_VS, WATER_FS);
const meshProg = createProgram(MESH_VS, MESH_FS);
const particleProg = createProgram(PARTICLE_VS, PARTICLE_FS);

// ============================================================================
// PROCEDURAL GEOMETRY BUILDERS (Water Grid, Warship Hull, Turret, Enemy, Crate)
// ============================================================================
function createSkyQuad() {
  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);
  const vbo = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
  return { vao, count: 4 };
}

function createWaterMesh(gridN = 210) {
  // Nonlinear radial-style grid: ultra-dense near the boat, stretching to 950m horizon
  const verts = [];
  const indices = [];
  const half = gridN / 2;

  for (let z = 0; z <= gridN; z++) {
    for (let x = 0; x <= gridN; x++) {
      const nx = (x - half) / half; // -1..1
      const nz = (z - half) / half;
      // Nonlinear cubic+linear mapping gives ~0.4m resolution near center and 900m at edges
      const wx = (nx * 0.16 + nx * nx * nx * 0.84) * 850.0;
      const wz = (nz * 0.16 + nz * nz * nz * 0.84) * 850.0;
      verts.push(wx, wz);
    }
  }

  const row = gridN + 1;
  for (let z = 0; z < gridN; z++) {
    for (let x = 0; x < gridN; x++) {
      const i0 = z * row + x;
      const i1 = i0 + 1;
      const i2 = i0 + row;
      const i3 = i2 + 1;
      indices.push(i0, i2, i1, i1, i2, i3);
    }
  }

  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);

  const vbo = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

  const ibo = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);

  return { vao, count: indices.length };
}

// Helper to construct 3D triangle meshes with flat/smooth normals and vertex colors
class MeshBuilder {
  constructor() {
    this.data = []; // x,y,z, nx,ny,nz, r,g,b
  }
  addTri(p1, p2, p3, color) {
    const ux = p2[0] - p1[0], uy = p2[1] - p1[1], uz = p2[2] - p1[2];
    const vx = p3[0] - p1[0], vy = p3[1] - p1[1], vz = p3[2] - p1[2];
    let nx = uy * vz - uz * vy;
    let ny = uz * vx - ux * vz;
    let nz = ux * vy - uy * vx;
    const len = Math.hypot(nx, ny, nz) || 1;
    nx /= len; ny /= len; nz /= len;
    for (const p of [p1, p2, p3]) {
      this.data.push(p[0], p[1], p[2], nx, ny, nz, color[0], color[1], color[2]);
    }
  }
  addQuad(p1, p2, p3, p4, color) {
    this.addTri(p1, p2, p3, color);
    this.addTri(p1, p3, p4, color);
  }
  addBox(cx, cy, cz, sx, sy, sz, color, topColor = color) {
    const x0 = cx - sx * 0.5, x1 = cx + sx * 0.5;
    const y0 = cy - sy * 0.5, y1 = cy + sy * 0.5;
    const z0 = cz - sz * 0.5, z1 = cz + sz * 0.5;
    // Top
    this.addQuad([x0,y1,z0], [x0,y1,z1], [x1,y1,z1], [x1,y1,z0], topColor);
    // Bottom
    this.addQuad([x0,y0,z1], [x0,y0,z0], [x1,y0,z0], [x1,y0,z1], color);
    // Front (+Z)
    this.addQuad([x0,y0,z1], [x1,y0,z1], [x1,y1,z1], [x0,y1,z1], color);
    // Back (-Z)
    this.addQuad([x1,y0,z0], [x0,y0,z0], [x0,y1,z0], [x1,y1,z0], color);
    // Right (+X)
    this.addQuad([x1,y0,z1], [x1,y0,z0], [x1,y1,z0], [x1,y1,z1], color);
    // Left (-X)
    this.addQuad([x0,y0,z0], [x0,y0,z1], [x0,y1,z1], [x0,y1,z0], color);
  }
  addCylinderZ(cx, cy, z0, z1, r0, r1, segs, color) {
    for (let i = 0; i < segs; i++) {
      const a0 = (i / segs) * Math.PI * 2;
      const a1 = ((i + 1) / segs) * Math.PI * 2;
      const c0 = Math.cos(a0), s0 = Math.sin(a0);
      const c1 = Math.cos(a1), s1 = Math.sin(a1);
      const p1 = [cx + c0 * r0, cy + s0 * r0, z0];
      const p2 = [cx + c1 * r0, cy + s1 * r0, z0];
      const p3 = [cx + c1 * r1, cy + s1 * r1, z1];
      const p4 = [cx + c0 * r1, cy + s0 * r1, z1];
      this.addQuad(p1, p4, p3, p2, color);
    }
  }
  build() {
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);
    const vbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
    const arr = new Float32Array(this.data);
    gl.bufferData(gl.ARRAY_BUFFER, arr, gl.STATIC_DRAW);
    const stride = 9 * 4;
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(1);
    gl.vertexAttribPointer(1, 3, gl.FLOAT, false, stride, 3 * 4);
    gl.enableVertexAttribArray(2);
    gl.vertexAttribPointer(2, 3, gl.FLOAT, false, stride, 6 * 4);
    return { vao, count: arr.length / 9 };
  }
}

function buildWarshipHullMesh(hullCol, deckCol, cabinCol, trimCol) {
  const mb = new MeshBuilder();
  // Articulated V-hull stations from Stern (z = -6.0) to Bow (z = +6.8)
  const stations = [
    { z: -6.0, wDeck: 2.0, wChine: 1.75, yKeel: -1.1, yChine: -0.45, yDeck: 0.75 },
    { z: -3.2, wDeck: 2.35, wChine: 2.05, yKeel: -1.35, yChine: -0.55, yDeck: 0.72 },
    { z:  0.0, wDeck: 2.45, wChine: 2.10, yKeel: -1.40, yChine: -0.55, yDeck: 0.78 },
    { z:  3.2, wDeck: 1.85, wChine: 1.35, yKeel: -1.30, yChine: -0.45, yDeck: 0.95 },
    { z:  5.4, wDeck: 0.75, wChine: 0.38, yKeel: -1.05, yChine: -0.25, yDeck: 1.18 },
    { z:  6.8, wDeck: 0.04, wChine: 0.02, yKeel: -0.40, yChine:  0.30, yDeck: 1.38 }
  ];

  const keelCol = [0.48, 0.11, 0.11]; // Anti-fouling red oxide lower hull

  for (let i = 0; i < stations.length - 1; i++) {
    const s0 = stations[i], s1 = stations[i + 1];
    // Starboard (+X) Upper Hull (Chine to Deck)
    mb.addQuad([s0.wChine, s0.yChine, s0.z], [s1.wChine, s1.yChine, s1.z], [s1.wDeck, s1.yDeck, s1.z], [s0.wDeck, s0.yDeck, s0.z], hullCol);
    // Port (-X) Upper Hull
    mb.addQuad([-s1.wChine, s1.yChine, s1.z], [-s0.wChine, s0.yChine, s0.z], [-s0.wDeck, s0.yDeck, s0.z], [-s1.wDeck, s1.yDeck, s1.z], hullCol);

    // Starboard Lower Hull (Keel to Chine)
    mb.addQuad([0, s0.yKeel, s0.z], [0, s1.yKeel, s1.z], [s1.wChine, s1.yChine, s1.z], [s0.wChine, s0.yChine, s0.z], keelCol);
    // Port Lower Hull
    mb.addQuad([0, s1.yKeel, s1.z], [0, s0.yKeel, s0.z], [-s0.wChine, s0.yChine, s0.z], [-s1.wChine, s1.yChine, s1.z], keelCol);

    // Main Deck Surface
    mb.addQuad([-s0.wDeck, s0.yDeck, s0.z], [s0.wDeck, s0.yDeck, s0.z], [s1.wDeck, s1.yDeck, s1.z], [-s1.wDeck, s1.yDeck, s1.z], deckCol);

    // Gunwale armored coaming strips
    mb.addBox( s0.wDeck * 0.94, s0.yDeck + 0.12, (s0.z + s1.z) * 0.5, 0.12, 0.22, s1.z - s0.z, trimCol);
    mb.addBox(-s0.wDeck * 0.94, s0.yDeck + 0.12, (s0.z + s1.z) * 0.5, 0.12, 0.22, s1.z - s0.z, trimCol);
  }

  // Stern Transom Cap
  const st = stations[0];
  mb.addQuad([st.wChine, st.yChine, st.z], [-st.wChine, st.yChine, st.z], [-st.wDeck, st.yDeck, st.z], [st.wDeck, st.yDeck, st.z], hullCol);
  mb.addTri([0, st.yKeel, st.z], [st.wChine, st.yChine, st.z], [-st.wChine, st.yChine, st.z], keelCol);

  // Armored Superstructure & Bridge Cabin
  mb.addBox(0, 1.35, -1.1, 2.5, 1.1, 3.8, cabinCol, deckCol);
  // Slanted Bridge Command House
  mb.addBox(0, 2.15, -0.5, 2.0, 0.75, 2.1, cabinCol, deckCol);
  // Glowing Cyan/Amber Bridge Windows
  mb.addBox(0, 2.22, 0.58, 1.75, 0.32, 0.1, trimCol);
  mb.addBox(0, 2.22, -0.3, 2.06, 0.30, 1.2, trimCol);

  // Smokestack / Exhaust Funnel (tilted aft)
  mb.addBox(0, 2.35, -2.2, 0.95, 1.3, 1.1, cabinCol, [0.1, 0.1, 0.12]);
  // Radar Mast Tower
  mb.addBox(0, 3.1, -1.1, 0.28, 1.6, 0.28, cabinCol);
  mb.addBox(0, 3.75, -1.1, 1.6, 0.15, 0.22, trimCol);

  // Forward Turret Barbet Base
  mb.addCylinderZ(0, 0.85, 2.1, 2.2, 0.9, 0.9, 12, cabinCol);
  // Rear Depth-Salvo Racks
  mb.addBox(-1.1, 1.05, -4.6, 0.65, 0.55, 1.4, cabinCol, trimCol);
  mb.addBox( 1.1, 1.05, -4.6, 0.65, 0.55, 1.4, cabinCol, trimCol);

  return mb.build();
}

function buildTurretMesh(turretCol, barrelCol, accentCol) {
  const mb = new MeshBuilder();
  // Armored angled turret housing
  mb.addBox(0, 0.35, -0.1, 1.45, 0.65, 1.65, turretCol, accentCol);
  mb.addBox(0, 0.32,  0.65, 1.10, 0.48, 0.6, turretCol);
  // Twin 120mm Rifled Barrels extending along +Z
  mb.addCylinderZ(-0.34, 0.36, 0.6, 3.15, 0.13, 0.10, 10, barrelCol);
  mb.addCylinderZ( 0.34, 0.36, 0.6, 3.15, 0.13, 0.10, 10, barrelCol);
  // Muzzle brakes
  mb.addCylinderZ(-0.34, 0.36, 3.05, 3.35, 0.16, 0.15, 10, accentCol);
  mb.addCylinderZ( 0.34, 0.36, 3.05, 3.35, 0.16, 0.15, 10, accentCol);
  return mb.build();
}

function buildShellMesh() {
  const mb = new MeshBuilder();
  mb.addBox(0, 0, 0, 0.36, 0.36, 0.95, [1.0, 0.82, 0.3]);
  return mb.build();
}

function buildCrateMesh() {
  const mb = new MeshBuilder();
  mb.addBox(0, 0, 0, 1.8, 1.4, 1.8, [0.15, 0.75, 0.42], [0.25, 0.95, 0.55]);
  mb.addBox(0, 0, 0, 1.9, 0.3, 1.9, [0.95, 0.85, 0.2]);
  return mb.build();
}

const skyGeo = createSkyQuad();
const waterGeo = createWaterMesh(210);
const playerHullGeo = buildWarshipHullMesh([0.24, 0.33, 0.42], [0.18, 0.23, 0.29], [0.34, 0.43, 0.52], [0.15, 0.82, 1.0]);
const playerTurretGeo = buildTurretMesh([0.32, 0.42, 0.52], [0.16, 0.20, 0.24], [0.15, 0.85, 1.0]);

const enemyHullGeo = buildWarshipHullMesh([0.28, 0.22, 0.22], [0.18, 0.15, 0.16], [0.36, 0.26, 0.26], [1.0, 0.32, 0.15]);
const enemyTurretGeo = buildTurretMesh([0.36, 0.25, 0.25], [0.15, 0.15, 0.15], [1.0, 0.32, 0.15]);

const shellGeo = buildShellMesh();
const crateGeo = buildCrateMesh();

// ============================================================================
// OCEAN HYDRODYNAMICS: GERSTNER WAVES + ROGUE WAVE + EXACT BUOYANCY SAMPLER
// ============================================================================
const WAVES = [
  { dx:  0.88, dz:  0.47, q: 0.34, a: 0.78, l: 48.0 },
  { dx:  0.96, dz: -0.28, q: 0.32, a: 0.56, l: 31.0 },
  { dx:  0.52, dz:  0.85, q: 0.30, a: 0.42, l: 21.0 },
  { dx: -0.35, dz:  0.93, q: 0.26, a: 0.28, l: 14.5 },
  { dx:  0.76, dz: -0.65, q: 0.24, a: 0.19, l:  9.8 },
  { dx: -0.82, dz: -0.57, q: 0.22, a: 0.14, l:  6.7 },
  { dx:  0.22, dz: -0.97, q: 0.20, a: 0.09, l:  4.3 },
  { dx: -0.60, dz:  0.80, q: 0.18, a: 0.06, l:  2.8 }
];

// Normalize directions and precompute wave numbers k & dispersion speeds omega
const uWavesAData = new Float32Array(8 * 4);
const uWavesBData = new Float32Array(8 * 4);

WAVES.forEach((w, i) => {
  const len = Math.hypot(w.dx, w.dz) || 1;
  w.dx /= len;
  w.dz /= len;
  w.k = (Math.PI * 2) / w.l;
  w.omega = Math.sqrt(9.81 * w.k); // Deep-water dispersion relation
  uWavesAData.set([w.dx, w.dz, w.q, w.a], i * 4);
  uWavesBData.set([w.k, w.omega, 0, 0], i * 4);
});

// Occasional Rogue Wave state
const rogueWave = {
  active: false,
  intensity: 0.0,
  dirX: 0.85,
  dirZ: 0.52,
  centerDist: -320,
  speed: 24.0,
  amplitude: 7.4,
  width: 28.0,
  cooldown: 16.0
};

function triggerRogueWave(playerPos) {
  const angle = Math.random() * Math.PI * 2;
  rogueWave.dirX = Math.cos(angle);
  rogueWave.dirZ = Math.sin(angle);
  const playerProj = playerPos.x * rogueWave.dirX + playerPos.z * rogueWave.dirZ;
  rogueWave.centerDist = playerProj - 250.0;
  rogueWave.active = true;
  rogueWave.intensity = 0.01;
  rogueWave.cooldown = 32.0;
  audio.playRogueHorn();
  document.getElementById('alertBanner').classList.add('visible');
}

// Sea State & Lighting Presets
const SEA_PRESETS = {
  sunset: {
    sunDir: new THREE.Vector3(0.75, 0.20, -0.62).normalize(),
    sunColor: [1.55, 0.78, 0.36],
    skyZenith: [0.07, 0.16, 0.34],
    skyHorizon: [0.85, 0.44, 0.25],
    waterDeep: [0.01, 0.05, 0.10],
    waterShallow: [0.02, 0.32, 0.34],
    waveScale: 1.0,
    stormFactor: 0.15
  },
  noon: {
    sunDir: new THREE.Vector3(0.35, 0.84, -0.42).normalize(),
    sunColor: [1.35, 1.30, 1.18],
    skyZenith: [0.06, 0.30, 0.68],
    skyHorizon: [0.52, 0.78, 0.95],
    waterDeep: [0.005, 0.08, 0.18],
    waterShallow: [0.0, 0.58, 0.56],
    waveScale: 0.88,
    stormFactor: 0.0
  },
  storm: {
    sunDir: new THREE.Vector3(-0.55, 0.30, -0.78).normalize(),
    sunColor: [0.72, 0.80, 0.88],
    skyZenith: [0.05, 0.08, 0.12],
    skyHorizon: [0.24, 0.30, 0.35],
    waterDeep: [0.008, 0.03, 0.05],
    waterShallow: [0.03, 0.20, 0.22],
    waveScale: 1.52,
    stormFactor: 0.9
  },
  night: {
    sunDir: new THREE.Vector3(-0.62, 0.38, 0.68).normalize(),
    sunColor: [0.45, 0.65, 0.98],
    skyZenith: [0.01, 0.02, 0.06],
    skyHorizon: [0.06, 0.12, 0.22],
    waterDeep: [0.003, 0.015, 0.04],
    waterShallow: [0.01, 0.16, 0.24],
    waveScale: 1.12,
    stormFactor: 0.35
  }
};

let currentPreset = SEA_PRESETS.sunset;

// Sample exact Gerstner + Rogue wave height, surface normal, and horizontal surge velocity at (worldX, worldZ)
function sampleWaterAt(worldX, worldZ, time) {
  const scale = currentPreset.waveScale;
  // Two-step fixed-point inversion for Gerstner horizontal displacement so physics matches visual mesh 1:1
  let bx = worldX, bz = worldZ;
  for (let iter = 0; iter < 2; iter++) {
    let ox = 0, oz = 0;
    for (let i = 0; i < 8; i++) {
      const w = WAVES[i];
      const a = w.a * scale;
      const phase = w.k * (w.dx * bx + w.dz * bz) - w.omega * time;
      const c = Math.cos(phase);
      ox -= w.q * a * w.dx * c;
      oz -= w.q * a * w.dz * c;
    }
    bx = worldX - ox;
    bz = worldZ - oz;
  }

  let y = 0;
  let tx = 1, ty = 0, tz = 0;
  let bnx = 0, bny = 0, bnz = 1;
  let vx = 0, vz = 0;

  for (let i = 0; i < 8; i++) {
    const w = WAVES[i];
    const a = w.a * scale;
    const phase = w.k * (w.dx * bx + w.dz * bz) - w.omega * time;
    const s = Math.sin(phase);
    const c = Math.cos(phase);
    y += a * s;

    const ka = w.k * a;
    tx -= w.q * w.dx * w.dx * ka * s;
    ty += w.dx * ka * c;
    tz -= w.q * w.dx * w.dz * ka * s;

    bnx -= w.q * w.dx * w.dz * ka * s;
    bny += w.dz * ka * c;
    bnz -= w.q * w.dz * w.dz * ka * s;

    // Orbital wave surge velocity
    const orb = w.omega * a * s;
    vx += w.dx * orb;
    vz += w.dz * orb;
  }

  // Add Rogue Wave contribution if active
  let isRogueHit = 0;
  if (rogueWave.intensity > 0.001) {
    const rDirX = rogueWave.dirX, rDirZ = rogueWave.dirZ;
    const proj = bx * rDirX + bz * rDirZ;
    const dist = proj - rogueWave.centerDist;
    const normDist = dist / rogueWave.width;
    const env = Math.exp(-normDist * normDist);
    const rAmp = rogueWave.amplitude * rogueWave.intensity;

    y += rAmp * env;
    const dEnv = (-2.0 * normDist / rogueWave.width) * rAmp * env;
    ty += rDirX * dEnv;
    bny += rDirZ * dEnv;

    // Strong horizontal surge force from the rogue wave wall
    const surge = env * rogueWave.speed * 0.68 * rogueWave.intensity;
    vx += rDirX * surge;
    vz += rDirZ * surge;
    isRogueHit = env * rogueWave.intensity;
  }

  // Normal = cross(binormal, tangent)
  let nx = bny * tz - bnz * ty;
  let ny = bnz * tx - bnx * tz;
  let nz = bnx * ty - bny * tx;
  const nLen = Math.hypot(nx, ny, nz) || 1;

  return {
    height: y,
    nx: nx / nLen,
    ny: ny / nLen,
    nz: nz / nLen,
    vx,
    vz,
    isRogueHit
  };
}

// ============================================================================
// MULTI-POINT RIGID-BODY BUOYANCY BOAT CLASS
// ============================================================================
class Warship {
  constructor(x, z, isPlayer = false) {
    this.isPlayer = isPlayer;
    this.pos = new THREE.Vector3(x, 0, z);
    this.vel = new THREE.Vector3(0, 0, 0);
    this.yaw = isPlayer ? 0 : Math.random() * Math.PI * 2;
    this.pitch = 0;
    this.roll = 0;
    this.yawVel = 0;
    this.pitchVel = 0;
    this.rollVel = 0;

    this.throttle = 0;
    this.rudder = 0;
    this.boostFuel = 1.0;
    this.isBoosting = false;

    this.turretYaw = this.yaw;
    this.turretPitch = 0.05;
    this.recoil = 0;

    this.maxHp = isPlayer ? 100 : 45;
    this.hp = this.maxHp;
    this.sinking = false;
    this.sinkTimer = 0;
    this.damageFlash = 0;

    this.primaryCooldown = 0;
    this.secondaryCooldown = 0;
    this.wakeTimer = 0;
    this.waterHeight = 0;

    // 5 Hull Pontoon sample points in local coordinates: Center, Bow, Stern, Starboard, Port
    this.pontoons = [
      new THREE.Vector3( 0.0, -0.5,  0.0), // Center
      new THREE.Vector3( 0.0, -0.4,  5.2), // Bow
      new THREE.Vector3( 0.0, -0.5, -5.0), // Stern
      new THREE.Vector3( 2.1, -0.4,  0.0), // Starboard
      new THREE.Vector3(-2.1, -0.4,  0.0)  // Port
    ];

    this.modelMatrix = new THREE.Matrix4();
    this.turretMatrix = new THREE.Matrix4();
  }

  updatePhysics(dt, time) {
    if (this.damageFlash > 0) this.damageFlash = Math.max(0, this.damageFlash - dt * 4.0);
    if (this.recoil > 0) this.recoil = Math.max(0, this.recoil - dt * 3.5);

    this.modelMatrix.composeYPR(this.pos, this.yaw, this.pitch, this.roll, new THREE.Vector3(1, 1, 1));

    if (this.sinking) {
      this.sinkTimer += dt;
      this.pos.y -= dt * 2.2;
      this.pitch += dt * 0.35;
      this.roll += dt * 0.45;
      this.modelMatrix.composeYPR(this.pos, this.yaw, this.pitch, this.roll, new THREE.Vector3(1, 1, 1));
      return;
    }

    // Evaluate water at each of the 5 pontoons
    const wSamples = [];
    let avgWaterY = 0;
    let avgWaveVx = 0, avgWaveVz = 0;
    let totalSubmerged = 0;
    let maxRogueHit = 0;

    for (let i = 0; i < 5; i++) {
      const wp = this.modelMatrix.transformPoint(this.pontoons[i]);
      const ws = sampleWaterAt(wp.x, wp.z, time);
      const depth = ws.height - wp.y;
      wSamples.push({ wp, ws, depth });
      avgWaterY += ws.height;
      avgWaveVx += ws.vx;
      avgWaveVz += ws.vz;
      if (depth > 0) totalSubmerged += Math.min(depth, 2.5);
      if (ws.isRogueHit > maxRogueHit) maxRogueHit = ws.isRogueHit;
    }
    avgWaterY /= 5;
    avgWaveVx /= 5;
    avgWaveVz /= 5;
    this.waterHeight = avgWaterY;

    // Vertical Buoyancy & Gravity
    const targetFloatY = avgWaterY + 0.18;
    const yError = targetFloatY - this.pos.y;
    // Spring-damper vertical hydrostatics + wave vertical acceleration
    const buoyAccel = yError * 26.0 - this.vel.y * 5.2;
    this.vel.y += buoyAccel * dt;
    this.pos.y += this.vel.y * dt;

    // Pitch torque from Bow vs Stern wave height difference
    const bowWaterY = wSamples[1].ws.height;
    const sternWaterY = wSamples[2].ws.height;
    const wavePitchTarget = -Math.atan2(bowWaterY - sternWaterY, 10.2);
    // Add bow-up planing trim under forward throttle + recoil kick
    const planingPitch = -Math.max(0, this.throttle) * (this.isBoosting ? 0.12 : 0.065);
    const pitchDiff = (wavePitchTarget + planingPitch) - this.pitch;
    this.pitchVel += (pitchDiff * 24.0 - this.pitchVel * 4.8) * dt;
    this.pitch = Math.max(-0.65, Math.min(0.65, this.pitch + this.pitchVel * dt));

    // Roll torque from Port vs Starboard wave height difference + centrifugal turn lean
    const stbdWaterY = wSamples[3].ws.height;
    const portWaterY = wSamples[4].ws.height;
    const waveRollTarget = Math.atan2(portWaterY - stbdWaterY, 4.2);
    const turnLean = -this.rudder * (this.vel.length() * 0.012);
    const rollDiff = (waveRollTarget + turnLean) - this.roll;
    this.rollVel += (rollDiff * 28.0 - this.rollVel * 4.4) * dt;
    this.roll = Math.max(-0.75, Math.min(0.75, this.roll + this.rollVel * dt));

    // Forward & Lateral Hydrodynamics
    const fwdX = Math.sin(this.yaw);
    const fwdZ = Math.cos(this.yaw);
    const rightX = Math.cos(this.yaw);
    const rightZ = -Math.sin(this.yaw);

    const maxThrust = (this.isPlayer ? 46.0 : 32.0) * (this.isBoosting ? 1.65 : 1.0);
    const contactFactor = Math.min(1.0, Math.max(0.25, totalSubmerged * 0.35));

    // Apply engine thrust + wave slope gravity slide + wave orbital surge
    const centerNorm = wSamples[0].ws;
    const wavePushX = centerNorm.nx * 14.0 + avgWaveVx * 0.45;
    const wavePushZ = centerNorm.nz * 14.0 + avgWaveVz * 0.45;

    this.vel.x += (fwdX * this.throttle * maxThrust * contactFactor + wavePushX) * dt;
    this.vel.z += (fwdZ * this.throttle * maxThrust * contactFactor + wavePushZ) * dt;

    // Anisotropic hull drag: low drag along keel (forward), very high drag sideways (keel resistance)
    const fwdSpeed = this.vel.x * fwdX + this.vel.z * fwdZ;
    const sideSpeed = this.vel.x * rightX + this.vel.z * rightZ;

    const dampedFwd = fwdSpeed * Math.exp(-1.55 * dt);
    const dampedSide = sideSpeed * Math.exp(-6.5 * dt);

    this.vel.x = fwdX * dampedFwd + rightX * dampedSide;
    this.vel.z = fwdZ * dampedFwd + rightZ * dampedSide;

    this.pos.x += this.vel.x * dt;
    this.pos.z += this.vel.z * dt;

    // Rudder Yaw Torque (responsive at both low and high speeds)
    const steerEffectiveness = (0.45 + Math.min(1.0, Math.abs(fwdSpeed) / 10.0) * 0.85) * (fwdSpeed >= -1.0 ? 1 : -1);
    const targetYawVel = this.rudder * (this.isPlayer ? 1.55 : 1.15) * steerEffectiveness;
    this.yawVel += (targetYawVel - this.yawVel) * 6.5 * dt;
    this.yaw += this.yawVel * dt;

    // Update matrices
    this.modelMatrix.composeYPR(this.pos, this.yaw, this.pitch, this.roll, new THREE.Vector3(1, 1, 1));
    const turretWorldPos = this.modelMatrix.transformPoint(new THREE.Vector3(0, 0.88, 2.15));
    // Apply recoil offset along turret barrel
    const recX = Math.sin(this.turretYaw) * this.recoil * 0.35;
    const recZ = Math.cos(this.turretYaw) * this.recoil * 0.35;
    turretWorldPos.x -= recX;
    turretWorldPos.z -= recZ;
    this.turretMatrix.composeYPR(turretWorldPos, this.turretYaw, this.pitch * 0.5 - this.turretPitch, this.roll * 0.5, new THREE.Vector3(1, 1, 1));

    // Spawn bow spray & wake trail when moving or hit by rogue wave
    const speed = Math.hypot(this.vel.x, this.vel.z);
    this.wakeTimer += dt * (1.0 + speed * 0.3);
    if (this.wakeTimer > 0.14 && speed > 1.8) {
      this.wakeTimer = 0;
      const sternPos = this.modelMatrix.transformPoint(new THREE.Vector3(0, 0, -5.6));
      addWakePoint(sternPos.x, sternPos.z, 2.2 + Math.min(2.2, speed * 0.08));

      // Bow whitewater spray particles
      const bowPos = this.modelMatrix.transformPoint(new THREE.Vector3(0, 0.2, 5.8));
      if (bowPos.y < wSamples[1].ws.height + 0.6) {
        for (let s = -1; s <= 1; s += 2) {
          spawnParticle(
            bowPos.x + rightX * s * 0.7,
            wSamples[1].ws.height + 0.2,
            bowPos.z + rightZ * s * 0.7,
            this.vel.x * 0.35 + rightX * s * (3.5 + Math.random() * 2.5),
            2.5 + speed * 0.16 + Math.random() * 2.0,
            this.vel.z * 0.35 + rightZ * s * (3.5 + Math.random() * 2.5),
            [0.88, 0.96, 1.0, 0.75],
            1.4 + Math.random() * 1.2,
            0.65,
            true
          );
        }
      }
    }

    // Rogue wave crashing over deck effect
    if (maxRogueHit > 0.45 && Math.random() < 0.45) {
      const deckPos = this.modelMatrix.transformPoint(new THREE.Vector3((Math.random() - 0.5) * 3, 1.0, (Math.random() - 0.5) * 8));
      spawnParticle(
        deckPos.x, deckPos.y, deckPos.z,
        avgWaveVx * 0.5 + (Math.random() - 0.5) * 8,
        5.0 + Math.random() * 5.0,
        avgWaveVz * 0.5 + (Math.random() - 0.5) * 8,
        [0.85, 0.95, 1.0, 0.85],
        2.5,
        0.9,
        true
      );
      if (this.isPlayer) {
        addLensDroplets(3);
        audio.playSplash(1.4);
      }
    }
  }
}

// ============================================================================
// WAKE BUFFER, SPLASH BUFFER, PROJECTILES, CRATES & 3D PARTICLES
// ============================================================================
const MAX_WAKE = 24;
const wakeList = [];
function addWakePoint(x, z, radius) {
  wakeList.unshift({ x, z, radius, age: 0.01 });
  if (wakeList.length > MAX_WAKE) wakeList.pop();
}

const MAX_SPLASH = 12;
const splashList = [];
function addSplashRing(x, z, radius) {
  splashList.unshift({ x, z, radius, age: 0.01 });
  if (splashList.length > MAX_SPLASH) splashList.pop();
}

const MAX_PARTICLES = 900;
const particles = [];
const particlePos = new Float32Array(MAX_PARTICLES * 3);
const particleCol = new Float32Array(MAX_PARTICLES * 4);
const particleSize = new Float32Array(MAX_PARTICLES);

const particleVAO = gl.createVertexArray();
gl.bindVertexArray(particleVAO);
const pPosBuf = gl.createBuffer();
const pColBuf = gl.createBuffer();
const pSizeBuf = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, pPosBuf);
gl.bufferData(gl.ARRAY_BUFFER, particlePos.byteLength, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, pColBuf);
gl.bufferData(gl.ARRAY_BUFFER, particleCol.byteLength, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, pSizeBuf);
gl.bufferData(gl.ARRAY_BUFFER, particleSize.byteLength, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 0, 0);

function spawnParticle(x, y, z, vx, vy, vz, color, size, life, hasGravity = true) {
  if (particles.length >= MAX_PARTICLES) particles.shift();
  particles.push({
    x, y, z, vx, vy, vz,
    r: color[0], g: color[1], b: color[2], a: color[3],
    size, maxLife: life, life, hasGravity
  });
}

function spawnWaterPlume(x, y, z, scale = 1.0) {
  addSplashRing(x, z, 3.8 * scale);
  const count = Math.floor(24 * scale);
  for (let i = 0; i < count; i++) {
    const ang = Math.random() * Math.PI * 2;
    const spd = (1.5 + Math.random() * 5.5) * scale;
    spawnParticle(
      x + (Math.random() - 0.5) * 1.2,
      y + 0.2,
      z + (Math.random() - 0.5) * 1.2,
      Math.cos(ang) * spd,
      (7.0 + Math.random() * 11.0) * scale,
      Math.sin(ang) * spd,
      [0.88, 0.96, 1.0, 0.9],
      (1.8 + Math.random() * 2.2) * scale,
      0.75 + Math.random() * 0.55,
      true
    );
  }
}

function spawnExplosionFX(x, y, z, scale = 1.0) {
  addSplashRing(x, z, 5.5 * scale);
  for (let i = 0; i < Math.floor(32 * scale); i++) {
    const ang = Math.random() * Math.PI * 2;
    const elev = (Math.random() - 0.2) * Math.PI * 0.5;
    const spd = (3.0 + Math.random() * 14.0) * scale;
    const isFire = i < 18 * scale;
    spawnParticle(
      x, y + 0.5, z,
      Math.cos(ang) * Math.cos(elev) * spd,
      Math.sin(elev) * spd + 4.5,
      Math.sin(ang) * Math.cos(elev) * spd,
      isFire ? [1.0, 0.45 + Math.random() * 0.45, 0.1, 0.95] : [0.18, 0.18, 0.2, 0.85],
      (2.2 + Math.random() * 3.0) * scale,
      0.6 + Math.random() * 0.9,
      !isFire
    );
  }
}

// Screen-space lens water droplets when waves crash over the player boat
const lensDroplets = [];
function addLensDroplets(count) {
  for (let i = 0; i < count; i++) {
    lensDroplets.push({
      x: Math.random() * window.innerWidth,
      y: Math.random() * window.innerHeight,
      r: 8 + Math.random() * 22,
      alpha: 0.65 + Math.random() * 0.25,
      vy: 15 + Math.random() * 35
    });
  }
}

// ============================================================================
// GAME STATE, PLAYER CONTROLS, ENEMY AI & COMBAT LOOP
// ============================================================================
let player = new Warship(0, 0, true);
let enemies = [];
let projectiles = [];
let crates = [];
let score = 0;
let waveNumber = 1;
let gameOver = false;

// Input state
const keys = {};
let mouseNDC = { x: 0, y: 0 };
let mouseDownLeft = false;
let mouseDownRight = false;
let cameraDist = 28.0;
let cameraPos = new THREE.Vector3(0, 11, -28);
let cameraLookAt = new THREE.Vector3(0, 1.5, 8);
let aimWorldPos = new THREE.Vector3(0, 0, 45);

window.addEventListener('keydown', (e) => {
  keys[e.code] = true;
  audio.init();
});
window.addEventListener('keyup', (e) => {
  keys[e.code] = false;
});
window.addEventListener('mousemove', (e) => {
  mouseNDC.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouseNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
window.addEventListener('mousedown', (e) => {
  if (e.target.tagName === 'BUTTON') return;
  audio.init();
  if (e.button === 0) mouseDownLeft = true;
  if (e.button === 2) mouseDownRight = true;
});
window.addEventListener('mouseup', (e) => {
  if (e.button === 0) mouseDownLeft = false;
  if (e.button === 2) mouseDownRight = false;
});
window.addEventListener('contextmenu', (e) => e.preventDefault());
window.addEventListener('wheel', (e) => {
  cameraDist = Math.max(15.0, Math.min(55.0, cameraDist + e.deltaY * 0.02));
}, { passive: true });

// UI Buttons
document.querySelectorAll('[data-preset]').forEach((btn) => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('[data-preset]').forEach((b) => b.classList.remove('active'));
    btn.classList.add('active');
    currentPreset = SEA_PRESETS[btn.dataset.preset];
  });
});

document.getElementById('rogueBtn').addEventListener('click', () => {
  audio.init();
  triggerRogueWave(player.pos);
});

document.getElementById('audioBtn').addEventListener('click', (e) => {
  const on = audio.toggle();
  e.target.textContent = on ? '🔊 Audio: ON' : '🔇 Audio: OFF';
});

document.getElementById('restartBtn').addEventListener('click', () => {
  resetGame();
});

function spawnEnemyWave(waveNum) {
  const count = 2 + waveNum;
  for (let i = 0; i < count; i++) {
    const angle = (i / count) * Math.PI * 2 + Math.random() * 0.5;
    const dist = 125 + Math.random() * 65;
    const ex = player.pos.x + Math.sin(angle) * dist;
    const ez = player.pos.z + Math.cos(angle) * dist;
    const enemy = new Warship(ex, ez, false);
    enemy.maxHp = 40 + waveNum * 10;
    enemy.hp = enemy.maxHp;
    enemy.primaryCooldown = 1.5 + Math.random() * 2.0;
    enemies.push(enemy);
  }
}

function resetGame() {
  player = new Warship(0, 0, true);
  enemies = [];
  projectiles = [];
  crates = [];
  score = 0;
  waveNumber = 1;
  gameOver = false;
  document.getElementById('modalOverlay').classList.remove('show');
  spawnEnemyWave(waveNumber);
}

function fireMainTurret(boat, targetPos, isPlayer) {
  const muzzle1 = boat.turretMatrix.transformPoint(new THREE.Vector3(-0.34, 0.36, 3.35));
  const muzzle2 = boat.turretMatrix.transformPoint(new THREE.Vector3( 0.34, 0.36, 3.35));

  const dx = targetPos.x - muzzle1.x;
  const dz = targetPos.z - muzzle1.z;
  const horizDist = Math.hypot(dx, dz) || 1;
  const shellSpeed = isPlayer ? 72.0 : 52.0;
  const gravity = 22.0;

  // Compute ballistic flight time & required vertical launch velocity so shells land right on the target!
  const flightTime = Math.min(3.5, horizDist / shellSpeed);
  const dy = (targetPos.y + 0.4) - muzzle1.y;
  const vy = (dy + 0.5 * gravity * flightTime * flightTime) / Math.max(0.15, flightTime);
  const dirX = dx / horizDist;
  const dirZ = dz / horizDist;

  for (const m of [muzzle1, muzzle2]) {
    projectiles.push({
      pos: m.clone(),
      vel: new THREE.Vector3(
        dirX * shellSpeed + (Math.random() - 0.5) * (isPlayer ? 0.6 : 2.2),
        vy + (Math.random() - 0.5) * 0.6,
        dirZ * shellSpeed + (Math.random() - 0.5) * (isPlayer ? 0.6 : 2.2)
      ),
      isPlayer,
      damage: isPlayer ? 16 : 9,
      life: 4.0
    });

    // Muzzle flash & smoke particles
    for (let k = 0; k < 7; k++) {
      spawnParticle(
        m.x, m.y, m.z,
        dirX * (8 + Math.random() * 10) + (Math.random() - 0.5) * 3,
        1.5 + Math.random() * 2.5,
        dirZ * (8 + Math.random() * 10) + (Math.random() - 0.5) * 3,
        k < 3 ? [1.0, 0.78, 0.25, 0.95] : [0.65, 0.68, 0.72, 0.65],
        1.6 + Math.random() * 1.4,
        0.35 + Math.random() * 0.25,
        false
      );
    }
  }

  // Physical recoil pitches/rolls the boat slightly
  boat.recoil = 1.0;
  boat.pitchVel -= 0.16;
  audio.playCannon(true);
}

function fireSalvoMortar(boat, targetPos) {
  const shellSpeed = 48.0;
  const gravity = 22.0;
  for (let i = -2; i <= 2; i++) {
    const launchPos = boat.modelMatrix.transformPoint(new THREE.Vector3(i * 0.6, 1.3, -4.5));
    const spreadX = targetPos.x + (Math.random() - 0.5) * 9.0;
    const spreadZ = targetPos.z + (Math.random() - 0.5) * 9.0;
    const dx = spreadX - launchPos.x;
    const dz = spreadZ - launchPos.z;
    const dist = Math.hypot(dx, dz) || 1;
    const t = Math.min(3.8, dist / shellSpeed);
    const vy = ((targetPos.y - launchPos.y) + 0.5 * gravity * t * t) / Math.max(0.2, t);

    projectiles.push({
      pos: launchPos,
      vel: new THREE.Vector3((dx / dist) * shellSpeed, vy, (dz / dist) * shellSpeed),
      isPlayer: true,
      damage: 14,
      life: 4.5
    });

    spawnParticle(launchPos.x, launchPos.y, launchPos.z, 0, 5, 0, [1.0, 0.6, 0.2, 0.9], 2.2, 0.45, false);
  }
  boat.pitchVel += 0.22;
  audio.playCannon(false);
}

// Compute 3D world intersection of mouse ray with the ocean surface
function updateMouseAimRay(invProj, invView, time) {
  // Near and far points in clip space
  const pElements = invProj.elements;
  const x = mouseNDC.x, y = mouseNDC.y;
  // View space ray
  const vx = x / (1.0 / pElements[0]);
  const vy = y / (1.0 / pElements[5]);
  const vDir = new THREE.Vector3(vx, vy, -1.0).normalize();
  const worldDir = invView.transformDirection(vDir);

  if (worldDir.y < -0.015) {
    const avgWater = player.waterHeight;
    const t = Math.min(280.0, Math.max(8.0, (avgWater - cameraPos.y) / worldDir.y));
    aimWorldPos.set(
      cameraPos.x + worldDir.x * t,
      avgWater,
      cameraPos.z + worldDir.z * t
    );
    // Refine once with actual wave height at that XZ
    const ws = sampleWaterAt(aimWorldPos.x, aimWorldPos.z, time);
    aimWorldPos.y = ws.height;
  } else {
    // Horizon aim fallback
    aimWorldPos.set(
      cameraPos.x + worldDir.x * 220.0,
      player.waterHeight,
      cameraPos.z + worldDir.z * 220.0
    );
  }
}

// ============================================================================
// MAIN SIMULATION & RENDER LOOP
// ============================================================================
const projMat = new THREE.Matrix4();
const viewMat = new THREE.Matrix4();
const invProjMat = new THREE.Matrix4();
const invViewMat = new THREE.Matrix4();
const tempMat = new THREE.Matrix4();

function resize() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.floor(window.innerWidth * dpr);
  canvas.height = Math.floor(window.innerHeight * dpr);
  fxCanvas.width = window.innerWidth;
  fxCanvas.height = window.innerHeight;
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

// Spawn initial wave of enemy warships
spawnEnemyWave(waveNumber);

let lastTime = performance.now() * 0.001;

function animate(nowMs) {
  requestAnimationFrame(animate);
  const time = nowMs * 0.001;
  const dt = Math.min(0.04, Math.max(0.001, time - lastTime));
  lastTime = time;

  // --- 1. UPDATE ROGUE WAVE SYSTEM ---
  if (!rogueWave.active) {
    rogueWave.cooldown -= dt;
    if (rogueWave.cooldown <= 0) {
      triggerRogueWave(player.pos);
    }
  } else {
    rogueWave.centerDist += rogueWave.speed * dt;
    const playerProj = player.pos.x * rogueWave.dirX + player.pos.z * rogueWave.dirZ;
    const relDist = rogueWave.centerDist - playerProj;
    if (relDist < 90) {
      rogueWave.intensity = Math.min(1.0, rogueWave.intensity + dt * 0.6);
    } else {
      rogueWave.intensity = Math.max(0.0, rogueWave.intensity - dt * 0.45);
      if (rogueWave.intensity <= 0.001) {
        rogueWave.active = false;
        document.getElementById('alertBanner').classList.remove('visible');
      }
    }
    if (relDist > 25) {
      document.getElementById('alertBanner').classList.remove('visible');
    }
  }

  // --- 2. PLAYER INPUT & CONTROLS ---
  if (!gameOver) {
    let targetThrottle = 0;
    if (keys['KeyW'] || keys['ArrowUp']) targetThrottle += 1.0;
    if (keys['KeyS'] || keys['ArrowDown']) targetThrottle -= 0.65;
    player.throttle += (targetThrottle - player.throttle) * 5.0 * dt;

    let targetRudder = 0;
    if (keys['KeyA'] || keys['ArrowLeft']) targetRudder += 1.0;
    if (keys['KeyD'] || keys['ArrowRight']) targetRudder -= 1.0;
    player.rudder += (targetRudder - player.rudder) * 7.0 * dt;

    // Hydro-Boost
    if ((keys['ShiftLeft'] || keys['ShiftRight'] || keys['Space']) && player.boostFuel > 0.05 && Math.abs(targetThrottle) > 0.1) {
      player.isBoosting = true;
      player.boostFuel = Math.max(0, player.boostFuel - dt * 0.38);
    } else {
      player.isBoosting = false;
      player.boostFuel = Math.min(1.0, player.boostFuel + dt * 0.18);
    }

    // Smoothly rotate player turret toward 3D mouse aim point
    const turretOrigin = player.modelMatrix.transformPoint(new THREE.Vector3(0, 0.88, 2.15));
    const desiredYaw = Math.atan2(aimWorldPos.x - turretOrigin.x, aimWorldPos.z - turretOrigin.z);
    let yawDiff = desiredYaw - player.turretYaw;
    while (yawDiff > Math.PI) yawDiff -= Math.PI * 2;
    while (yawDiff < -Math.PI) yawDiff += Math.PI * 2;
    player.turretYaw += yawDiff * Math.min(1.0, dt * 12.0);

    // Weapon cooldowns & firing
    player.primaryCooldown = Math.max(0, player.primaryCooldown - dt);
    player.secondaryCooldown = Math.max(0, player.secondaryCooldown - dt);

    if (mouseDownLeft && player.primaryCooldown <= 0) {
      fireMainTurret(player, aimWorldPos, true);
      player.primaryCooldown = 0.42;
    }
    if (mouseDownRight && player.secondaryCooldown <= 0) {
      fireSalvoMortar(player, aimWorldPos);
      player.secondaryCooldown = 2.4;
    }
  }

  player.updatePhysics(dt, time);

  // Update continuous engine & wave audio
  const playerSpeed = Math.hypot(player.vel.x, player.vel.z);
  audio.updateContinuous(
    Math.min(1.0, playerSpeed / 22.0),
    currentPreset.waveScale * 0.6 + rogueWave.intensity * 0.8,
    Math.max(0, (player.waterHeight - player.pos.y) * 0.5)
  );

  // --- 3. UPDATE ENEMY WARSHIPS & AI ---
  for (let i = enemies.length - 1; i >= 0; i--) {
    const e = enemies[i];
    if (!e.sinking) {
      const toPlayerX = player.pos.x - e.pos.x;
      const toPlayerZ = player.pos.z - e.pos.z;
      const dist = Math.hypot(toPlayerX, toPlayerZ) || 1;

      // Desired navigation angle: circle around player at ~55m combat distance
      const directAngle = Math.atan2(toPlayerX, toPlayerZ);
      const orbitOffset = dist < 68 ? Math.PI * 0.38 : (dist < 105 ? Math.PI * 0.18 : 0.0);
      const desiredNavYaw = directAngle + orbitOffset;

      let navDiff = desiredNavYaw - e.yaw;
      while (navDiff > Math.PI) navDiff -= Math.PI * 2;
      while (navDiff < -Math.PI) navDiff += Math.PI * 2;

      e.rudder = Math.max(-1.0, Math.min(1.0, navDiff * 1.8));
      e.throttle = dist > 35 ? 0.85 : 0.45;

      // Avoid colliding with other enemy boats
      for (let j = 0; j < enemies.length; j++) {
        if (i === j) continue;
        const other = enemies[j];
        const sepX = e.pos.x - other.pos.x;
        const sepZ = e.pos.z - other.pos.z;
        const sepDist = Math.hypot(sepX, sepZ);
        if (sepDist < 22.0 && sepDist > 0.1) {
          e.vel.x += (sepX / sepDist) * 14.0 * dt;
          e.vel.z += (sepZ / sepDist) * 14.0 * dt;
        }
      }

      // Aim turret with predictive target leading on the player boat
      const leadTime = Math.min(2.2, dist / 52.0);
      const leadPos = new THREE.Vector3(
        player.pos.x + player.vel.x * leadTime,
        player.pos.y,
        player.pos.z + player.vel.z * leadTime
      );
      const desiredTurretYaw = Math.atan2(leadPos.x - e.pos.x, leadPos.z - e.pos.z);
      let tDiff = desiredTurretYaw - e.turretYaw;
      while (tDiff > Math.PI) tDiff -= Math.PI * 2;
      while (tDiff < -Math.PI) tDiff += Math.PI * 2;
      e.turretYaw += tDiff * Math.min(1.0, dt * 5.0);

      // Fire at player when within range
      e.primaryCooldown -= dt;
      if (e.primaryCooldown <= 0 && dist < 175.0 && !gameOver) {
        fireMainTurret(e, leadPos, false);
        e.primaryCooldown = 2.2 + Math.random() * 1.4;
      }

      // Smoke plume if heavily damaged
      if (e.hp < e.maxHp * 0.5 && Math.random() < 0.35) {
        spawnParticle(
          e.pos.x + (Math.random() - 0.5) * 2,
          e.pos.y + 2.0,
          e.pos.z + (Math.random() - 0.5) * 2,
          (Math.random() - 0.5) * 2, 3.5 + Math.random() * 2, (Math.random() - 0.5) * 2,
          [0.18, 0.18, 0.2, 0.75], 2.4, 1.1, false
        );
      }
    }

    e.updatePhysics(dt, time);

    if (e.sinking && e.sinkTimer > 3.5) {
      enemies.splice(i, 1);
    }
  }

  // Spawn next fleet wave if all enemies defeated
  if (enemies.length === 0 && !gameOver) {
    waveNumber++;
    score += 250;
    spawnEnemyWave(waveNumber);
  }

  // --- 4. UPDATE PROJECTILES & COLLISIONS ---
  for (let i = projectiles.length - 1; i >= 0; i--) {
    const p = projectiles[i];
    p.life -= dt;
    p.vel.y -= 22.0 * dt; // Gravity
    p.pos.addScaledVector(p.vel, dt);

    // Tracer smoke trail
    if (Math.random() < 0.65) {
      spawnParticle(
        p.pos.x, p.pos.y, p.pos.z,
        0, 0.4, 0,
        p.isPlayer ? [0.7, 0.9, 1.0, 0.45] : [1.0, 0.6, 0.4, 0.45],
        0.9, 0.28, false
      );
    }

    let hitSomething = false;

    // Check boat hits
    if (p.isPlayer) {
      for (const e of enemies) {
        if (e.sinking) continue;
        const dx = p.pos.x - e.pos.x;
        const dz = p.pos.z - e.pos.z;
        const dy = p.pos.y - (e.pos.y + 1.0);
        if (dx * dx + dz * dz < 28.0 && Math.abs(dy) < 3.2) {
          e.hp -= p.damage;
          e.damageFlash = 1.0;
          e.rollVel += (Math.random() - 0.5) * 0.9;
          spawnExplosionFX(p.pos.x, p.pos.y, p.pos.z, 0.75);
          audio.playExplosion();
          hitSomething = true;

          if (e.hp <= 0 && !e.sinking) {
            e.sinking = true;
            score += 100;
            spawnExplosionFX(e.pos.x, e.pos.y + 1.0, e.pos.z, 1.6);
            // Drop floating repair crate
            if (Math.random() < 0.65) {
              crates.push({ pos: new THREE.Vector3(e.pos.x, e.pos.y, e.pos.z), yaw: 0 });
            }
          }
          break;
        }
      }
    } else if (!gameOver) {
      const dx = p.pos.x - player.pos.x;
      const dz = p.pos.z - player.pos.z;
      const dy = p.pos.y - (player.pos.y + 1.0);
      if (dx * dx + dz * dz < 26.0 && Math.abs(dy) < 3.2) {
        player.hp = Math.max(0, player.hp - p.damage);
        player.damageFlash = 1.0;
        player.rollVel += (Math.random() - 0.5) * 0.85;
        spawnExplosionFX(p.pos.x, p.pos.y, p.pos.z, 0.75);
        addLensDroplets(2);
        audio.playExplosion();
        hitSomething = true;

        if (player.hp <= 0 && !gameOver) {
          gameOver = true;
          player.sinking = true;
          spawnExplosionFX(player.pos.x, player.pos.y + 1.0, player.pos.z, 2.0);
          document.getElementById('finalStatsText').textContent =
            `Your warship was claimed by the sea on Wave ${waveNumber} with a combat score of ${score}.`;
          document.getElementById('modalOverlay').classList.add('show');
        }
      }
    }

    // Check water impact
    if (!hitSomething) {
      const ws = sampleWaterAt(p.pos.x, p.pos.z, time);
      if (p.pos.y <= ws.height) {
        spawnWaterPlume(p.pos.x, ws.height, p.pos.z, 0.9);
        if (p.pos.distanceTo(player.pos) < 85) audio.playSplash(0.8);
        hitSomething = true;
      }
    }

    if (hitSomething || p.life <= 0) {
      projectiles.splice(i, 1);
    }
  }

  // --- 5. UPDATE REPAIR CRATES ---
  for (let i = crates.length - 1; i >= 0; i--) {
    const c = crates[i];
    const ws = sampleWaterAt(c.pos.x, c.pos.z, time);
    c.pos.y = ws.height + 0.25;
    c.yaw += dt * 1.4;
    if (c.pos.distanceTo(player.pos) < 8.5 && !gameOver) {
      player.hp = Math.min(player.maxHp, player.hp + 30);
      score += 50;
      crates.splice(i, 1);
    }
  }

  // --- 6. UPDATE WAKE, SPLASHES & PARTICLES ---
  for (let i = wakeList.length - 1; i >= 0; i--) {
    wakeList[i].age += dt * 0.24;
    if (wakeList[i].age >= 1.0) wakeList.splice(i, 1);
  }
  for (let i = splashList.length - 1; i >= 0; i--) {
    splashList[i].age += dt * 0.55;
    if (splashList[i].age >= 1.0) splashList.splice(i, 1);
  }
  for (let i = particles.length - 1; i >= 0; i--) {
    const pt = particles[i];
    pt.life -= dt;
    if (pt.life <= 0) {
      particles.splice(i, 1);
      continue;
    }
    if (pt.hasGravity) pt.vy -= 18.0 * dt;
    pt.x += pt.vx * dt;
    pt.y += pt.vy * dt;
    pt.z += pt.vz * dt;
  }

  // --- 7. UPDATE 3RD-PERSON FOLLOW CAMERA ---
  const camYaw = player.yaw - mouseNDC.x * 0.55;
  const targetCamPos = new THREE.Vector3(
    player.pos.x - Math.sin(camYaw) * cameraDist,
    Math.max(player.waterHeight + 3.8, player.pos.y + 7.5 + cameraDist * 0.24 - mouseNDC.y * 3.5),
    player.pos.z - Math.cos(camYaw) * cameraDist
  );
  // Prevent camera from dipping below waves at camera's own XZ
  const camWave = sampleWaterAt(targetCamPos.x, targetCamPos.z, time);
  targetCamPos.y = Math.max(targetCamPos.y, camWave.height + 2.4);

  cameraPos.lerp(targetCamPos, Math.min(1.0, dt * 6.5));

  const targetLook = new THREE.Vector3(
    player.pos.x + Math.sin(player.yaw) * 7.5,
    player.pos.y + 1.8,
    player.pos.z + Math.cos(player.yaw) * 7.5
  );
  cameraLookAt.lerp(targetLook, Math.min(1.0, dt * 8.5));

  const aspect = canvas.width / canvas.height;
  projMat.makePerspective(55.0, aspect, 0.5, 1800.0);
  viewMat.makeLookAt(cameraPos, cameraLookAt, new THREE.Vector3(Math.sin(player.roll) * 0.14, 1.0, 0.0).normalize());

  // Compute inverse matrices for sky ray-casting & mouse aiming
  // Inverse Perspective
  const pe = projMat.elements;
  invProjMat.identity();
  invProjMat.elements[0] = 1.0 / pe[0];
  invProjMat.elements[5] = 1.0 / pe[5];
  invProjMat.elements[11] = 1.0 / pe[14];
  invProjMat.elements[14] = -1.0;
  invProjMat.elements[15] = pe[10] / pe[14];

  // Inverse View (transpose of 3x3 rotation + camera position)
  const ve = viewMat.elements;
  invViewMat.identity();
  invViewMat.elements[0] = ve[0]; invViewMat.elements[4] = ve[1]; invViewMat.elements[8]  = ve[2];
  invViewMat.elements[1] = ve[4]; invViewMat.elements[5] = ve[5]; invViewMat.elements[9]  = ve[6];
  invViewMat.elements[2] = ve[8]; invViewMat.elements[6] = ve[9]; invViewMat.elements[10] = ve[10];
  invViewMat.elements[12] = cameraPos.x;
  invViewMat.elements[13] = cameraPos.y;
  invViewMat.elements[14] = cameraPos.z;

  updateMouseAimRay(invProjMat, invViewMat, time);

  // ==========================================================================
  // RENDER PASS 1: ATMOSPHERIC SKY DOME
  // ==========================================================================
  gl.disable(gl.DEPTH_TEST);
  gl.depthMask(false);
  gl.useProgram(skyProg);
  gl.uniformMatrix4fv(gl.getUniformLocation(skyProg, 'uInvProj'), false, invProjMat.elements);
  gl.uniformMatrix4fv(gl.getUniformLocation(skyProg, 'uInvView'), false, invViewMat.elements);
  gl.uniform3f(gl.getUniformLocation(skyProg, 'uSunDir'), currentPreset.sunDir.x, currentPreset.sunDir.y, currentPreset.sunDir.z);
  gl.uniform3fv(gl.getUniformLocation(skyProg, 'uSunColor'), currentPreset.sunColor);
  gl.uniform3fv(gl.getUniformLocation(skyProg, 'uSkyZenith'), currentPreset.skyZenith);
  gl.uniform3fv(gl.getUniformLocation(skyProg, 'uSkyHorizon'), currentPreset.skyHorizon);
  gl.uniform1f(gl.getUniformLocation(skyProg, 'uTime'), time);
  gl.uniform1f(gl.getUniformLocation(skyProg, 'uStormFactor'), currentPreset.stormFactor);

  gl.bindVertexArray(skyGeo.vao);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, skyGeo.count);

  // ==========================================================================
  // RENDER PASS 2: ULTRA-REALISTIC GERSTNER & ROGUE WAVE OCEAN
  // ==========================================================================
  gl.enable(gl.DEPTH_TEST);
  gl.depthMask(true);
  gl.clear(gl.DEPTH_BUFFER_BIT);

  gl.useProgram(waterProg);
  gl.uniformMatrix4fv(gl.getUniformLocation(waterProg, 'uProj'), false, projMat.elements);
  gl.uniformMatrix4fv(gl.getUniformLocation(waterProg, 'uView'), false, viewMat.elements);
  gl.uniform3f(gl.getUniformLocation(waterProg, 'uCameraPos'), cameraPos.x, cameraPos.y, cameraPos.z);
  gl.uniform1f(gl.getUniformLocation(waterProg, 'uTime'), time);
  gl.uniform1f(gl.getUniformLocation(waterProg, 'uWaveScale'), currentPreset.waveScale);
  gl.uniform4fv(gl.getUniformLocation(waterProg, 'uWavesA'), uWavesAData);
  gl.uniform4fv(gl.getUniformLocation(waterProg, 'uWavesB'), uWavesBData);

  gl.uniform4f(gl.getUniformLocation(waterProg, 'uRogueA'), rogueWave.dirX, rogueWave.dirZ, rogueWave.centerDist, rogueWave.amplitude);
  gl.uniform4f(gl.getUniformLocation(waterProg, 'uRogueB'), rogueWave.width, rogueWave.intensity, 0, 0);

  gl.uniform3f(gl.getUniformLocation(waterProg, 'uSunDir'), currentPreset.sunDir.x, currentPreset.sunDir.y, currentPreset.sunDir.z);
  gl.uniform3fv(gl.getUniformLocation(waterProg, 'uSunColor'), currentPreset.sunColor);
  gl.uniform3fv(gl.getUniformLocation(waterProg, 'uSkyZenith'), currentPreset.skyZenith);
  gl.uniform3fv(gl.getUniformLocation(waterProg, 'uSkyHorizon'), currentPreset.skyHorizon);
  gl.uniform3fv(gl.getUniformLocation(waterProg, 'uWaterDeep'), currentPreset.waterDeep);
  gl.uniform3fv(gl.getUniformLocation(waterProg, 'uWaterShallow'), currentPreset.waterShallow);
  gl.uniform1f(gl.getUniformLocation(waterProg, 'uStormFactor'), currentPreset.stormFactor);

  // Pack Wake uniform array
  const wakeUniform = new Float32Array(MAX_WAKE * 4);
  for (let i = 0; i < wakeList.length; i++) {
    const w = wakeList[i];
    wakeUniform.set([w.x, w.z, w.radius, w.age], i * 4);
  }
  gl.uniform4fv(gl.getUniformLocation(waterProg, 'uWake'), wakeUniform);

  // Pack Splash uniform array
  const splashUniform = new Float32Array(MAX_SPLASH * 4);
  for (let i = 0; i < splashList.length; i++) {
    const sp = splashList[i];
    splashUniform.set([sp.x, sp.z, sp.radius, sp.age], i * 4);
  }
  gl.uniform4fv(gl.getUniformLocation(waterProg, 'uSplashes'), splashUniform);

  // Pack Boats uniform array for hull contact foam
  const boatsUniform = new Float32Array(10 * 4);
  let boatCount = 0;
  if (!player.sinking) {
    boatsUniform.set([player.pos.x, player.pos.z, player.yaw, 6.4], 0);
    boatCount = 1;
  }
  for (let i = 0; i < enemies.length && boatCount < 10; i++) {
    const e = enemies[i];
    if (!e.sinking) {
      boatsUniform.set([e.pos.x, e.pos.z, e.yaw, 6.4], boatCount * 4);
      boatCount++;
    }
  }
  gl.uniform4fv(gl.getUniformLocation(waterProg, 'uBoats'), boatsUniform);
  gl.uniform1i(gl.getUniformLocation(waterProg, 'uNumBoats'), boatCount);

  gl.uniform4f(gl.getUniformLocation(waterProg, 'uAimReticle'), aimWorldPos.x, aimWorldPos.z, 3.2, gameOver ? 0.0 : 1.0);

  gl.bindVertexArray(waterGeo.vao);
  gl.drawElements(gl.TRIANGLES, waterGeo.count, gl.UNSIGNED_INT, 0);

  // ==========================================================================
  // RENDER PASS 3: 3D WARSHIPS, TURRETS, SHELLS & SUPPLY CRATES
  // ==========================================================================
  gl.useProgram(meshProg);
  gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, 'uProj'), false, projMat.elements);
  gl.uniformMatrix4fv(gl.getUniformLocation(meshProg, 'uView'), false, viewMat.elements);
  gl.uniform3f(gl.getUniformLocation(meshProg, 'uCameraPos'), cameraPos.x, cameraPos.y, cameraPos.z);
  gl.uniform3f(gl.getUniformLocation(meshProg, 'uSunDir'), currentPreset.sunDir.x, currentPreset.sunDir.y, currentPreset.sunDir.z);
  gl.uniform3fv(gl.getUniformLocation(meshProg, 'uSunColor'), currentPreset.sunColor);
  gl.uniform3fv(gl.getUniformLocation(meshProg, 'uSkyZenith'), currentPreset.skyZenith);
  gl.uniform3fv(gl.getUniformLocation(meshProg, 'uSkyHorizon'), currentPreset.skyHorizon);

  const uModelLoc = gl.getUniformLocation(meshProg, 'uModel');
  const uWaterHtLoc = gl.getUniformLocation(meshProg, 'uWaterHeightAtBoat');
  const uEmissiveLoc = gl.getUniformLocation(meshProg, 'uEmissive');
  const uDamageLoc = gl.getUniformLocation(meshProg, 'uDamageFlash');

  // Render Player Boat & Turret
  gl.uniform1f(uWaterHtLoc, player.waterHeight);
  gl.uniform1f(uEmissiveLoc, 0.08);
  gl.uniform1f(uDamageLoc, player.damageFlash);
  gl.uniformMatrix4fv(uModelLoc, false, player.modelMatrix.elements);
  gl.bindVertexArray(playerHullGeo.vao);
  gl.drawArrays(gl.TRIANGLES, 0, playerHullGeo.count);

  gl.uniformMatrix4fv(uModelLoc, false, player.turretMatrix.elements);
  gl.bindVertexArray(playerTurretGeo.vao);
  gl.drawArrays(gl.TRIANGLES, 0, playerTurretGeo.count);

  // Render Enemy Boats & Turrets
  for (const e of enemies) {
    gl.uniform1f(uWaterHtLoc, e.waterHeight);
    gl.uniform1f(uDamageLoc, e.damageFlash);
    gl.uniformMatrix4fv(uModelLoc, false, e.modelMatrix.elements);
    gl.bindVertexArray(enemyHullGeo.vao);
    gl.drawArrays(gl.TRIANGLES, 0, enemyHullGeo.count);

    gl.uniformMatrix4fv(uModelLoc, false, e.turretMatrix.elements);
    gl.bindVertexArray(enemyTurretGeo.vao);
    gl.drawArrays(gl.TRIANGLES, 0, enemyTurretGeo.count);
  }

  // Render Repair Crates
  gl.uniform1f(uDamageLoc, 0.0);
  gl.uniform1f(uEmissiveLoc, 0.35 + Math.sin(time * 5.0) * 0.2);
  gl.bindVertexArray(crateGeo.vao);
  for (const c of crates) {
    gl.uniform1f(uWaterHtLoc, c.pos.y - 0.2);
    tempMat.composeYPR(c.pos, c.yaw, Math.sin(time * 3.0) * 0.15, Math.cos(time * 2.5) * 0.15, new THREE.Vector3(1, 1, 1));
    gl.uniformMatrix4fv(uModelLoc, false, tempMat.elements);
    gl.drawArrays(gl.TRIANGLES, 0, crateGeo.count);
  }

  // Render Artillery Shells
  gl.uniform1f(uEmissiveLoc, 1.4);
  gl.bindVertexArray(shellGeo.vao);
  for (const p of projectiles) {
    const yaw = Math.atan2(p.vel.x, p.vel.z);
    const pitch = -Math.atan2(p.vel.y, Math.hypot(p.vel.x, p.vel.z));
    tempMat.composeYPR(p.pos, yaw, pitch, 0, new THREE.Vector3(1, 1, 1));
    gl.uniformMatrix4fv(uModelLoc, false, tempMat.elements);
    gl.drawArrays(gl.TRIANGLES, 0, shellGeo.count);
  }

  // ==========================================================================
  // RENDER PASS 4: 3D SOFT PARTICLES (SPRAY, SMOKE, EXPLOSIONS)
  // ==========================================================================
  if (particles.length > 0) {
    for (let i = 0; i < particles.length; i++) {
      const pt = particles[i];
      const lifeRatio = pt.life / pt.maxLife;
      particlePos[i * 3] = pt.x;
      particlePos[i * 3 + 1] = pt.y;
      particlePos[i * 3 + 2] = pt.z;
      particleCol[i * 4] = pt.r;
      particleCol[i * 4 + 1] = pt.g;
      particleCol[i * 4 + 2] = pt.b;
      particleCol[i * 4 + 3] = pt.a * lifeRatio;
      particleSize[i] = pt.size * (1.25 - lifeRatio * 0.45);
    }

    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.depthMask(false);

    gl.useProgram(particleProg);
    gl.uniformMatrix4fv(gl.getUniformLocation(particleProg, 'uProj'), false, projMat.elements);
    gl.uniformMatrix4fv(gl.getUniformLocation(particleProg, 'uView'), false, viewMat.elements);
    gl.uniform1f(gl.getUniformLocation(particleProg, 'uViewportHeight'), canvas.height);

    gl.bindVertexArray(particleVAO);
    gl.bindBuffer(gl.ARRAY_BUFFER, pPosBuf);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, particlePos.subarray(0, particles.length * 3));
    gl.bindBuffer(gl.ARRAY_BUFFER, pColBuf);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleCol.subarray(0, particles.length * 4));
    gl.bindBuffer(gl.ARRAY_BUFFER, pSizeBuf);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleSize.subarray(0, particles.length));

    gl.drawArrays(gl.POINTS, 0, particles.length);
    gl.depthMask(true);
    gl.disable(gl.BLEND);
  }

  // ==========================================================================
  // RENDER PASS 5: 2D OVERLAYS (ENEMY HEALTH BARS, LENS DROPLETS, RADAR & HUD)
  // ==========================================================================
  draw2DOverlays(dt);
  updateHUDTelemetry();
}

function draw2DOverlays(dt) {
  fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height);

  // Project 3D enemy health bars onto 2D screen
  const vpMat = new THREE.Matrix4().multiplyMatrices(projMat, viewMat);
  const vpe = vpMat.elements;

  for (const e of enemies) {
    if (e.sinking) continue;
    const wx = e.pos.x, wy = e.pos.y + 4.8, wz = e.pos.z;
    const cx = vpe[0] * wx + vpe[4] * wy + vpe[8] * wz + vpe[12];
    const cy = vpe[1] * wx + vpe[5] * wy + vpe[9] * wz + vpe[13];
    const cw = vpe[3] * wx + vpe[7] * wy + vpe[11] * wz + vpe[15];

    if (cw > 0.1) {
      const sx = (cx / cw * 0.5 + 0.5) * fxCanvas.width;
      const sy = (-cy / cw * 0.5 + 0.5) * fxCanvas.height;

      const barW = 54, barH = 6;
      fxCtx.fillStyle = 'rgba(0, 0, 0, 0.65)';
      fxCtx.fillRect(sx - barW * 0.5 - 1, sy - 1, barW + 2, barH + 2);
      fxCtx.fillStyle = '#ff4757';
      fxCtx.fillRect(sx - barW * 0.5, sy, barW * Math.max(0, e.hp / e.maxHp), barH);
    }
  }

  // Draw water droplets on camera lens when hit by big waves
  for (let i = lensDroplets.length - 1; i >= 0; i--) {
    const d = lensDroplets[i];
    d.y += d.vy * dt;
    d.alpha -= dt * 0.25;
    if (d.alpha <= 0) {
      lensDroplets.splice(i, 1);
      continue;
    }
    const grad = fxCtx.createRadialGradient(d.x, d.y, 1, d.x, d.y, d.r);
    grad.addColorStop(0, `rgba(200, 240, 255, ${d.alpha * 0.55})`);
    grad.addColorStop(0.7, `rgba(120, 200, 255, ${d.alpha * 0.25})`);
    grad.addColorStop(1, 'rgba(120, 200, 255, 0)');
    fxCtx.fillStyle = grad;
    fxCtx.beginPath();
    fxCtx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
    fxCtx.fill();
  }

  // Draw Tactical Naval Radar
  const rw = radarCanvas.width, rh = radarCanvas.height;
  const cx = rw * 0.5, cy = rh * 0.5;
  const radarRange = 230.0;
  radarCtx.clearRect(0, 0, rw, rh);

  // Range rings
  radarCtx.strokeStyle = 'rgba(80, 190, 255, 0.22)';
  radarCtx.lineWidth = 1;
  for (const r of [25, 50, 72]) {
    radarCtx.beginPath();
    radarCtx.arc(cx, cy, r, 0, Math.PI * 2);
    radarCtx.stroke();
  }

  // Rotate radar relative to player heading (Up = Boat Bow)
  radarCtx.save();
  radarCtx.translate(cx, cy);
  radarCtx.rotate(player.yaw);

  // Draw Rogue Wave front line on radar if active
  if (rogueWave.active && rogueWave.intensity > 0.05) {
    const playerProj = player.pos.x * rogueWave.dirX + player.pos.z * rogueWave.dirZ;
    const distToWave = rogueWave.centerDist - playerProj;
    if (Math.abs(distToWave) < radarRange) {
      const wx = (rogueWave.dirX * distToWave / radarRange) * 75;
      const wz = -(rogueWave.dirZ * distToWave / radarRange) * 75;
      const px = -rogueWave.dirZ * 80;
      const pz = -rogueWave.dirX * 80;
      radarCtx.strokeStyle = 'rgba(255, 80, 80, 0.85)';
      radarCtx.lineWidth = 3;
      radarCtx.beginPath();
      radarCtx.moveTo(wx - px, wz - pz);
      radarCtx.lineTo(wx + px, wz + pz);
      radarCtx.stroke();
    }
  }

  // Draw Repair Crates
  radarCtx.fillStyle = '#00e676';
  for (const c of crates) {
    const dx = ((c.pos.x - player.pos.x) / radarRange) * 75;
    const dz = -((c.pos.z - player.pos.z) / radarRange) * 75;
    if (dx * dx + dz * dz < 72 * 72) {
      radarCtx.fillRect(dx - 2.5, dz - 2.5, 5, 5);
    }
  }

  // Draw Enemy Boats
  radarCtx.fillStyle = '#ff4757';
  for (const e of enemies) {
    if (e.sinking) continue;
    const dx = ((e.pos.x - player.pos.x) / radarRange) * 75;
    const dz = -((e.pos.z - player.pos.z) / radarRange) * 75;
    const d = Math.hypot(dx, dz);
    const clampedD = Math.min(72, d);
    const rx = d > 0 ? (dx / d) * clampedD : 0;
    const rz = d > 0 ? (dz / d) * clampedD : 0;
    radarCtx.beginPath();
    radarCtx.arc(rx, rz, 3.5, 0, Math.PI * 2);
    radarCtx.fill();
  }

  radarCtx.restore();

  // Player Boat Icon at Radar Center
  radarCtx.fillStyle = '#00e5ff';
  radarCtx.beginPath();
  radarCtx.moveTo(cx, cy - 6);
  radarCtx.lineTo(cx + 4.5, cy + 5);
  radarCtx.lineTo(cx - 4.5, cy + 5);
  radarCtx.closePath();
  radarCtx.fill();
}

function updateHUDTelemetry() {
  const hpPct = Math.max(0, Math.round((player.hp / player.maxHp) * 100));
  document.getElementById('hullPctText').textContent = `${hpPct}%`;
  const hullBar = document.getElementById('hullBar');
  hullBar.style.width = `${hpPct}%`;
  hullBar.style.background = hpPct > 50
    ? 'linear-gradient(90deg, #00e676, #69f0ae)'
    : (hpPct > 25 ? 'linear-gradient(90deg, #ffab00, #ffd740)' : 'linear-gradient(90deg, #ff1744, #ff5252)');

  document.getElementById('scoreVal').textContent = score;
  document.getElementById('waveVal').textContent = waveNumber;
  document.getElementById('enemiesVal').textContent = enemies.filter((e) => !e.sinking).length;

  const knots = (Math.hypot(player.vel.x, player.vel.z) * 1.65).toFixed(1);
  document.getElementById('speedVal').textContent = knots;
  document.getElementById('pitchVal').textContent = `${Math.round(player.pitch * 57.3)}°`;
  document.getElementById('rollVal').textContent = `${Math.round(player.roll * 57.3)}°`;
  document.getElementById('waveHtVal').textContent = `${player.waterHeight.toFixed(1)}m`;

  const w1Pct = Math.round((1.0 - player.primaryCooldown / 0.42) * 100);
  document.getElementById('w1Bar').style.width = `${w1Pct}%`;
  document.getElementById('w1Status').textContent = player.primaryCooldown <= 0 ? 'READY' : 'RELOAD';

  const w2Pct = Math.round((1.0 - player.secondaryCooldown / 2.4) * 100);
  document.getElementById('w2Bar').style.width = `${w2Pct}%`;
  document.getElementById('w2Status').textContent = player.secondaryCooldown <= 0 ? 'READY' : 'RELOAD';

  const boostPct = Math.round(player.boostFuel * 100);
  document.getElementById('boostBar').style.width = `${boostPct}%`;
  document.getElementById('boostStatus').textContent = `${boostPct}%`;
}

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

Same prompt, other models

6 more
Compare side by side

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