← RTS results

RTS

Gemini 3.8 Flash · full

◈Preview unavailable — try playing the result

Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 112,955 bytes · SHA-256 a0b702dc14b3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>FRONTIER WAR — 3D Sci-Fi Real-Time Strategy</title>
<!-- Three.js CDN for WebGL rendering -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; }
  body, html { width: 100%; height: 100%; overflow: hidden; background: #050811; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; color: #d0e4ff; }
  #canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
  canvas { display: block; width: 100%; height: 100%; }

  /* HUD Layout */
  #hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; display: flex; flex-direction: column; justify-content: space-between; }
  .hud-panel { pointer-events: auto; background: rgba(8, 14, 26, 0.88); border: 1px solid #1e3557; backdrop-filter: blur(4px); box-shadow: 0 4px 18px rgba(0,0,0,0.6); }

  /* Top Bar */
  #top-bar { display: flex; align-items: center; justify-content: space-between; padding: 6px 16px; font-size: 13px; font-weight: 600; border-top: none; border-left: none; border-right: none; }
  .res-badge { display: inline-flex; align-items: center; gap: 6px; margin-right: 18px; }
  .res-icon { width: 14px; height: 14px; border-radius: 3px; display: inline-block; }
  .res-ore { background: #00e5ff; box-shadow: 0 0 8px #00e5ff; }
  .res-sup { background: #ffaa00; box-shadow: 0 0 8px #ffaa00; }
  .top-btn { background: #13243d; border: 1px solid #2d4f7c; color: #aadcff; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 6px; transition: all 0.15s; }
  .top-btn:hover { background: #1f3b64; color: #ffffff; border-color: #00d4ff; }

  /* Alert Banner */
  #alert-box { position: absolute; top: 48px; left: 50%; transform: translateX(-50%); pointer-events: none; text-align: center; }
  .alert-msg { background: rgba(220, 20, 40, 0.9); color: #fff; padding: 6px 16px; border-radius: 4px; font-weight: 600; font-size: 13px; margin-bottom: 4px; animation: fadeout 4s forwards; box-shadow: 0 0 10px rgba(255, 30, 60, 0.6); }
  @keyframes fadeout { 0% { opacity: 0; transform: translateY(-8px); } 15% { opacity: 1; transform: translateY(0); } 80% { opacity: 1; } 100% { opacity: 0; transform: translateY(-8px); } }

  /* Bottom Bar Area */
  #bottom-bar { display: flex; height: 160px; margin: 0 0 0 0; border-bottom: none; border-left: none; border-right: none; background: rgba(8, 14, 26, 0.95); }
  #minimap-wrap { width: 160px; height: 160px; border-right: 1px solid #1e3557; position: relative; background: #03070d; flex-shrink: 0; box-shadow: inset 0 0 10px rgba(0,0,0,0.8); }
  #minimap { width: 100%; height: 100%; display: block; cursor: crosshair; }
  #minimap-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }

  /* Selection Info Panel */
  #info-panel { width: 270px; padding: 10px 14px; border-right: 1px solid #1e3557; display: flex; flex-direction: column; justify-content: space-between; flex-shrink: 0; }
  #info-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #1c304d; padding-bottom: 4px; margin-bottom: 6px; }
  #info-title { font-weight: 700; font-size: 14px; color: #64b5f6; }
  #info-count { font-size: 12px; color: #8198b5; }
  #info-body { font-size: 12px; line-height: 1.4; color: #bcd3eb; flex: 1; overflow-y: auto; }
  .hp-bar-bg { width: 100%; height: 7px; background: #1c2738; border-radius: 3px; overflow: hidden; margin: 4px 0 6px 0; }
  .hp-bar-fill { height: 100%; background: #00e676; transition: width 0.1s; }
  .shield-bar-fill { height: 100%; background: #00b0ff; transition: width 0.1s; }

  /* Action Commands Grid */
  #action-panel { flex: 1; padding: 10px 14px; display: flex; flex-direction: column; justify-content: space-between; }
  #action-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
  .cmd-btn { background: #101c2e; border: 1px solid #233b5d; border-radius: 4px; padding: 6px 4px; color: #bcd3eb; font-size: 11px; font-weight: 600; cursor: pointer; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 48px; transition: all 0.12s; position: relative; }
  .cmd-btn:hover:not(:disabled) { background: #1d3354; border-color: #00d4ff; color: #fff; transform: translateY(-1px); }
  .cmd-btn:active:not(:disabled) { transform: translateY(1px); }
  .cmd-btn:disabled { opacity: 0.38; cursor: not-allowed; border-color: #1a2433; }
  .cmd-btn .key { position: absolute; top: 2px; left: 4px; font-size: 9px; color: #ffb74d; }
  .cmd-btn .cost { font-size: 9px; color: #00e5ff; margin-top: 2px; }
  .cmd-btn.active { background: #006064; border-color: #00e5ff; color: #fff; }

  /* Sub info / Queue bar */
  #queue-bar { display: flex; align-items: center; gap: 4px; height: 20px; margin-top: 4px; }
  .queue-slot { width: 22px; height: 18px; background: #142236; border: 1px solid #2a4163; border-radius: 2px; font-size: 9px; display: flex; align-items: center; justify-content: center; color: #90caf9; }
  .queue-slot.active { border-color: #00e5ff; background: #183d5a; }

  /* Drag selection box */
  #select-box { position: absolute; border: 1px solid #00e5ff; background: rgba(0, 229, 255, 0.12); pointer-events: none; display: none; z-index: 5; }

  /* Modal Overlays (Menu / Pause / Victory / Defeat / Help) */
  .modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(3, 7, 15, 0.85); z-index: 100; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(6px); }
  .modal-card { background: #0c1626; border: 1px solid #274773; border-radius: 8px; width: 560px; max-width: 90%; max-height: 88%; overflow-y: auto; padding: 26px 30px; box-shadow: 0 10px 40px rgba(0,0,0,0.8); color: #d0e4ff; }
  .modal-card h1 { font-size: 24px; color: #00e5ff; margin-bottom: 6px; letter-spacing: 1px; }
  .modal-card h2 { font-size: 16px; color: #64b5f6; margin-top: 14px; margin-bottom: 6px; }
  .modal-card p { font-size: 13px; line-height: 1.5; color: #a5c2e0; margin-bottom: 10px; }
  .modal-card ul { font-size: 13px; line-height: 1.6; margin-left: 20px; color: #a5c2e0; margin-bottom: 12px; }
  .modal-card strong { color: #fff; }
  .modal-btn { background: #0288d1; border: none; color: #fff; font-weight: 700; font-size: 14px; padding: 10px 20px; border-radius: 4px; cursor: pointer; display: inline-block; margin-top: 14px; transition: background 0.15s; }
  .modal-btn:hover { background: #03a9f4; }
  .modal-btn.secondary { background: #1e3352; margin-left: 10px; }
  .modal-btn.secondary:hover { background: #2b4975; }

  /* Tooltip */
  #tooltip { position: absolute; pointer-events: none; background: #0c182a; border: 1px solid #355f94; padding: 6px 10px; border-radius: 4px; font-size: 11px; color: #e1f5fe; z-index: 50; display: none; max-width: 220px; box-shadow: 0 4px 12px rgba(0,0,0,0.6); }

  /* Floating combat numbers */
  .combat-text { position: absolute; pointer-events: none; font-size: 11px; font-weight: 700; color: #ff5252; text-shadow: 0 0 3px #000; animation: floatup 0.9s forwards; z-index: 6; }
  @keyframes floatup { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-24px); } }
</style>
</head>
<body>

<div id="canvas-container"></div>
<div id="select-box"></div>
<div id="tooltip"></div>

<!-- HUD -->
<div id="hud">
  <!-- Top Bar -->
  <div id="top-bar" class="hud-panel">
    <div style="display:flex; align-items:center;">
      <span style="font-weight:900; letter-spacing:1px; color:#00e5ff; margin-right:24px;">FRONTIER WAR</span>
      <div class="res-badge">
        <span class="res-icon res-ore"></span>
        <span>ORE: <span id="res-ore-val" style="color:#fff;">300</span></span>
      </div>
      <div class="res-badge">
        <span class="res-icon res-sup"></span>
        <span>SUPPLY: <span id="res-sup-val" style="color:#fff;">4 / 15</span></span>
      </div>
      <div class="res-badge" style="font-size:11px; color:#8198b5;">
        <span>EXPEDITION CORPS (BLUE)</span>
      </div>
    </div>
    <div>
      <span id="game-timer" style="font-size:12px; color:#8ba7c7; margin-right:14px;">00:00</span>
      <button class="top-btn" id="btn-sound" title="Toggle Sound">AUDIO: ON</button>
      <button class="top-btn" id="btn-help">HELP (F1)</button>
      <button class="top-btn" id="btn-pause">PAUSE (ESC)</button>
    </div>
  </div>

  <!-- Alert message box -->
  <div id="alert-box"></div>

  <!-- Bottom Control Bar -->
  <div id="bottom-bar" class="hud-panel">
    <!-- Minimap -->
    <div id="minimap-wrap">
      <canvas id="minimap" width="160" height="160"></canvas>
      <div id="minimap-overlay"></div>
    </div>

    <!-- Selected Entity Info -->
    <div id="info-panel">
      <div id="info-header">
        <span id="info-title">No Selection</span>
        <span id="info-count"></span>
      </div>
      <div id="info-body">
        Select a unit or building to issue orders. Drag-box selects multiple combat units.
      </div>
      <div id="queue-bar"></div>
    </div>

    <!-- Commands & Production Grid -->
    <div id="action-panel">
      <div id="action-grid"></div>
      <div style="font-size:10px; color:#6888ab; display:flex; justify-content:space-between; margin-top:4px;">
        <span>[A] Attack-Move &nbsp;|&nbsp; [S] Stop &nbsp;|&nbsp; [H] Hold &nbsp;|&nbsp; [1-9] Groups</span>
        <span id="tech-status">Armor Lv 0 | Weap Lv 0</span>
      </div>
    </div>
  </div>
</div>

<!-- Modal Start / Victory / Defeat / Help Screen -->
<div id="modal-screen" class="modal-overlay">
  <div class="modal-card" id="modal-content">
    <h1>FRONTIER WAR</h1>
    <p>Command the United Frontier Expedition against the rival Dominion Syndicate on resource-rich Exoplanet Tartarus VII.</p>
    <h2>OBJECTIVE</h2>
    <p>Harvest crystalline Ore deposits, expand supply infrastructure, build ground and air forces, scout through the fog of war, and <strong>destroy all enemy Headquarters</strong>. You are defeated if you lose all of your Headquarters.</p>
    <h2>COMMANDS & CONTROLS</h2>
    <ul>
      <li><strong>Left-Click / Drag Box:</strong> Select units or structures (Shift to add). Double-click to select all visible units of that type.</li>
      <li><strong>Right-Click:</strong> Contextual order (Move, Attack, Harvest crystalline ore deposit, or Return minerals).</li>
      <li><strong>Camera Controls:</strong> W/A/S/D or Arrow keys to Pan. Mouse Scroll to Zoom. Hold Middle Mouse or Q/E to Rotate camera. Spacebar to center on selected units or base.</li>
      <li><strong>Combat Orders:</strong> <strong>[A]</strong> Attack-Move, <strong>[S]</strong> Stop, <strong>[H]</strong> Hold Position.</li>
      <li><strong>Control Groups:</strong> Ctrl + 1-9 to create group; 1-9 to recall group.</li>
    </ul>
    <h2>ESCALATION & COUNTERS</h2>
    <ul>
      <li><strong>Mining Rover:</strong> Gathers ore. Can build Headquarters, Solar Reactors (supply), Barracks, Foundry, Aerodrome, Defense Turret.</li>
      <li><strong>Scout Raider:</strong> Fast close-combat buggy with light blades. Strong against light ranged units.</li>
      <li><strong>Rifle Marine:</strong> Rapid kinetic rifle. Hits ground and air. Counters light aircraft and buggies.</li>
      <li><strong>Siege Titan:</strong> Heavy armor, twin plasma cannon. Devastates ground armor and structures at long range. Cannot hit air.</li>
      <li><strong>Valkyrie Gunship:</strong> Flying attack craft. Bypasses terrain chokepoints. Strong ground strikes; countered by Marines and Turrets.</li>
      <li><strong>Upgrades:</strong> Research Weapons and Armor upgrades at the Engineering Bay to power up your entire army!</li>
    </ul>
    <div style="margin-top:16px;">
      <button class="modal-btn" id="btn-start-game">START MISSION</button>
    </div>
  </div>
</div>

<script>
/* =========================================================================
   FRONTIER WAR — 3D REAL-TIME STRATEGY ENGINE
   Single-file zero-dependency WebGL RTS
   ========================================================================= */

// Audio Synth helper using Web Audio API
class SoundFx {
  constructor() {
    this.ctx = null;
    this.enabled = true;
  }
  init() {
    if (!this.ctx) {
      const AudioCtx = window.AudioContext || window.webkitAudioContext;
      if (AudioCtx) this.ctx = new AudioCtx();
    }
    if (this.ctx && this.ctx.state === 'suspended') {
      this.ctx.resume();
    }
  }
  playClick() {
    if (!this.enabled || !this.ctx) return;
    try {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(480, this.ctx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.04);
      gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.05);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start();
      osc.stop(this.ctx.currentTime + 0.05);
    } catch(e){}
  }
  playOrder() {
    if (!this.enabled || !this.ctx) return;
    try {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(320, this.ctx.currentTime);
      osc.frequency.setValueAtTime(540, this.ctx.currentTime + 0.04);
      gain.gain.setValueAtTime(0.09, this.ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.09);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start();
      osc.stop(this.ctx.currentTime + 0.09);
    } catch(e){}
  }
  playShoot(type) {
    if (!this.enabled || !this.ctx) return;
    try {
      const now = this.ctx.currentTime;
      if (type === 'laser') {
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(880, now);
        osc.frequency.exponentialRampToValueAtTime(220, now + 0.08);
        gain.gain.setValueAtTime(0.05, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
        osc.connect(gain);
        gain.connect(this.ctx.destination);
        osc.start();
        osc.stop(now + 0.08);
      } else if (type === 'heavy') {
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(140, now);
        osc.frequency.exponentialRampToValueAtTime(40, now + 0.18);
        gain.gain.setValueAtTime(0.12, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
        osc.connect(gain);
        gain.connect(this.ctx.destination);
        osc.start();
        osc.stop(now + 0.2);
      } else {
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'square';
        osc.frequency.setValueAtTime(600, now);
        osc.frequency.exponentialRampToValueAtTime(120, now + 0.05);
        gain.gain.setValueAtTime(0.04, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.06);
        osc.connect(gain);
        gain.connect(this.ctx.destination);
        osc.start();
        osc.stop(now + 0.06);
      }
    } catch(e){}
  }
  playExplode() {
    if (!this.enabled || !this.ctx) return;
    try {
      const now = this.ctx.currentTime;
      // White noise buffer
      const bufferSize = this.ctx.sampleRate * 0.25;
      const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
      const output = buffer.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) {
        output[i] = Math.random() * 2 - 1;
      }
      const whiteNoise = this.ctx.createBufferSource();
      whiteNoise.buffer = buffer;
      const filter = this.ctx.createBiquadFilter();
      filter.type = 'lowpass';
      filter.frequency.setValueAtTime(800, now);
      filter.frequency.exponentialRampToValueAtTime(80, now + 0.25);
      const gain = this.ctx.createGain();
      gain.gain.setValueAtTime(0.16, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
      whiteNoise.connect(filter);
      filter.connect(gain);
      gain.connect(this.ctx.destination);
      whiteNoise.start();
      whiteNoise.stop(now + 0.25);
    } catch(e){}
  }
  playAlert() {
    if (!this.enabled || !this.ctx) return;
    try {
      const now = this.ctx.currentTime;
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(750, now);
      osc.frequency.setValueAtTime(500, now + 0.1);
      osc.frequency.setValueAtTime(750, now + 0.2);
      gain.gain.setValueAtTime(0.12, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
      osc.connect(gain);
      gain.connect(this.ctx.destination);
      osc.start();
      osc.stop(now + 0.3);
    } catch(e){}
  }
}
const sfx = new SoundFx();

/* =========================================================================
   GAME DATA DEFINITIONS & STATS
   ========================================================================= */
const FACTION_PLAYER = 'player';
const FACTION_ENEMY = 'enemy';

// Colors
const COLOR_PLAYER = 0x2979ff;
const COLOR_PLAYER_HEX = '#2979ff';
const COLOR_ENEMY = 0xff1744;
const COLOR_ENEMY_HEX = '#ff1744';
const COLOR_NEUTRAL = 0x90a4ae;
const COLOR_ORE = 0x00e5ff;

// Unit & Structure Catalog
const UNIT_STATS = {
  worker: {
    name: 'Mining Rover',
    hp: 120,
    armor: 1,
    armorType: 'light',
    speed: 6.8,
    range: 2.2,
    damage: 6,
    cooldown: 1.0,
    targets: 'ground',
    damageType: 'kinetic',
    sight: 18,
    cost: 50,
    supply: 1,
    buildTime: 12,
    radius: 1.0,
    desc: 'Harvests ore, returns minerals to Headquarters, and erects base infrastructure.'
  },
  scout: {
    name: 'Scout Raider',
    hp: 170,
    armor: 1,
    armorType: 'light',
    speed: 9.5,
    range: 2.8,
    damage: 15,
    cooldown: 0.8,
    targets: 'ground',
    damageType: 'kinetic',
    sight: 24,
    cost: 75,
    supply: 1,
    buildTime: 14,
    radius: 1.1,
    desc: 'Rapid close-assault reconnaissance vehicle. Effective against light infantry and scouts.'
  },
  marine: {
    name: 'Rifle Marine',
    hp: 140,
    armor: 1,
    armorType: 'light',
    speed: 5.6,
    range: 13,
    damage: 12,
    cooldown: 0.7,
    targets: 'both',
    damageType: 'kinetic',
    sight: 20,
    cost: 60,
    supply: 1,
    buildTime: 14,
    radius: 0.8,
    desc: 'Versatile ranged ground infantry armed with hyper-velocity kinetic rifle. Targets ground & air.'
  },
  tank: {
    name: 'Siege Titan',
    hp: 480,
    armor: 4,
    armorType: 'heavy',
    speed: 4.2,
    range: 16,
    damage: 48,
    cooldown: 2.0,
    targets: 'ground',
    damageType: 'explosive',
    sight: 22,
    cost: 180,
    supply: 3,
    buildTime: 28,
    radius: 1.6,
    desc: 'Heavy tracked armor with twin plasma cannons. Heavy damage vs armor and structures; ground only.'
  },
  flyer: {
    name: 'Valkyrie Gunship',
    hp: 260,
    armor: 2,
    armorType: 'light',
    speed: 7.2,
    range: 12,
    damage: 24,
    cooldown: 1.2,
    targets: 'both',
    damageType: 'energy',
    sight: 22,
    cost: 160,
    supply: 2,
    buildTime: 24,
    radius: 1.3,
    isFlying: true,
    flightHeight: 5.5,
    desc: 'VTOL strike craft that flies over terrain. Armed with plasma beam batteries. Hits ground & air.'
  }
};

const BUILDING_STATS = {
  hq: {
    name: 'Headquarters',
    hp: 2200,
    armor: 5,
    armorType: 'heavy',
    sight: 28,
    cost: 350,
    supplyGranted: 15,
    buildTime: 40,
    size: 5.5,
    isHQ: true,
    isDropoff: true,
    canTrain: ['worker'],
    desc: 'Primary colony command center. Mineral dropoff point; provides 15 supply. Trains Mining Rovers.'
  },
  reactor: {
    name: 'Solar Reactor',
    hp: 750,
    armor: 2,
    armorType: 'light',
    sight: 14,
    cost: 80,
    supplyGranted: 12,
    buildTime: 18,
    size: 3.2,
    desc: 'Power and logistics generator. Increases available army supply capacity by +12.'
  },
  barracks: {
    name: 'Barracks',
    hp: 1100,
    armor: 3,
    armorType: 'heavy',
    sight: 18,
    cost: 140,
    supplyGranted: 0,
    buildTime: 24,
    size: 4.2,
    canTrain: ['marine', 'scout'],
    desc: 'Infantry and light vehicle assembly. Trains Rifle Marines and Scout Raiders.'
  },
  foundry: {
    name: 'Foundry',
    hp: 1350,
    armor: 4,
    armorType: 'heavy',
    sight: 18,
    cost: 190,
    supplyGranted: 0,
    buildTime: 32,
    size: 4.6,
    canTrain: ['tank'],
    desc: 'Heavy armored vehicle factory. Manufactures twin-cannon Siege Titans.'
  },
  aerodrome: {
    name: 'Aerodrome',
    hp: 1200,
    armor: 3,
    armorType: 'heavy',
    sight: 18,
    cost: 210,
    supplyGranted: 0,
    buildTime: 34,
    size: 4.6,
    canTrain: ['flyer'],
    desc: 'Aeronautics hangar and launching pad. Produces Valkyrie Gunships.'
  },
  turret: {
    name: 'Defense Turret',
    hp: 850,
    armor: 4,
    armorType: 'heavy',
    sight: 22,
    range: 15,
    damage: 26,
    cooldown: 0.9,
    targets: 'both',
    damageType: 'energy',
    cost: 110,
    supplyGranted: 0,
    buildTime: 22,
    size: 2.8,
    isDefensive: true,
    desc: 'Automated twin-laser defensive bastion. Engages both ground and aerial hostiles.'
  },
  techlab: {
    name: 'Research Bay',
    hp: 950,
    armor: 3,
    armorType: 'heavy',
    sight: 16,
    cost: 150,
    supplyGranted: 0,
    buildTime: 26,
    size: 3.8,
    canResearch: ['upgrade_weapons', 'upgrade_armor'],
    desc: 'Upgrades combat engineering: improves weapon damage and armor plating across all units.'
  }
};

const UPGRADE_STATS = {
  upgrade_weapons: {
    name: 'Weapons Cal. II',
    cost: 130,
    time: 25,
    desc: 'Increases all combat unit attack damage by +20%.'
  },
  upgrade_armor: {
    name: 'Reinforced Plating II',
    cost: 130,
    time: 25,
    desc: 'Increases all units and structures armor plating by +2.'
  }
};

// Combat Damage Multipliers: [damageType][armorType]
const DAMAGE_MULTIPLIER = {
  kinetic: {
    light: 1.15,
    heavy: 0.70
  },
  explosive: {
    light: 0.75,
    heavy: 1.35
  },
  energy: {
    light: 1.0,
    heavy: 1.05
  }
};

/* =========================================================================
   MAP & TERRAIN SPECIFICATION
   ========================================================================= */
const MAP_SIZE = 260; // -130 to +130
const FOG_RES = 130;  // 2x2 meter fog cells
const CELL_SIZE = MAP_SIZE / FOG_RES;

/* =========================================================================
   GLOBAL GAME STATE
   ========================================================================= */
const G = {
  time: 0,
  paused: false,
  gameOver: false,
  victory: false,

  // Player Economy
  player: {
    ore: 320,
    usedSupply: 0,
    maxSupply: 15,
    weaponLevel: 0,
    armorLevel: 0,
    researched: {}
  },

  // AI Economy
  enemy: {
    ore: 320,
    usedSupply: 0,
    maxSupply: 15,
    weaponLevel: 0,
    armorLevel: 0,
    researched: {},
    strategyTimer: 0,
    lastAttackTime: 0,
    attackWave: 1,
    rallyPoint: new THREE.Vector3(80, 0, 80),
    knownPlayerBase: null
  },

  // Entities
  units: [],
  buildings: [],
  resources: [],
  projectiles: [],
  particles: [],

  // Selection
  selectedUnits: [],
  selectedBuilding: null,
  controlGroups: {},

  // Fog of war grid: 0 = unseen, 1 = explored (shrouded), 2 = currently visible
  fogGrid: new Uint8Array(FOG_RES * FOG_RES),
  enemyFogGrid: new Uint8Array(FOG_RES * FOG_RES),

  // Placer state
  placingType: null,
  placementMesh: null,
  activeCommand: null, // 'attack-move', etc.

  // Terrain height & obstacles
  obstacles: []
};

/* =========================================================================
   THREE.JS SETUP & RENDER PIPELINE
   ========================================================================= */
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a101d);
scene.fog = new THREE.FogExp2(0x0a101d, 0.0035);

const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 600);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);

// Lighting
const ambientLight = new THREE.AmbientLight(0x7090b8, 0.7);
scene.add(ambientLight);

const sunLight = new THREE.DirectionalLight(0xfff5e6, 1.25);
sunLight.position.set(-80, 140, -60);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 380;
const d = 140;
sunLight.shadow.camera.left = -d;
sunLight.shadow.camera.right = d;
sunLight.shadow.camera.top = d;
sunLight.shadow.camera.bottom = -d;
sunLight.shadow.bias = -0.0005;
scene.add(sunLight);

const hemiLight = new THREE.HemisphereLight(0x3a5f8a, 0x1b281f, 0.45);
scene.add(hemiLight);

/* Camera Rig / Controller */
class CameraController {
  constructor() {
    this.target = new THREE.Vector3(-80, 0, -80); // Start at player base
    this.distance = 64;
    this.pitch = 0.95; // Radians down from horizon (~54 deg)
    this.yaw = THREE.MathUtils.degToRad(-45); // angled view
    this.moveSpeed = 65;
    this.keys = {};
    this.isDragging = false;
    this.lastMouse = { x: 0, y: 0 };
    this.mouseButton = -1;

    this.updatePosition();
  }

  updatePosition() {
    // Keep target on map
    const limit = MAP_SIZE / 2 - 10;
    this.target.x = THREE.MathUtils.clamp(this.target.x, -limit, limit);
    this.target.z = THREE.MathUtils.clamp(this.target.z, -limit, limit);

    const x = this.target.x + this.distance * Math.cos(this.pitch) * Math.sin(this.yaw);
    const z = this.target.z + this.distance * Math.cos(this.pitch) * Math.cos(this.yaw);
    const y = this.distance * Math.sin(this.pitch);

    camera.position.set(x, y, z);
    camera.lookAt(this.target);

    // Keep sun shadow frustum centered around camera target for crisp shadows
    sunLight.position.set(this.target.x - 70, 140, this.target.z - 50);
    sunLight.target.position.copy(this.target);
    sunLight.target.updateMatrixWorld();
  }

  update(dt) {
    let dx = 0;
    let dz = 0;

    // Movement forward/back along ground projection of camera
    const forward = new THREE.Vector3(-Math.sin(this.yaw), 0, -Math.cos(this.yaw)).normalize();
    const right = new THREE.Vector3(Math.cos(this.yaw), 0, -Math.sin(this.yaw)).normalize();

    if (this.keys['KeyW'] || this.keys['ArrowUp']) {
      dx += forward.x;
      dz += forward.z;
    }
    if (this.keys['KeyS'] || this.keys['ArrowDown']) {
      dx -= forward.x;
      dz -= forward.z;
    }
    if (this.keys['KeyA'] || this.keys['ArrowLeft']) {
      dx -= right.x;
      dz -= right.z;
    }
    if (this.keys['KeyD'] || this.keys['ArrowRight']) {
      dx += right.x;
      dz += right.z;
    }

    if (dx !== 0 || dz !== 0) {
      const len = Math.hypot(dx, dz);
      this.target.x += (dx / len) * this.moveSpeed * dt;
      this.target.z += (dz / len) * this.moveSpeed * dt;
    }

    if (this.keys['KeyQ']) this.yaw += 1.8 * dt;
    if (this.keys['KeyE']) this.yaw -= 1.8 * dt;

    this.updatePosition();
  }

  zoom(delta) {
    this.distance = THREE.MathUtils.clamp(this.distance + delta * 0.05, 25, 120);
    this.updatePosition();
  }
}
const camCtrl = new CameraController();

/* =========================================================================
   TERRAIN CREATION & PROCEDURAL GENERATION
   ========================================================================= */
// Simple smooth simplex/perlin approximation for height
function getTerrainHeight(x, z) {
  // Flat playfield base with slight undulating hills on periphery and specific cliffs
  const distFromCenter = Math.hypot(x, z);
  let h = Math.sin(x * 0.03) * Math.cos(z * 0.03) * 1.5;

  // Ensure bases at (-80, -80) and (80, 80) and center expansions are relatively level
  const distBase1 = Math.hypot(x - (-80), z - (-80));
  const distBase2 = Math.hypot(x - 80, z - 80);
  const distExp1 = Math.hypot(x - (-60), z - 60);
  const distExp2 = Math.hypot(x - 60, z - (-60));
  const distCenter = Math.hypot(x, z);

  const flatFactor = Math.min(distBase1 / 35, distBase2 / 35, distExp1 / 25, distExp2 / 25, distCenter / 25);
  h *= Math.min(1.0, flatFactor);
  return h;
}

// Generate Terrain Mesh
const terrainGeo = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 128, 128);
terrainGeo.rotateX(-Math.PI / 2);
const posAttr = terrainGeo.attributes.position;
const colorAttr = new Float32Array(posAttr.count * 3);

for (let i = 0; i < posAttr.count; i++) {
  const vx = posAttr.getX(i);
  const vz = posAttr.getZ(i);
  const vy = getTerrainHeight(vx, vz);
  posAttr.setY(i, vy);

  // Surface vertex tinting (martian / exoplanet red-tan soil with dark basalt and crater rings)
  const n = (Math.sin(vx * 0.1) * Math.cos(vz * 0.1) + 1) * 0.5;
  const r = 0.22 + n * 0.12;
  const g = 0.18 + n * 0.08;
  const b = 0.18 + n * 0.06;

  colorAttr[i * 3] = r;
  colorAttr[i * 3 + 1] = g;
  colorAttr[i * 3 + 2] = b;
}
terrainGeo.setAttribute('color', new THREE.BufferAttribute(colorAttr, 3));
terrainGeo.computeVertexNormals();

const terrainMat = new THREE.MeshStandardMaterial({
  vertexColors: true,
  roughness: 0.9,
  metalness: 0.1
});
const terrainMesh = new THREE.Mesh(terrainGeo, terrainMat);
terrainMesh.receiveShadow = true;
scene.add(terrainMesh);

// Boundary Mountain Walls & Natural Chokepoints
function createMountainRidge(x, z, sx, sy, sz, rot) {
  const mesh = new THREE.Mesh(
    new THREE.DodecahedronGeometry(1, 1),
    new THREE.MeshStandardMaterial({ color: 0x1e2730, roughness: 0.95, flatShading: true })
  );
  mesh.scale.set(sx, sy, sz);
  mesh.rotation.set(0.1, rot, 0.2);
  mesh.position.set(x, sy * 0.4 + getTerrainHeight(x, z), z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  scene.add(mesh);

  // Add bounding circle obstacle for ground pathfinding
  G.obstacles.push({
    x: x,
    z: z,
    radius: Math.max(sx, sz) * 0.8
  });
}

// Map Boundary Walls
const half = MAP_SIZE / 2;
for (let p = -half; p <= half; p += 18) {
  createMountainRidge(p, -half, 16, 18, 16, Math.random() * 3);
  createMountainRidge(p, half, 16, 18, 16, Math.random() * 3);
  createMountainRidge(-half, p, 16, 18, 16, Math.random() * 3);
  createMountainRidge(half, p, 16, 18, 16, Math.random() * 3);
}

// Chokepoints and Ridge Barriers separating paths
// Central ridge dividing top-left / bottom-right with 3 open corridors
createMountainRidge(-20, -15, 14, 15, 14, 0.5);
createMountainRidge(-15, -35, 12, 14, 12, 0.8);
createMountainRidge(-10, -55, 14, 16, 14, 0.2);

createMountainRidge(20, 15, 14, 15, 14, 0.5);
createMountainRidge(15, 35, 12, 14, 12, 0.8);
createMountainRidge(10, 55, 14, 16, 14, 0.2);

// Center plateau rock formations
createMountainRidge(-35, 15, 11, 12, 11, 1.2);
createMountainRidge(35, -15, 11, 12, 11, 1.2);

/* =========================================================================
   FOG OF WAR SYSTEM
   ========================================================================= */
// We project a dynamic 2D canvas texture over the terrain via a custom transparent fog plane
const fogCanvas = document.createElement('canvas');
fogCanvas.width = FOG_RES;
fogCanvas.height = FOG_RES;
const fogCtx = fogCanvas.getContext('2d');

const fogTexture = new THREE.CanvasTexture(fogCanvas);
fogTexture.generateMipmaps = false;
fogTexture.minFilter = THREE.LinearFilter;
fogTexture.magFilter = THREE.LinearFilter;

// Fog Overlay Plane directly above terrain with matched height vertices
const fogPlaneGeo = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 64, 64);
fogPlaneGeo.rotateX(-Math.PI / 2);
const fogPos = fogPlaneGeo.attributes.position;
for (let i = 0; i < fogPos.count; i++) {
  const fx = fogPos.getX(i);
  const fz = fogPos.getZ(i);
  fogPos.setY(i, getTerrainHeight(fx, fz) + 0.35);
}
fogPlaneGeo.computeVertexNormals();

const fogMat = new THREE.MeshBasicMaterial({
  color: 0x050a14,
  alphaMap: fogTexture,
  transparent: true,
  depthWrite: false
});
const fogPlaneMesh = new THREE.Mesh(fogPlaneGeo, fogMat);
scene.add(fogPlaneMesh);

function worldToFogGrid(x, z) {
  const gx = Math.floor(((x + MAP_SIZE / 2) / MAP_SIZE) * FOG_RES);
  const gz = Math.floor(((z + MAP_SIZE / 2) / MAP_SIZE) * FOG_RES);
  return { gx: THREE.MathUtils.clamp(gx, 0, FOG_RES - 1), gz: THREE.MathUtils.clamp(gz, 0, FOG_RES - 1) };
}

function updateFogOfWar() {
  // Step 1: degrade currently visible (2) to explored (1)
  for (let i = 0; i < G.fogGrid.length; i++) {
    if (G.fogGrid[i] === 2) {
      G.fogGrid[i] = 1;
    }
  }

  // Step 2: mark vision circles around player units & buildings
  const revealCircle = (wx, wz, sightRadius, grid) => {
    const { gx: cx, gz: cz } = worldToFogGrid(wx, wz);
    const cellRadius = Math.ceil((sightRadius / MAP_SIZE) * FOG_RES);
    const r2 = cellRadius * cellRadius;

    const minX = Math.max(0, cx - cellRadius);
    const maxX = Math.min(FOG_RES - 1, cx + cellRadius);
    const minZ = Math.max(0, cz - cellRadius);
    const maxZ = Math.min(FOG_RES - 1, cz + cellRadius);

    for (let gz = minZ; gz <= maxZ; gz++) {
      const dz = gz - cz;
      const rowOffset = gz * FOG_RES;
      for (let gx = minX; gx <= maxX; gx++) {
        const dx = gx - cx;
        if (dx * dx + dz * dz <= r2) {
          grid[rowOffset + gx] = 2; // fully visible
        }
      }
    }
  };

  for (const u of G.units) {
    if (u.faction === FACTION_PLAYER) {
      revealCircle(u.x, u.z, u.sight, G.fogGrid);
    } else {
      revealCircle(u.x, u.z, u.sight, G.enemyFogGrid);
    }
  }

  for (const b of G.buildings) {
    if (b.faction === FACTION_PLAYER) {
      revealCircle(b.x, b.z, b.sight, G.fogGrid);
    } else {
      revealCircle(b.x, b.z, b.sight, G.enemyFogGrid);
    }
  }

  // Step 3: Draw fog alpha texture
  // 0 (unexplored) => full opacity (255)
  // 1 (explored)   => semi-transparent shroud (150)
  // 2 (visible)    => clear (0)
  const imgData = fogCtx.createImageData(FOG_RES, FOG_RES);
  const data = imgData.data;

  for (let i = 0; i < G.fogGrid.length; i++) {
    const val = G.fogGrid[i];
    const p = i * 4;
    data[p] = 255;
    data[p + 1] = 255;
    data[p + 2] = 255;
    if (val === 2) {
      data[p + 3] = 0; // completely visible
    } else if (val === 1) {
      data[p + 3] = 135; // explored / shroud (softer contrast)
    } else {
      data[p + 3] = 230; // unexplored fog
    }
  }

  fogCtx.putImageData(imgData, 0, 0);
  fogTexture.needsUpdate = true;

  // Step 4: Update 3D visibility of enemy objects based on player's fog
  for (const u of G.units) {
    if (u.faction === FACTION_ENEMY) {
      const { gx, gz } = worldToFogGrid(u.x, u.z);
      const isVisible = G.fogGrid[gz * FOG_RES + gx] === 2;
      u.mesh.visible = isVisible;
      if (u.hpBar) u.hpBar.visible = isVisible;
    } else {
      u.mesh.visible = true;
    }
  }

  for (const b of G.buildings) {
    if (b.faction === FACTION_ENEMY) {
      const { gx, gz } = worldToFogGrid(b.x, b.z);
      const state = G.fogGrid[gz * FOG_RES + gx];
      if (state === 2) {
        b.mesh.visible = true;
        b.lastSeen = true;
        if (b.hpBar) b.hpBar.visible = true;
      } else if (state === 1 && b.lastSeen) {
        // Explored ghost silhouette
        b.mesh.visible = true;
        if (b.hpBar) b.hpBar.visible = false;
      } else {
        b.mesh.visible = false;
        if (b.hpBar) b.hpBar.visible = false;
      }
    } else {
      b.mesh.visible = true;
    }
  }
}

/* =========================================================================
   CRYSTAL RESOURCE FIELDS
   ========================================================================= */
class CrystalDeposit {
  constructor(x, z, amount = 1600) {
    this.x = x;
    this.z = z;
    this.amount = amount;
    this.maxAmount = amount;
    this.mesh = this.createMesh();
    scene.add(this.mesh);
  }

  createMesh() {
    const group = new THREE.Group();
    group.position.set(this.x, getTerrainHeight(this.x, this.z), this.z);

    // Cluster of teal crystal shards
    const mat = new THREE.MeshStandardMaterial({
      color: COLOR_ORE,
      emissive: 0x004d60,
      roughness: 0.2,
      metalness: 0.8,
      flatShading: true
    });

    const numShards = 7;
    for (let i = 0; i < numShards; i++) {
      const h = 2.2 + Math.random() * 2.2;
      const shardGeo = new THREE.ConeGeometry(0.55 + Math.random() * 0.3, h, 5);
      const shard = new THREE.Mesh(shardGeo, mat);
      const angle = (i / numShards) * Math.PI * 2 + Math.random() * 0.4;
      const dist = 0.5 + Math.random() * 1.4;
      shard.position.set(Math.cos(angle) * dist, h * 0.45, Math.sin(angle) * dist);
      shard.rotation.set((Math.random() - 0.5) * 0.4, Math.random() * 3, (Math.random() - 0.5) * 0.4);
      shard.castShadow = true;
      group.add(shard);
    }

    return group;
  }

  harvest(amt) {
    const taken = Math.min(amt, this.amount);
    this.amount -= taken;
    // Scale down visual cluster as it depletes
    const scale = THREE.MathUtils.clamp(0.25 + (this.amount / this.maxAmount) * 0.75, 0.2, 1.0);
    this.mesh.scale.set(scale, scale, scale);

    if (this.amount <= 0) {
      this.destroy();
    }
    return taken;
  }

  destroy() {
    scene.remove(this.mesh);
    const idx = G.resources.indexOf(this);
    if (idx !== -1) G.resources.splice(idx, 1);
  }
}

function spawnResourceCluster(centerX, centerZ, clusterSize = 5) {
  for (let i = 0; i < clusterSize; i++) {
    const angle = (i / clusterSize) * Math.PI * 2 + Math.random() * 0.5;
    const r = 3.5 + Math.random() * 3.5;
    const rx = centerX + Math.cos(angle) * r;
    const rz = centerZ + Math.sin(angle) * r;
    G.resources.push(new CrystalDeposit(rx, rz, 1400 + Math.floor(Math.random() * 600)));
  }
}

// Distribute Resource Fields Across Map
// Starting Player Base deposits
spawnResourceCluster(-92, -68, 5);
// Starting Enemy Base deposits
spawnResourceCluster(92, 68, 5);

// Natural Expansions (North-West, South-East, and Central Contested)
spawnResourceCluster(-60, 60, 5);
spawnResourceCluster(60, -60, 5);
spawnResourceCluster(0, 0, 7); // Rich center site
spawnResourceCluster(-15, 65, 4);
spawnResourceCluster(15, -65, 4);

/* =========================================================================
   3D UNIT & BUILDING MESH BUILDERS
   ========================================================================= */
function createUnitMesh(type, faction) {
  const group = new THREE.Group();
  const factionColor = faction === FACTION_PLAYER ? COLOR_PLAYER : COLOR_ENEMY;
  const hullMat = new THREE.MeshStandardMaterial({ color: 0x273546, roughness: 0.5, metalness: 0.6 });
  const trimMat = new THREE.MeshStandardMaterial({ color: factionColor, emissive: factionColor, emissiveIntensity: 0.35, roughness: 0.4 });
  const glowMat = new THREE.MeshBasicMaterial({ color: faction === FACTION_PLAYER ? 0x00e5ff : 0xff3d00 });

  if (type === 'worker') {
    // 6-wheeled mining rover with hydraulic drill and cargo container
    const body = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.8, 2.2), hullMat);
    body.position.y = 0.7;
    body.castShadow = true;
    group.add(body);

    const cab = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.6, 1.0), trimMat);
    cab.position.set(0, 1.2, 0.3);
    cab.castShadow = true;
    group.add(cab);

    // Front arm
    const arm = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.15, 1.2), hullMat);
    arm.rotation.x = Math.PI / 3;
    arm.position.set(0, 0.8, -1.2);
    group.add(arm);

    // Crystal cargo indicator
    const cargo = new THREE.Mesh(new THREE.OctahedronGeometry(0.4), glowMat);
    cargo.name = 'cargoMesh';
    cargo.position.set(0, 1.3, -0.6);
    cargo.visible = false;
    group.add(cargo);

  } else if (type === 'scout') {
    // Scout Raider: Low-slung attack buggy
    const chassis = new THREE.Mesh(new THREE.ConeGeometry(1.0, 2.4, 4), trimMat);
    chassis.rotation.x = -Math.PI / 2;
    chassis.position.y = 0.6;
    chassis.castShadow = true;
    group.add(chassis);

    const wheels = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 2.2, 8), hullMat);
    wheels.rotation.z = Math.PI / 2;
    wheels.position.set(0, 0.35, -0.2);
    group.add(wheels);

    const gun = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 1.0), hullMat);
    gun.position.set(0, 1.0, -0.4);
    group.add(gun);

  } else if (type === 'marine') {
    // Rifle Marine: Powered infantry armor
    const torso = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.0, 0.6), trimMat);
    torso.position.y = 1.1;
    torso.castShadow = true;
    group.add(torso);

    const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.35, 8, 8), hullMat);
    helmet.position.set(0, 1.8, 0);
    group.add(helmet);

    const visor = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.15, 0.2), glowMat);
    visor.position.set(0, 1.8, -0.25);
    group.add(visor);

    const rifle = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1.2), hullMat);
    rifle.rotation.x = Math.PI / 2;
    rifle.position.set(0.4, 1.0, -0.5);
    group.add(rifle);

  } else if (type === 'tank') {
    // Siege Titan: Massive dual-tread battle tank with double plasma cannon turret
    const hull = new THREE.Mesh(new THREE.BoxGeometry(2.8, 1.0, 3.8), hullMat);
    hull.position.y = 0.65;
    hull.castShadow = true;
    group.add(hull);

    // Tread guards
    const treadsL = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.8, 4.0), trimMat);
    treadsL.position.set(-1.4, 0.5, 0);
    treadsL.castShadow = true;
    group.add(treadsL);
    const treadsR = treadsL.clone();
    treadsR.position.x = 1.4;
    group.add(treadsR);

    // Turret
    const turret = new THREE.Mesh(new THREE.CylinderGeometry(1.0, 1.2, 0.7, 8), trimMat);
    turret.name = 'turret';
    turret.position.set(0, 1.4, -0.2);
    turret.castShadow = true;
    group.add(turret);

    // Twin Cannons
    const b1 = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 2.8, 8), hullMat);
    b1.rotation.x = Math.PI / 2;
    b1.position.set(-0.35, 0.1, -1.4);
    turret.add(b1);
    const b2 = b1.clone();
    b2.position.x = 0.35;
    turret.add(b2);

  } else if (type === 'flyer') {
    // Valkyrie Gunship: Swept-wing stealth gunship
    const fuselage = new THREE.Mesh(new THREE.ConeGeometry(1.1, 3.2, 5), trimMat);
    fuselage.rotation.x = -Math.PI / 2;
    fuselage.castShadow = true;
    group.add(fuselage);

    const wings = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.12, 1.4), hullMat);
    wings.position.set(0, 0.1, 0.2);
    wings.castShadow = true;
    group.add(wings);

    const engines = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.3, 1.6, 8), glowMat);
    engines.rotation.x = Math.PI / 2;
    engines.position.set(0, 0.2, 1.4);
    group.add(engines);
  }

  // Selection Decal Ring (underneath)
  const ringGeo = new THREE.RingGeometry(0.9, 1.15, 24);
  ringGeo.rotateX(-Math.PI / 2);
  const ringMat = new THREE.MeshBasicMaterial({ color: 0x00e5ff, side: THREE.DoubleSide });
  const selectRing = new THREE.Mesh(ringGeo, ringMat);
  selectRing.name = 'selectRing';
  selectRing.position.y = 0.08;
  selectRing.visible = false;
  group.add(selectRing);

  return group;
}

function createBuildingMesh(type, faction) {
  const group = new THREE.Group();
  const factionColor = faction === FACTION_PLAYER ? COLOR_PLAYER : COLOR_ENEMY;
  const hullMat = new THREE.MeshStandardMaterial({ color: 0x1b2737, roughness: 0.6, metalness: 0.5 });
  const trimMat = new THREE.MeshStandardMaterial({ color: factionColor, emissive: factionColor, emissiveIntensity: 0.25, roughness: 0.4 });
  const glowMat = new THREE.MeshBasicMaterial({ color: faction === FACTION_PLAYER ? 0x00e5ff : 0xff2a40 });

  if (type === 'hq') {
    // Large command dome with communications dish and landing pad
    const base = new THREE.Mesh(new THREE.CylinderGeometry(4.5, 5.0, 1.4, 8), hullMat);
    base.position.y = 0.7;
    base.castShadow = true;
    group.add(base);

    const dome = new THREE.Mesh(new THREE.SphereGeometry(3.0, 12, 10, 0, Math.PI * 2, 0, Math.PI / 2), trimMat);
    dome.position.y = 1.4;
    dome.castShadow = true;
    group.add(dome);

    const spire = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.4, 4.0), glowMat);
    spire.position.set(0, 4.8, 0);
    group.add(spire);

  } else if (type === 'reactor') {
    // Futuristic fusion core with rotating glowing ring
    const base = new THREE.Mesh(new THREE.CylinderGeometry(2.4, 2.7, 1.0, 6), hullMat);
    base.position.y = 0.5;
    base.castShadow = true;
    group.add(base);

    const pillar = new THREE.Mesh(new THREE.CylinderGeometry(1.2, 1.2, 3.2, 8), trimMat);
    pillar.position.y = 2.0;
    pillar.castShadow = true;
    group.add(pillar);

    const orb = new THREE.Mesh(new THREE.SphereGeometry(1.1, 12, 12), glowMat);
    orb.position.y = 4.0;
    group.add(orb);

  } else if (type === 'barracks') {
    // Military barracks with troop egress bay
    const b = new THREE.Mesh(new THREE.BoxGeometry(4.6, 2.2, 3.8), hullMat);
    b.position.y = 1.1;
    b.castShadow = true;
    group.add(b);

    const roof = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.6, 3.4), trimMat);
    roof.position.y = 2.4;
    roof.castShadow = true;
    group.add(roof);

    const door = new THREE.Mesh(new THREE.PlaneGeometry(1.8, 1.4), glowMat);
    door.position.set(0, 0.7, 1.92);
    group.add(door);

  } else if (type === 'foundry') {
    // Heavy vehicle foundry with exhaust chimneys
    const main = new THREE.Mesh(new THREE.BoxGeometry(5.2, 2.6, 4.8), hullMat);
    main.position.y = 1.3;
    main.castShadow = true;
    group.add(main);

    const stripe = new THREE.Mesh(new THREE.BoxGeometry(5.4, 0.5, 5.0), trimMat);
    stripe.position.y = 1.5;
    group.add(stripe);

    const stack1 = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.5, 2.2, 8), hullMat);
    stack1.position.set(-1.6, 3.4, -1.2);
    group.add(stack1);
    const stack2 = stack1.clone();
    stack2.position.x = 1.6;
    group.add(stack2);

  } else if (type === 'aerodrome') {
    // Runway pad with control tower and hangar
    const pad = new THREE.Mesh(new THREE.BoxGeometry(5.2, 0.4, 5.2), hullMat);
    pad.position.y = 0.2;
    pad.castShadow = true;
    group.add(pad);

    const runway = new THREE.Mesh(new THREE.PlaneGeometry(2.0, 4.8), trimMat);
    runway.rotation.x = -Math.PI / 2;
    runway.position.y = 0.42;
    group.add(runway);

    const tower = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.9, 3.8, 6), hullMat);
    tower.position.set(-1.8, 1.9, -1.8);
    tower.castShadow = true;
    group.add(tower);

    const beacon = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), glowMat);
    beacon.position.set(-1.8, 4.0, -1.8);
    group.add(beacon);

  } else if (type === 'turret') {
    // Twin-barrel defensive turret
    const base = new THREE.Mesh(new THREE.CylinderGeometry(1.6, 2.0, 1.4, 8), hullMat);
    base.position.y = 0.7;
    base.castShadow = true;
    group.add(base);

    const swivel = new THREE.Mesh(new THREE.SphereGeometry(1.1, 10, 10), trimMat);
    swivel.name = 'swivel';
    swivel.position.y = 1.8;
    swivel.castShadow = true;
    group.add(swivel);

    const b1 = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.14, 2.0, 8), hullMat);
    b1.rotation.x = Math.PI / 2;
    b1.position.set(-0.35, 0.1, -1.0);
    swivel.add(b1);
    const b2 = b1.clone();
    b2.position.x = 0.35;
    swivel.add(b2);

  } else if (type === 'techlab') {
    // Spherical research laboratory with orbiting arrays
    const base = new THREE.Mesh(new THREE.CylinderGeometry(2.4, 2.8, 1.2, 6), hullMat);
    base.position.y = 0.6;
    base.castShadow = true;
    group.add(base);

    const dome = new THREE.Mesh(new THREE.SphereGeometry(1.8, 12, 12), trimMat);
    dome.position.y = 2.2;
    dome.castShadow = true;
    group.add(dome);

    const ring = new THREE.Mesh(new THREE.TorusGeometry(2.6, 0.15, 8, 24), glowMat);
    ring.rotation.x = Math.PI / 3;
    ring.position.y = 2.2;
    group.add(ring);
  }

  // Selection footprint
  const ringGeo = new THREE.RingGeometry(BUILDING_STATS[type].size * 0.45, BUILDING_STATS[type].size * 0.55, 32);
  ringGeo.rotateX(-Math.PI / 2);
  const ringMat = new THREE.MeshBasicMaterial({ color: 0x00e5ff, side: THREE.DoubleSide });
  const selectRing = new THREE.Mesh(ringGeo, ringMat);
  selectRing.name = 'selectRing';
  selectRing.position.y = 0.1;
  selectRing.visible = false;
  group.add(selectRing);

  return group;
}

/* =========================================================================
   HP BARS & OVERHEAD HUD
   ========================================================================= */
function createHpBarMesh(width = 1.8, height = 0.22) {
  const group = new THREE.Group();
  const bg = new THREE.Mesh(new THREE.PlaneGeometry(width, height), new THREE.MeshBasicMaterial({ color: 0x0a101b, side: THREE.DoubleSide }));
  group.add(bg);

  const fill = new THREE.Mesh(new THREE.PlaneGeometry(width, height), new THREE.MeshBasicMaterial({ color: 0x00e676, side: THREE.DoubleSide }));
  fill.position.z = 0.01;
  group.add(fill);
  group.fillMesh = fill;
  group.barWidth = width;

  group.updateHp = function(cur, max, shield = 0) {
    const pct = THREE.MathUtils.clamp(cur / max, 0, 1);
    fill.scale.x = pct;
    fill.position.x = -(width * (1 - pct)) / 2;
    if (pct < 0.3) fill.material.color.setHex(0xff3d00);
    else if (pct < 0.6) fill.material.color.setHex(0xffea00);
    else fill.material.color.setHex(0x00e676);
  };

  return group;
}

/* =========================================================================
   UNIT CLASS
   ========================================================================= */
let nextEntityId = 1;

class Unit {
  constructor(type, faction, x, z) {
    this.id = nextEntityId++;
    this.type = type;
    this.faction = faction;
    this.stats = UNIT_STATS[type];
    this.radius = this.stats.radius;
    this.speed = this.stats.speed;
    this.maxHp = this.stats.hp;
    this.hp = this.maxHp;
    this.armor = this.stats.armor + (faction === FACTION_PLAYER ? G.player.armorLevel * 2 : G.enemy.armorLevel * 2);
    this.sight = this.stats.sight;
    this.range = this.stats.range;
    this.damage = this.stats.damage;
    this.cooldown = this.stats.cooldown;
    this.attackTimer = 0;
    this.isFlying = !!this.stats.isFlying;

    this.x = x;
    this.z = z;
    this.y = this.isFlying ? this.stats.flightHeight : getTerrainHeight(x, z);
    this.targetPos = null;
    this.order = 'idle'; // 'idle', 'move', 'attack-move', 'attack', 'gather', 'return-ore', 'hold'
    this.targetEntity = null;
    this.harvestTarget = null;
    this.carriedOre = 0;
    this.harvestTimer = 0;

    this.mesh = createUnitMesh(type, faction);
    this.mesh.position.set(this.x, this.y, this.z);
    scene.add(this.mesh);

    this.hpBar = createHpBarMesh(type === 'tank' ? 2.4 : 1.4, 0.18);
    this.hpBar.position.set(this.x, this.y + (this.isFlying ? 2.0 : 1.8), this.z);
    scene.add(this.hpBar);
  }

  setSelected(val) {
    const ring = this.mesh.getObjectByName('selectRing');
    if (ring) ring.visible = val;
  }

  issueMove(tx, tz) {
    this.order = 'move';
    this.targetPos = new THREE.Vector3(tx, 0, tz);
    this.targetEntity = null;
    this.harvestTarget = null;
  }

  issueAttackMove(tx, tz) {
    this.order = 'attack-move';
    this.targetPos = new THREE.Vector3(tx, 0, tz);
    this.targetEntity = null;
    this.harvestTarget = null;
  }

  issueAttack(target) {
    this.order = 'attack';
    this.targetEntity = target;
    this.targetPos = null;
    this.harvestTarget = null;
  }

  issueGather(crystal) {
    this.order = 'gather';
    this.harvestTarget = crystal;
    this.targetEntity = null;
    this.targetPos = new THREE.Vector3(crystal.x, 0, crystal.z);
  }

  issueStop() {
    this.order = 'idle';
    this.targetPos = null;
    this.targetEntity = null;
    this.harvestTarget = null;
  }

  issueHold() {
    this.order = 'hold';
    this.targetPos = null;
    this.targetEntity = null;
  }

  takeDamage(rawDamage, damageType) {
    const mult = (DAMAGE_MULTIPLIER[damageType] && DAMAGE_MULTIPLIER[damageType][this.stats.armorType]) || 1.0;
    const effectiveArmor = Math.max(0, this.armor);
    const damageDealt = Math.max(1, (rawDamage * mult) - effectiveArmor);
    this.hp -= damageDealt;
    this.hpBar.updateHp(this.hp, this.maxHp);

    spawnDamageNumber(this.x, this.y + 1.2, this.z, Math.round(damageDealt));

    if (this.hp <= 0) {
      this.die();
    }
  }

  die() {
    spawnExplosion(this.x, this.y, this.z, this.isFlying ? 2.5 : 1.8);
    sfx.playExplode();
    scene.remove(this.mesh);
    scene.remove(this.hpBar);

    // Release supply
    if (this.faction === FACTION_PLAYER) {
      G.player.usedSupply = Math.max(0, G.player.usedSupply - this.stats.supply);
    } else {
      G.enemy.usedSupply = Math.max(0, G.enemy.usedSupply - this.stats.supply);
    }

    const idx = G.units.indexOf(this);
    if (idx !== -1) G.units.splice(idx, 1);

    const selIdx = G.selectedUnits.indexOf(this);
    if (selIdx !== -1) G.selectedUnits.splice(selIdx, 1);

    updateHudSelection();
    updateTopBar();
  }

  findNearestDropoff() {
    let nearest = null;
    let minDist = Infinity;
    for (const b of G.buildings) {
      if (b.faction === this.faction && b.stats.isDropoff && b.completed) {
        const d = Math.hypot(b.x - this.x, b.z - this.z);
        if (d < minDist) {
          minDist = d;
          nearest = b;
        }
      }
    }
    return nearest;
  }

  update(dt) {
    if (this.attackTimer > 0) this.attackTimer -= dt;

    // Keep HP bar billboard facing camera
    this.hpBar.position.set(this.x, this.y + (this.isFlying ? 2.0 : 1.8), this.z);
    this.hpBar.quaternion.copy(camera.quaternion);

    // Combat auto-targeting if idle or attack-moving
    if (this.stats.damage > 0 && (this.order === 'idle' || this.order === 'attack-move' || this.order === 'hold')) {
      if (!this.targetEntity || this.targetEntity.hp <= 0) {
        this.targetEntity = this.findHostileTarget(this.range + (this.order === 'hold' ? 0 : 8));
      }
    }

    // Process Specific Orders
    if (this.targetEntity) {
      if (this.targetEntity.hp <= 0) {
        this.targetEntity = null;
        if (this.order === 'attack') this.order = 'idle';
      } else {
        const dist = Math.hypot(this.targetEntity.x - this.x, this.targetEntity.z - this.z);
        if (dist <= this.range) {
          // In range -> shoot
          this.engageTarget(this.targetEntity);
        } else if (this.order !== 'hold') {
          // Pursue into range
          this.moveTowards(this.targetEntity.x, this.targetEntity.z, dt);
        }
      }
    } else if (this.order === 'move' || this.order === 'attack-move') {
      if (this.targetPos) {
        const dist = Math.hypot(this.targetPos.x - this.x, this.targetPos.z - this.z);
        if (dist > 1.2) {
          this.moveTowards(this.targetPos.x, this.targetPos.z, dt);
        } else {
          this.targetPos = null;
          this.order = 'idle';
        }
      }
    } else if (this.type === 'worker') {
      this.updateWorkerHarvesting(dt);
    }

    // Local separation force so units never stack or clump into a single point
    this.applyFlockingSeparation();

    // Position mesh
    const groundY = getTerrainHeight(this.x, this.z);
    this.y = this.isFlying ? groundY + this.stats.flightHeight : groundY;
    this.mesh.position.set(this.x, this.y, this.z);
  }

  updateWorkerHarvesting(dt) {
    const cargoMesh = this.mesh.getObjectByName('cargoMesh');
    if (cargoMesh) cargoMesh.visible = this.carriedOre > 0;

    if (this.carriedOre >= 10) {
      // Find headquarters to return minerals
      const dropoff = this.findNearestDropoff();
      if (dropoff) {
        const dist = Math.hypot(dropoff.x - this.x, dropoff.z - this.z);
        if (dist > dropoff.stats.size * 0.6) {
          this.moveTowards(dropoff.x, dropoff.z, dt);
        } else {
          // Unload crystals
          if (this.faction === FACTION_PLAYER) {
            G.player.ore += this.carriedOre;
            updateTopBar();
          } else {
            G.enemy.ore += this.carriedOre;
          }
          this.carriedOre = 0;

          // Resume gathering from previous crystal
          if (this.harvestTarget && this.harvestTarget.amount > 0) {
            this.order = 'gather';
          } else {
            // Find nearby crystal
            this.harvestTarget = this.findNearestOre();
            if (this.harvestTarget) this.order = 'gather';
            else this.order = 'idle';
          }
        }
      }
    } else if (this.order === 'gather') {
      if (!this.harvestTarget || this.harvestTarget.amount <= 0) {
        this.harvestTarget = this.findNearestOre();
      }
      if (this.harvestTarget) {
        const dist = Math.hypot(this.harvestTarget.x - this.x, this.harvestTarget.z - this.z);
        if (dist > 2.2) {
          this.moveTowards(this.harvestTarget.x, this.harvestTarget.z, dt);
        } else {
          // Harvest over time
          this.harvestTimer += dt;
          if (this.harvestTimer >= 1.5) {
            this.harvestTimer = 0;
            this.carriedOre = this.harvestTarget.harvest(10);
          }
        }
      } else {
        this.order = 'idle';
      }
    }
  }

  findNearestOre() {
    let nearest = null;
    let minDist = Infinity;
    for (const res of G.resources) {
      if (res.amount > 0) {
        const d = Math.hypot(res.x - this.x, res.z - this.z);
        if (d < minDist) {
          minDist = d;
          nearest = res;
        }
      }
    }
    return nearest;
  }

  findHostileTarget(maxRange) {
    let bestTarget = null;
    let bestDist = maxRange;

    // Check enemy units
    for (const u of G.units) {
      if (u.faction !== this.faction && u.hp > 0) {
        if (u.isFlying && this.stats.targets === 'ground') continue;
        // Fog check: Can't acquire target if hidden in fog
        if (this.faction === FACTION_PLAYER && !u.mesh.visible) continue;
        if (this.faction === FACTION_ENEMY) {
          const { gx, gz } = worldToFogGrid(u.x, u.z);
          if (G.enemyFogGrid[gz * FOG_RES + gx] !== 2) continue;
        }

        const d = Math.hypot(u.x - this.x, u.z - this.z);
        if (d < bestDist) {
          bestDist = d;
          bestTarget = u;
        }
      }
    }

    // Check enemy buildings
    if (!bestTarget) {
      for (const b of G.buildings) {
        if (b.faction !== this.faction && b.hp > 0) {
          if (this.faction === FACTION_PLAYER && !b.mesh.visible) continue;
          if (this.faction === FACTION_ENEMY) {
            const { gx, gz } = worldToFogGrid(b.x, b.z);
            if (G.enemyFogGrid[gz * FOG_RES + gx] !== 2) continue;
          }

          const d = Math.hypot(b.x - this.x, b.z - this.z);
          if (d < bestDist) {
            bestDist = d;
            bestTarget = b;
          }
        }
      }
    }
    return bestTarget;
  }

  engageTarget(target) {
    // Face target
    const angle = Math.atan2(target.x - this.x, target.z - this.z);
    this.mesh.rotation.y = angle;

    if (this.attackTimer <= 0) {
      this.attackTimer = this.cooldown;
      const weaponBuff = (this.faction === FACTION_PLAYER ? G.player.weaponLevel * 0.2 : G.enemy.weaponLevel * 0.2);
      const totalDamage = this.damage * (1.0 + weaponBuff);

      // Spawn projectile effect
      spawnProjectile(this, target, this.stats.damageType);

      // Sound
      if (this.type === 'marine') sfx.playShoot('rifle');
      else if (this.type === 'tank') sfx.playShoot('heavy');
      else sfx.playShoot('laser');
    }
  }

  moveTowards(tx, tz, dt) {
    let dx = tx - this.x;
    let dz = tz - this.z;
    const dist = Math.hypot(dx, dz);
    if (dist < 0.1) return;

    dx /= dist;
    dz /= dist;

    // Obstacle avoidance for ground units
    if (!this.isFlying) {
      for (const obs of G.obstacles) {
        const ox = this.x - obs.x;
        const oz = this.z - obs.z;
        const odist = Math.hypot(ox, oz);
        const minDist = obs.radius + this.radius + 1.2;
        if (odist < minDist) {
          // Push direction away from obstacle
          const pushX = (ox / (odist || 1));
          const pushZ = (oz / (odist || 1));
          dx = dx * 0.3 + pushX * 0.7;
          dz = dz * 0.3 + pushZ * 0.7;
        }
      }
      // Avoid buildings
      for (const b of G.buildings) {
        const bx = this.x - b.x;
        const bz = this.z - b.z;
        const bdist = Math.hypot(bx, bz);
        const minDist = b.stats.size * 0.55 + this.radius;
        if (bdist < minDist) {
          dx = dx * 0.2 + (bx / (bdist || 1)) * 0.8;
          dz = dz * 0.2 + (bz / (bdist || 1)) * 0.8;
        }
      }
    }

    const moveStep = this.speed * dt;
    this.x += dx * moveStep;
    this.z += dz * moveStep;

    // Smooth unit rotation
    const targetAngle = Math.atan2(dx, dz);
    let diff = targetAngle - this.mesh.rotation.y;
    while (diff < -Math.PI) diff += Math.PI * 2;
    while (diff > Math.PI) diff -= Math.PI * 2;
    this.mesh.rotation.y += diff * 12 * dt;
  }

  applyFlockingSeparation() {
    for (const other of G.units) {
      if (other === this) continue;
      // Air and ground don't push each other
      if (this.isFlying !== other.isFlying) continue;

      const dx = this.x - other.x;
      const dz = this.z - other.z;
      const d2 = dx * dx + dz * dz;
      const minDist = this.radius + other.radius;
      if (d2 < minDist * minDist && d2 > 0.001) {
        const d = Math.sqrt(d2);
        const overlap = (minDist - d) * 0.5;
        this.x += (dx / d) * overlap;
        this.z += (dz / d) * overlap;
      }
    }
  }
}

/* =========================================================================
   BUILDING CLASS
   ========================================================================= */
class Building {
  constructor(type, faction, x, z, instant = false) {
    this.id = nextEntityId++;
    this.type = type;
    this.faction = faction;
    this.stats = BUILDING_STATS[type];
    this.maxHp = this.stats.hp;
    this.hp = instant ? this.maxHp : Math.floor(this.maxHp * 0.15);
    this.armor = this.stats.armor + (faction === FACTION_PLAYER ? G.player.armorLevel * 2 : G.enemy.armorLevel * 2);
    this.sight = this.stats.sight;
    this.completed = instant;
    this.buildProgress = instant ? 1.0 : 0.15;
    this.buildTime = this.stats.buildTime;
    this.lastSeen = false;

    this.x = x;
    this.z = z;
    this.y = getTerrainHeight(x, z);

    // Production & Research queues
    this.queue = [];
    this.queueProgress = 0;
    this.rallyPoint = new THREE.Vector3(x + 4.5, 0, z + 4.5);

    // Defense turret combat stats
    this.attackTimer = 0;
    this.targetEntity = null;

    this.mesh = createBuildingMesh(type, faction);
    this.mesh.position.set(this.x, this.y, this.z);
    scene.add(this.mesh);

    this.hpBar = createHpBarMesh(this.stats.size * 0.8, 0.28);
    this.hpBar.position.set(this.x, this.y + 4.5, this.z);
    scene.add(this.hpBar);

    if (this.completed) {
      this.onComplete();
    }
  }

  setSelected(val) {
    const ring = this.mesh.getObjectByName('selectRing');
    if (ring) ring.visible = val;
  }

  onComplete() {
    this.completed = true;
    this.hp = this.maxHp;
    this.hpBar.updateHp(this.hp, this.maxHp);

    if (this.stats.supplyGranted > 0) {
      if (this.faction === FACTION_PLAYER) {
        G.player.maxSupply += this.stats.supplyGranted;
        updateTopBar();
      } else {
        G.enemy.maxSupply += this.stats.supplyGranted;
      }
    }
  }

  takeDamage(rawDamage, damageType) {
    const mult = (DAMAGE_MULTIPLIER[damageType] && DAMAGE_MULTIPLIER[damageType][this.stats.armorType]) || 1.0;
    const effectiveArmor = Math.max(0, this.armor);
    const damageDealt = Math.max(1, (rawDamage * mult) - effectiveArmor);
    this.hp -= damageDealt;
    this.hpBar.updateHp(this.hp, this.maxHp);

    spawnDamageNumber(this.x, this.y + 3.0, this.z, Math.round(damageDealt));

    if (this.hp <= 0) {
      this.die();
    }
  }

  die() {
    spawnExplosion(this.x, this.y + 1.5, this.z, this.stats.size * 0.8);
    sfx.playExplode();
    scene.remove(this.mesh);
    scene.remove(this.hpBar);

    if (this.completed && this.stats.supplyGranted > 0) {
      if (this.faction === FACTION_PLAYER) {
        G.player.maxSupply = Math.max(0, G.player.maxSupply - this.stats.supplyGranted);
        updateTopBar();
      } else {
        G.enemy.maxSupply = Math.max(0, G.enemy.maxSupply - this.stats.supplyGranted);
      }
    }

    const idx = G.buildings.indexOf(this);
    if (idx !== -1) G.buildings.splice(idx, 1);

    if (G.selectedBuilding === this) {
      G.selectedBuilding = null;
      updateHudSelection();
    }

    // Check Victory / Defeat if HQ was destroyed
    if (this.stats.isHQ) {
      checkHQConditions();
    }
  }

  queueProduction(itemId, isResearch = false) {
    const isPlayer = this.faction === FACTION_PLAYER;
    const eco = isPlayer ? G.player : G.enemy;

    const cost = isResearch ? UPGRADE_STATS[itemId].cost : UNIT_STATS[itemId].cost;
    const supplyNeed = isResearch ? 0 : UNIT_STATS[itemId].supply;

    if (eco.ore < cost) {
      if (isPlayer) showAlert('Insufficient Ore minerals!');
      return false;
    }
    if (supplyNeed > 0 && eco.usedSupply + supplyNeed > eco.maxSupply) {
      if (isPlayer) {
        showAlert('Supply limit reached! Build more Solar Reactors.');
        sfx.playAlert();
      }
      return false;
    }

    // Deduct cost and reserve supply
    eco.ore -= cost;
    if (supplyNeed > 0) eco.usedSupply += supplyNeed;
    if (isPlayer) updateTopBar();

    this.queue.push({
      id: itemId,
      isResearch: isResearch,
      totalTime: isResearch ? UPGRADE_STATS[itemId].time : UNIT_STATS[itemId].buildTime
    });

    if (isPlayer && G.selectedBuilding === this) {
      updateHudSelection();
    }
    return true;
  }

  cancelQueue(index) {
    if (index < 0 || index >= this.queue.length) return;
    const item = this.queue[index];
    const isPlayer = this.faction === FACTION_PLAYER;
    const eco = isPlayer ? G.player : G.enemy;

    // Refund resources
    const cost = item.isResearch ? UPGRADE_STATS[item.id].cost : UNIT_STATS[item.id].cost;
    const supply = item.isResearch ? 0 : UNIT_STATS[item.id].supply;

    eco.ore += cost;
    if (supply > 0) eco.usedSupply = Math.max(0, eco.usedSupply - supply);

    this.queue.splice(index, 1);
    if (index === 0) this.queueProgress = 0;

    if (isPlayer) {
      updateTopBar();
      if (G.selectedBuilding === this) updateHudSelection();
    }
  }

  update(dt) {
    // Face HP bar towards camera
    this.hpBar.quaternion.copy(camera.quaternion);

    // Construction phase
    if (!this.completed) {
      this.buildProgress += dt / this.buildTime;
      this.hp = Math.floor(this.maxHp * THREE.MathUtils.clamp(this.buildProgress, 0.1, 1.0));
      this.hpBar.updateHp(this.hp, this.maxHp);
      // Visual rise from ground
      this.mesh.position.y = this.y - (1.0 - Math.min(1.0, this.buildProgress)) * 2.0;

      if (this.buildProgress >= 1.0) {
        this.onComplete();
      }
      return;
    }

    // Production Queue execution
    if (this.queue.length > 0) {
      const current = this.queue[0];
      this.queueProgress += dt;
      if (this.queueProgress >= current.totalTime) {
        this.queueProgress = 0;
        this.finishProduction(current);
        this.queue.shift();
        if (this.faction === FACTION_PLAYER && G.selectedBuilding === this) {
          updateHudSelection();
        }
      }
    }

    // Defense Turret auto-defense
    if (this.stats.isDefensive) {
      if (this.attackTimer > 0) this.attackTimer -= dt;
      if (!this.targetEntity || this.targetEntity.hp <= 0) {
        this.targetEntity = this.findHostileTarget(this.stats.range);
      }
      if (this.targetEntity) {
        const d = Math.hypot(this.targetEntity.x - this.x, this.targetEntity.z - this.z);
        if (d <= this.stats.range) {
          // Aim swivel
          const swivel = this.mesh.getObjectByName('swivel');
          if (swivel) {
            swivel.rotation.y = Math.atan2(this.targetEntity.x - this.x, this.targetEntity.z - this.z);
          }
          if (this.attackTimer <= 0) {
            this.attackTimer = this.stats.cooldown;
            spawnProjectile(this, this.targetEntity, this.stats.damageType);
            sfx.playShoot('laser');
          }
        } else {
          this.targetEntity = null;
        }
      }
    }
  }

  finishProduction(item) {
    if (item.isResearch) {
      // Apply Upgrade
      if (item.id === 'upgrade_weapons') {
        if (this.faction === FACTION_PLAYER) {
          G.player.weaponLevel++;
          showAlert('Weapons Upgrade Cal. II Complete!');
        } else {
          G.enemy.weaponLevel++;
        }
      } else if (item.id === 'upgrade_armor') {
        if (this.faction === FACTION_PLAYER) {
          G.player.armorLevel++;
          showAlert('Reinforced Plating Upgrade Complete!');
        } else {
          G.enemy.armorLevel++;
        }
      }
      if (this.faction === FACTION_PLAYER) {
        document.getElementById('tech-status').textContent = `Armor Lv ${G.player.armorLevel} | Weap Lv ${G.player.weaponLevel}`;
      }
    } else {
      // Spawn Unit around building exit
      const spawnX = this.x + (Math.random() - 0.5) * 2;
      const spawnZ = this.z + this.stats.size * 0.65;
      const u = new Unit(item.id, this.faction, spawnX, spawnZ);
      G.units.push(u);

      // Follow rally point
      u.issueMove(this.rallyPoint.x + (Math.random() - 0.5) * 4, this.rallyPoint.z + (Math.random() - 0.5) * 4);

      if (this.faction === FACTION_PLAYER) {
        showAlert(`${u.stats.name} deployed.`);
      }
    }
  }

  findHostileTarget(maxRange) {
    let best = null;
    let bestDist = maxRange;
    for (const u of G.units) {
      if (u.faction !== this.faction && u.hp > 0) {
        if (this.faction === FACTION_PLAYER && !u.mesh.visible) continue;
        if (this.faction === FACTION_ENEMY) {
          const { gx, gz } = worldToFogGrid(u.x, u.z);
          if (G.enemyFogGrid[gz * FOG_RES + gx] !== 2) continue;
        }
        const d = Math.hypot(u.x - this.x, u.z - this.z);
        if (d < bestDist) {
          bestDist = d;
          best = u;
        }
      }
    }
    return best;
  }
}

function checkHQConditions() {
  const playerHQs = G.buildings.filter(b => b.faction === FACTION_PLAYER && b.stats.isHQ);
  const enemyHQs = G.buildings.filter(b => b.faction === FACTION_ENEMY && b.stats.isHQ);

  if (enemyHQs.length === 0 && !G.gameOver) {
    triggerGameOver(true);
  } else if (playerHQs.length === 0 && !G.gameOver) {
    triggerGameOver(false);
  }
}

function triggerGameOver(isVictory) {
  G.gameOver = true;
  G.victory = isVictory;
  const modal = document.getElementById('modal-screen');
  const card = document.getElementById('modal-content');

  if (isVictory) {
    card.innerHTML = `
      <h1 style="color:#00e676;">MISSION ACCOMPLISHED</h1>
      <p style="font-size:16px;">The Dominion Syndicate headquarters on Tartarus VII have been eliminated. The frontier crystal deposits are under expeditionary control.</p>
      <div style="background:#102235; padding:12px; border-radius:6px; margin:16px 0; font-size:13px;">
        <p><strong>Match Time:</strong> ${document.getElementById('game-timer').textContent}</p>
        <p><strong>Ore Harvested:</strong> ${G.player.ore} minerals</p>
        <p><strong>Army Units Deployed:</strong> ${G.units.filter(u => u.faction === FACTION_PLAYER).length}</p>
      </div>
      <button class="modal-btn" onclick="restartMatch()">PLAY AGAIN</button>
    `;
  } else {
    card.innerHTML = `
      <h1 style="color:#ff1744;">COMMAND CENTER DESTROYED</h1>
      <p style="font-size:16px;">All expedition Headquarters have fallen. Dominion Syndicate forces have overrun our frontier operation.</p>
      <div style="background:#102235; padding:12px; border-radius:6px; margin:16px 0; font-size:13px;">
        <p><strong>Match Time:</strong> ${document.getElementById('game-timer').textContent}</p>
      </div>
      <button class="modal-btn" onclick="restartMatch()">RESTART MISSION</button>
    `;
  }
  modal.style.display = 'flex';
}

/* =========================================================================
   PROJECTILES, PARTICLES & EXPLOSIONS
   ========================================================================= */
function spawnProjectile(source, target, damageType) {
  const isBeam = (damageType === 'energy');
  const start = new THREE.Vector3(source.x, source.y + 1.2, source.z);
  const end = new THREE.Vector3(target.x, target.y + 0.8, target.z);

  if (isBeam) {
    // Instant energy beam tracer
    const lineMat = new THREE.LineBasicMaterial({
      color: source.faction === FACTION_PLAYER ? 0x00e5ff : 0xff3d00,
      linewidth: 3
    });
    const lineGeo = new THREE.BufferGeometry().setFromPoints([start, end]);
    const line = new THREE.Line(lineGeo, lineMat);
    scene.add(line);
    setTimeout(() => scene.remove(line), 90);

    target.takeDamage(source.damage || source.stats.damage, damageType);
  } else {
    // Ballistic / kinetic projectile
    const geo = new THREE.SphereGeometry(damageType === 'explosive' ? 0.35 : 0.18, 6, 6);
    const mat = new THREE.MeshBasicMaterial({
      color: damageType === 'explosive' ? 0xffaa00 : 0xffffff
    });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.copy(start);
    scene.add(mesh);

    G.projectiles.push({
      mesh: mesh,
      source: source,
      target: target,
      damage: source.damage || source.stats.damage,
      damageType: damageType,
      pos: start,
      targetPos: end,
      speed: damageType === 'explosive' ? 42 : 58
    });
  }
}

function updateProjectiles(dt) {
  for (let i = G.projectiles.length - 1; i >= 0; i--) {
    const p = G.projectiles[i];
    if (p.target && p.target.hp > 0) {
      p.targetPos.set(p.target.x, p.target.y + 0.8, p.target.z);
    }
    const dir = new THREE.Vector3().subVectors(p.targetPos, p.pos);
    const dist = dir.length();
    const step = p.speed * dt;

    if (dist <= step || dist < 0.8) {
      // Hit target
      if (p.target && p.target.hp > 0) {
        p.target.takeDamage(p.damage, p.damageType);
      }
      scene.remove(p.mesh);
      G.projectiles.splice(i, 1);
    } else {
      dir.normalize();
      p.pos.addScaledVector(dir, step);
      p.mesh.position.copy(p.pos);
    }
  }
}

function spawnExplosion(x, y, z, scale = 1.0) {
  const count = Math.floor(12 * scale);
  for (let i = 0; i < count; i++) {
    const geo = new THREE.SphereGeometry(0.2 * scale, 4, 4);
    const mat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.4 ? 0xff6600 : 0xffcc00 });
    const p = new THREE.Mesh(geo, mat);
    p.position.set(x, y, z);
    scene.add(p);

    const vel = new THREE.Vector3(
      (Math.random() - 0.5) * 14 * scale,
      Math.random() * 12 * scale + 4,
      (Math.random() - 0.5) * 14 * scale
    );

    G.particles.push({
      mesh: p,
      vel: vel,
      life: 0.4 + Math.random() * 0.35,
      maxLife: 0.75
    });
  }
}

function updateParticles(dt) {
  for (let i = G.particles.length - 1; i >= 0; i--) {
    const p = G.particles[i];
    p.life -= dt;
    if (p.life <= 0) {
      scene.remove(p.mesh);
      G.particles.splice(i, 1);
    } else {
      p.vel.y -= 25 * dt; // gravity
      p.mesh.position.x += p.vel.x * dt;
      p.mesh.position.y += p.vel.y * dt;
      p.mesh.position.z += p.vel.z * dt;
      const s = p.life / p.maxLife;
      p.mesh.scale.set(s, s, s);
    }
  }
}

function spawnDamageNumber(x, y, z, amount) {
  const v = new THREE.Vector3(x, y, z).project(camera);
  if (v.z > 1) return; // behind camera
  const sx = (v.x * 0.5 + 0.5) * window.innerWidth;
  const sy = (-(v.y * 0.5) + 0.5) * window.innerHeight;

  const div = document.createElement('div');
  div.className = 'combat-text';
  div.textContent = `-${amount}`;
  div.style.left = `${sx}px`;
  div.style.top = `${sy}px`;
  document.body.appendChild(div);
  setTimeout(() => div.remove(), 900);
}

/* =========================================================================
   AUTONOMOUS AI OPPONENT (ECONOMY, PRODUCTION, SCOUTING & ATTACK WAVES)
   ========================================================================= */
function updateAI(dt) {
  const ai = G.enemy;
  ai.strategyTimer += dt;

  // 1. Worker management: keep miners harvesting
  const aiWorkers = G.units.filter(u => u.faction === FACTION_ENEMY && u.type === 'worker');
  for (const w of aiWorkers) {
    if (w.order === 'idle') {
      const ore = w.findNearestOre();
      if (ore) w.issueGather(ore);
    }
  }

  // 2. Base & Production planning every 2 seconds
  if (ai.strategyTimer >= 2.0) {
    ai.strategyTimer = 0;

    const aiBuildings = G.buildings.filter(b => b.faction === FACTION_ENEMY);
    const hqs = aiBuildings.filter(b => b.stats.isHQ);
    const reactors = aiBuildings.filter(b => b.type === 'reactor');
    const barracks = aiBuildings.filter(b => b.type === 'barracks');
    const foundries = aiBuildings.filter(b => b.type === 'foundry');
    const aeros = aiBuildings.filter(b => b.type === 'aerodrome');
    const turrets = aiBuildings.filter(b => b.type === 'turret');
    const techlabs = aiBuildings.filter(b => b.type === 'techlab');

    // Need more supply?
    if (ai.usedSupply + 4 >= ai.maxSupply && ai.ore >= BUILDING_STATS.reactor.cost) {
      aiBuildStructure('reactor');
    }

    // Need more workers? (up to ~14 miners)
    if (aiWorkers.length < 14 && ai.ore >= UNIT_STATS.worker.cost && hqs.length > 0) {
      const readyHQ = hqs.find(h => h.completed && h.queue.length === 0);
      if (readyHQ) readyHQ.queueProduction('worker');
    }

    // Construct Barracks early
    if (barracks.length === 0 && ai.ore >= BUILDING_STATS.barracks.cost) {
      aiBuildStructure('barracks');
    }

    // Construct Defense Turret near base perimeter
    if (turrets.length < 2 && ai.ore >= BUILDING_STATS.turret.cost && barracks.length > 0) {
      aiBuildStructure('turret');
    }

    // Construct Foundry for heavy armor
    if (barracks.length >= 1 && foundries.length === 0 && ai.ore >= BUILDING_STATS.foundry.cost) {
      aiBuildStructure('foundry');
    }

    // Construct Aerodrome for gunships
    if (foundries.length >= 1 && aeros.length === 0 && ai.ore >= BUILDING_STATS.aerodrome.cost) {
      aiBuildStructure('aerodrome');
    }

    // Construct Techlab for upgrades
    if (techlabs.length === 0 && barracks.length >= 1 && ai.ore >= BUILDING_STATS.techlab.cost) {
      aiBuildStructure('techlab');
    }

    // Research upgrades if techlab is available
    if (techlabs.length > 0 && techlabs[0].completed && techlabs[0].queue.length === 0) {
      if (ai.weaponLevel === 0 && ai.ore >= UPGRADE_STATS.upgrade_weapons.cost) {
        techlabs[0].queueProduction('upgrade_weapons', true);
      } else if (ai.armorLevel === 0 && ai.ore >= UPGRADE_STATS.upgrade_armor.cost) {
        techlabs[0].queueProduction('upgrade_armor', true);
      }
    }

    // Second HQ Expansion if wealthy
    if (hqs.length < 2 && ai.ore >= BUILDING_STATS.hq.cost + 50) {
      aiBuildExpansionHQ();
    }

    // Train Combat Army across all production buildings
    for (const b of barracks) {
      if (b.completed && b.queue.length === 0) {
        if (Math.random() < 0.6) b.queueProduction('marine');
        else b.queueProduction('scout');
      }
    }
    for (const f of foundries) {
      if (f.completed && f.queue.length === 0) {
        f.queueProduction('tank');
      }
    }
    for (const a of aeros) {
      if (a.completed && a.queue.length === 0) {
        a.queueProduction('flyer');
      }
    }
  }

  // 3. Army Assembly and Wave Attacks
  const aiCombatUnits = G.units.filter(u => u.faction === FACTION_ENEMY && u.type !== 'worker');
  const targetWaveSize = 4 + ai.attackWave * 3;

  // After 60 seconds of peace, AI begins escalating assaults
  if (G.time > 65 && G.time - ai.lastAttackTime > 50 && aiCombatUnits.length >= targetWaveSize) {
    ai.lastAttackTime = G.time;
    ai.attackWave++;

    // Locate player target (HQ or units)
    let attackTarget = G.buildings.find(b => b.faction === FACTION_PLAYER && b.stats.isHQ);
    if (!attackTarget) attackTarget = G.buildings.find(b => b.faction === FACTION_PLAYER);
    if (!attackTarget) attackTarget = G.units.find(u => u.faction === FACTION_PLAYER);

    const tx = attackTarget ? attackTarget.x : -80;
    const tz = attackTarget ? attackTarget.z : -80;

    for (const u of aiCombatUnits) {
      u.issueAttackMove(tx + (Math.random() - 0.5) * 8, tz + (Math.random() - 0.5) * 8);
    }

    showAlert('Hostile Dominion strike force detected inbound!');
    sfx.playAlert();
  }
}

function aiBuildStructure(type) {
  const stat = BUILDING_STATS[type];
  if (G.enemy.ore < stat.cost) return;

  // Position near enemy base at (80, 80)
  const angle = Math.random() * Math.PI * 2;
  const dist = 14 + Math.random() * 22;
  const bx = 80 + Math.cos(angle) * dist;
  const bz = 80 + Math.sin(angle) * dist;

  if (isValidPlacement(type, bx, bz)) {
    G.enemy.ore -= stat.cost;
    const b = new Building(type, FACTION_ENEMY, bx, bz, false);
    G.buildings.push(b);
  }
}

function aiBuildExpansionHQ() {
  const stat = BUILDING_STATS.hq;
  if (G.enemy.ore < stat.cost) return;
  // Expand towards the rich center-east deposit (60, -60)
  const bx = 60 + (Math.random() - 0.5) * 8;
  const bz = -60 + (Math.random() - 0.5) * 8;
  if (isValidPlacement('hq', bx, bz)) {
    G.enemy.ore -= stat.cost;
    const b = new Building('hq', FACTION_ENEMY, bx, bz, false);
    G.buildings.push(b);
  }
}

/* =========================================================================
   BUILDING PLACEMENT LOGIC & VALIDATION
   ========================================================================= */
function isValidPlacement(type, x, z) {
  const radius = BUILDING_STATS[type].size * 0.5;

  // Bounds check
  const lim = MAP_SIZE / 2 - 12;
  if (Math.abs(x) > lim || Math.abs(z) > lim) return false;

  // Collision with obstacles
  for (const obs of G.obstacles) {
    if (Math.hypot(x - obs.x, z - obs.z) < obs.radius + radius) return false;
  }

  // Collision with existing buildings
  for (const b of G.buildings) {
    const minDist = radius + b.stats.size * 0.5 + 1.5;
    if (Math.hypot(x - b.x, z - b.z) < minDist) return false;
  }

  // Collision with crystal deposits
  for (const res of G.resources) {
    if (Math.hypot(x - res.x, z - res.z) < radius + 2.5) return false;
  }

  return true;
}

function startPlacement(type) {
  G.placingType = type;
  if (G.placementMesh) scene.remove(G.placementMesh);

  G.placementMesh = createBuildingMesh(type, FACTION_PLAYER);
  // Give transparent ghost look
  G.placementMesh.traverse((child) => {
    if (child.isMesh && child.name !== 'selectRing') {
      child.material = new THREE.MeshBasicMaterial({
        color: 0x00e5ff,
        transparent: true,
        opacity: 0.55,
        wireframe: true
      });
    }
  });
  scene.add(G.placementMesh);
}

function cancelPlacement() {
  G.placingType = null;
  if (G.placementMesh) {
    scene.remove(G.placementMesh);
    G.placementMesh = null;
  }
}

function confirmPlacement(x, z) {
  if (!G.placingType) return;
  const stat = BUILDING_STATS[G.placingType];

  if (G.player.ore < stat.cost) {
    showAlert('Insufficient Ore!');
    cancelPlacement();
    return;
  }

  if (!isValidPlacement(G.placingType, x, z)) {
    showAlert('Invalid building position: terrain or structures obstructed!');
    sfx.playAlert();
    return;
  }

  // Deduct cost and spawn building
  G.player.ore -= stat.cost;
  updateTopBar();
  const b = new Building(G.placingType, FACTION_PLAYER, x, z, false);
  G.buildings.push(b);
  sfx.playOrder();

  // If worker was selected, assign to build it
  const selectedWorkers = G.selectedUnits.filter(u => u.type === 'worker');
  if (selectedWorkers.length > 0) {
    for (const w of selectedWorkers) {
      w.issueMove(x, z);
    }
  }

  cancelPlacement();
}

/* =========================================================================
   USER INPUT & SELECTION SYSTEM
   ========================================================================= */
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let dragStart = null;
let isDraggingBox = false;
const selectBoxEl = document.getElementById('select-box');

function getGroundIntersection(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObject(terrainMesh);
  if (hits.length > 0) return hits[0].point;
  return null;
}

window.addEventListener('mousedown', (e) => {
  if (e.target.closest('#hud') || e.target.closest('.modal-overlay')) return;

  if (e.button === 0) {
    // Left Click: Placement or selection start
    const pt = getGroundIntersection(e);
    if (G.placingType && pt) {
      confirmPlacement(pt.x, pt.z);
      return;
    }

    if (G.activeCommand === 'attack-move' && pt) {
      for (const u of G.selectedUnits) {
        u.issueAttackMove(pt.x, pt.z);
      }
      G.activeCommand = null;
      updateHudSelection();
      sfx.playOrder();
      return;
    }

    dragStart = { x: e.clientX, y: e.clientY };
    isDraggingBox = false;

  } else if (e.button === 2) {
    // Right Click: Order or cancel placement
    if (G.placingType) {
      cancelPlacement();
      return;
    }
    const pt = getGroundIntersection(e);
    if (!pt) return;

    // Check if clicked an entity
    const targetEntity = getEntityUnderMouse(e);

    if (targetEntity) {
      if (targetEntity.amount !== undefined) {
        // Crystal deposit clicked -> workers gather
        for (const u of G.selectedUnits) {
          if (u.type === 'worker') u.issueGather(targetEntity);
          else u.issueMove(targetEntity.x, targetEntity.z);
        }
        sfx.playOrder();
      } else if (targetEntity.faction !== FACTION_PLAYER) {
        // Hostile enemy clicked -> attack order
        for (const u of G.selectedUnits) {
          u.issueAttack(targetEntity);
        }
        sfx.playOrder();
      } else if (targetEntity.faction === FACTION_PLAYER && targetEntity.stats && targetEntity.stats.isDropoff) {
        // Friendly HQ -> deposit ore
        for (const u of G.selectedUnits) {
          if (u.type === 'worker' && u.carriedOre > 0) {
            u.issueMove(targetEntity.x, targetEntity.z);
          }
        }
        sfx.playOrder();
      }
    } else {
      // Ground move
      const count = G.selectedUnits.length;
      G.selectedUnits.forEach((u, i) => {
        // Spread units slightly into formation
        const angle = (i / (count || 1)) * Math.PI * 2;
        const spread = count > 1 ? Math.min(6, count * 0.75) : 0;
        const tx = pt.x + Math.cos(angle) * spread;
        const tz = pt.z + Math.sin(angle) * spread;
        u.issueMove(tx, tz);
      });
      sfx.playOrder();
    }
  }
});

window.addEventListener('mousemove', (e) => {
  // Placement ghost tracking
  if (G.placingType && G.placementMesh) {
    const pt = getGroundIntersection(e);
    if (pt) {
      G.placementMesh.position.set(pt.x, getTerrainHeight(pt.x, pt.z), pt.z);
    }
  }

  // Drag selection box
  if (dragStart && e.buttons === 1) {
    const dx = Math.abs(e.clientX - dragStart.x);
    const dy = Math.abs(e.clientY - dragStart.y);
    if (dx > 4 || dy > 4) {
      isDraggingBox = true;
      selectBoxEl.style.display = 'block';
      const left = Math.min(dragStart.x, e.clientX);
      const top = Math.min(dragStart.y, e.clientY);
      selectBoxEl.style.left = `${left}px`;
      selectBoxEl.style.top = `${top}px`;
      selectBoxEl.style.width = `${dx}px`;
      selectBoxEl.style.height = `${dy}px`;
    }
  }
});

window.addEventListener('mouseup', (e) => {
  if (e.button === 0 && dragStart) {
    if (isDraggingBox) {
      // Area box selection
      selectBoxEl.style.display = 'none';
      const x1 = Math.min(dragStart.x, e.clientX);
      const x2 = Math.max(dragStart.x, e.clientX);
      const y1 = Math.min(dragStart.y, e.clientY);
      const y2 = Math.max(dragStart.y, e.clientY);

      if (!e.shiftKey) clearSelection();

      for (const u of G.units) {
        if (u.faction === FACTION_PLAYER) {
          const sp = new THREE.Vector3(u.x, u.y, u.z).project(camera);
          const sx = (sp.x * 0.5 + 0.5) * window.innerWidth;
          const sy = (-(sp.y * 0.5) + 0.5) * window.innerHeight;
          if (sx >= x1 && sx <= x2 && sy >= y1 && sy <= y2) {
            selectUnit(u, true);
          }
        }
      }
      sfx.playClick();
      updateHudSelection();
    } else if (!e.target.closest('#hud') && !e.target.closest('.modal-overlay')) {
      // Single unit / building click
      const entity = getEntityUnderMouse(e);
      if (!e.shiftKey) clearSelection();

      if (entity) {
        if (entity instanceof Unit) {
          selectUnit(entity, true);
        } else if (entity instanceof Building) {
          selectBuilding(entity);
        }
        sfx.playClick();
      }
      updateHudSelection();
    }
    dragStart = null;
    isDraggingBox = false;
  }
});

function getEntityUnderMouse(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);

  // Check units
  for (const u of G.units) {
    if (!u.mesh.visible) continue;
    const hits = raycaster.intersectObjects(u.mesh.children, true);
    if (hits.length > 0) return u;
  }

  // Check buildings
  for (const b of G.buildings) {
    if (!b.mesh.visible) continue;
    const hits = raycaster.intersectObjects(b.mesh.children, true);
    if (hits.length > 0) return b;
  }

  // Check crystal deposits
  for (const res of G.resources) {
    const hits = raycaster.intersectObjects(res.mesh.children, true);
    if (hits.length > 0) return res;
  }

  return null;
}

function clearSelection() {
  for (const u of G.selectedUnits) u.setSelected(false);
  G.selectedUnits = [];
  if (G.selectedBuilding) {
    G.selectedBuilding.setSelected(false);
    G.selectedBuilding = null;
  }
}

function selectUnit(u, add = false) {
  if (!add) clearSelection();
  if (G.selectedBuilding) {
    G.selectedBuilding.setSelected(false);
    G.selectedBuilding = null;
  }
  if (!G.selectedUnits.includes(u)) {
    G.selectedUnits.push(u);
    u.setSelected(true);
  }
}

function selectBuilding(b) {
  clearSelection();
  G.selectedBuilding = b;
  b.setSelected(true);
}

// Double click to select all visible units of same type
window.addEventListener('dblclick', (e) => {
  if (G.selectedUnits.length === 1) {
    const type = G.selectedUnits[0].type;
    for (const u of G.units) {
      if (u.faction === FACTION_PLAYER && u.type === type) {
        selectUnit(u, true);
      }
    }
    updateHudSelection();
  }
});

// Mouse Wheel for Camera Zoom
window.addEventListener('wheel', (e) => {
  camCtrl.zoom(e.deltaY);
}, { passive: true });

// Keyboard Hotkeys
window.addEventListener('keydown', (e) => {
  camCtrl.keys[e.code] = true;

  if (e.code === 'KeyA') {
    // Attack move toggle
    if (G.selectedUnits.length > 0) {
      G.activeCommand = 'attack-move';
      showAlert('Click ground destination to Attack-Move.');
      updateHudSelection();
    }
  } else if (e.code === 'KeyS') {
    // Stop
    for (const u of G.selectedUnits) u.issueStop();
    sfx.playOrder();
  } else if (e.code === 'KeyH') {
    // Hold position
    for (const u of G.selectedUnits) u.issueHold();
    sfx.playOrder();
  } else if (e.code === 'Space') {
    // Jump camera to selected units or base
    if (G.selectedUnits.length > 0) {
      camCtrl.target.set(G.selectedUnits[0].x, 0, G.selectedUnits[0].z);
    } else {
      const hq = G.buildings.find(b => b.faction === FACTION_PLAYER && b.stats.isHQ);
      if (hq) camCtrl.target.set(hq.x, 0, hq.z);
    }
  } else if (e.code === 'Escape') {
    if (G.placingType) cancelPlacement();
    else togglePause();
  } else if (e.code === 'F1') {
    e.preventDefault();
    toggleHelp();
  }

  // Control Groups 1-9
  if (e.code.startsWith('Digit') || e.code.startsWith('Numpad')) {
    const num = e.code.replace('Digit', '').replace('Numpad', '');
    if (num >= '1' && num <= '9') {
      if (e.ctrlKey) {
        // Assign group
        G.controlGroups[num] = [...G.selectedUnits];
        showAlert(`Control Group ${num} assigned (${G.selectedUnits.length} units).`);
      } else {
        // Recall group
        if (G.controlGroups[num] && G.controlGroups[num].length > 0) {
          clearSelection();
          for (const u of G.controlGroups[num]) {
            if (u.hp > 0) selectUnit(u, true);
          }
          updateHudSelection();
        }
      }
    }
  }
});

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

// Prevent default context menu on right click
window.addEventListener('contextmenu', (e) => e.preventDefault());

/* =========================================================================
   HUD, SELECTION & PRODUCTION UI
   ========================================================================= */
const infoTitleEl = document.getElementById('info-title');
const infoCountEl = document.getElementById('info-count');
const infoBodyEl = document.getElementById('info-body');
const queueBarEl = document.getElementById('queue-bar');
const actionGridEl = document.getElementById('action-grid');

function updateTopBar() {
  document.getElementById('res-ore-val').textContent = Math.floor(G.player.ore);
  document.getElementById('res-sup-val').textContent = `${G.player.usedSupply} / ${G.player.maxSupply}`;
}

function showAlert(msg) {
  const box = document.getElementById('alert-box');
  const el = document.createElement('div');
  el.className = 'alert-msg';
  el.textContent = msg;
  box.appendChild(el);
  setTimeout(() => el.remove(), 4000);
}

function updateHudSelection() {
  actionGridEl.innerHTML = '';
  queueBarEl.innerHTML = '';

  // Case 1: Multiple Units Selected
  if (G.selectedUnits.length > 1) {
    infoTitleEl.textContent = 'Expedition Strike Force';
    infoCountEl.textContent = `${G.selectedUnits.length} Units`;

    // Group counts
    const counts = {};
    for (const u of G.selectedUnits) counts[u.stats.name] = (counts[u.stats.name] || 0) + 1;
    let desc = '<div style="margin-bottom:6px;">';
    for (const [k, v] of Object.entries(counts)) {
      desc += `<span><strong>${v}x</strong> ${k} &nbsp;&nbsp;</span>`;
    }
    desc += '</div><div style="font-size:11px; color:#8ba7c7;">Command whole group: Attack-Move, Stop, or Hold.</div>';
    infoBodyEl.innerHTML = desc;

    // Standard Combat Commands
    addCommandButton('A', 'Attack-Move', () => { G.activeCommand = 'attack-move'; showAlert('Click destination to Attack-Move'); }, '', G.activeCommand === 'attack-move');
    addCommandButton('S', 'Stop', () => { for (const u of G.selectedUnits) u.issueStop(); sfx.playOrder(); });
    addCommandButton('H', 'Hold Pos', () => { for (const u of G.selectedUnits) u.issueHold(); sfx.playOrder(); });
    return;
  }

  // Case 2: Single Unit Selected
  if (G.selectedUnits.length === 1) {
    const u = G.selectedUnits[0];
    infoTitleEl.textContent = u.stats.name;
    infoCountEl.textContent = `HP: ${Math.round(u.hp)} / ${u.maxHp}`;

    let html = `
      <div class="hp-bar-bg"><div class="hp-bar-fill" style="width:${(u.hp/u.maxHp)*100}%;"></div></div>
      <div style="font-size:11px; margin-bottom:4px;">
        <span>Armor: <strong>${u.armor}</strong> (${u.stats.armorType})</span> &nbsp;|&nbsp;
        <span>Damage: <strong>${Math.round(u.damage * (1 + G.player.weaponLevel * 0.2))}</strong></span>
      </div>
      <div style="font-size:11px; color:#8ba7c7; line-height:1.3;">${u.stats.desc}</div>
    `;
    if (u.type === 'worker' && u.carriedOre > 0) {
      html += `<div style="font-size:11px; color:#00e5ff; margin-top:4px;">Carrying: ${u.carriedOre} Ore crystals</div>`;
    }
    infoBodyEl.innerHTML = html;

    // Unit Commands
    if (u.stats.damage > 0) {
      addCommandButton('A', 'Attack-Move', () => { G.activeCommand = 'attack-move'; showAlert('Click destination to Attack-Move'); }, '', G.activeCommand === 'attack-move');
      addCommandButton('S', 'Stop', () => { u.issueStop(); sfx.playOrder(); });
      addCommandButton('H', 'Hold Pos', () => { u.issueHold(); sfx.playOrder(); });
    }

    // Worker Build Menu
    if (u.type === 'worker') {
      addCommandButton('B', 'Build HQ', () => startPlacement('hq'), `${BUILDING_STATS.hq.cost} Ore`, false, G.player.ore < BUILDING_STATS.hq.cost);
      addCommandButton('R', 'Reactor', () => startPlacement('reactor'), `${BUILDING_STATS.reactor.cost} Ore`, false, G.player.ore < BUILDING_STATS.reactor.cost);
      addCommandButton('K', 'Barracks', () => startPlacement('barracks'), `${BUILDING_STATS.barracks.cost} Ore`, false, G.player.ore < BUILDING_STATS.barracks.cost);
      addCommandButton('F', 'Foundry', () => startPlacement('foundry'), `${BUILDING_STATS.foundry.cost} Ore`, false, G.player.ore < BUILDING_STATS.foundry.cost);
      addCommandButton('D', 'Aerodrome', () => startPlacement('aerodrome'), `${BUILDING_STATS.aerodrome.cost} Ore`, false, G.player.ore < BUILDING_STATS.aerodrome.cost);
      addCommandButton('T', 'Turret', () => startPlacement('turret'), `${BUILDING_STATS.turret.cost} Ore`, false, G.player.ore < BUILDING_STATS.turret.cost);
      addCommandButton('L', 'Tech Bay', () => startPlacement('techlab'), `${BUILDING_STATS.techlab.cost} Ore`, false, G.player.ore < BUILDING_STATS.techlab.cost);
    }
    return;
  }

  // Case 3: Structure Selected
  if (G.selectedBuilding) {
    const b = G.selectedBuilding;
    infoTitleEl.textContent = b.stats.name;
    infoCountEl.textContent = b.completed ? `HP: ${Math.round(b.hp)} / ${b.maxHp}` : `Under Construction (${Math.floor(b.buildProgress * 100)}%)`;

    let html = `
      <div class="hp-bar-bg"><div class="hp-bar-fill" style="width:${(b.hp/b.maxHp)*100}%;"></div></div>
      <div style="font-size:11px; margin-bottom:4px;">
        <span>Armor: <strong>${b.armor}</strong></span> &nbsp;|&nbsp;
        <span>Status: <strong>${b.completed ? 'Operational' : 'Building'}</strong></span>
      </div>
      <div style="font-size:11px; color:#8ba7c7; line-height:1.3;">${b.stats.desc}</div>
    `;
    infoBodyEl.innerHTML = html;

    // Show Production / Training Buttons
    if (b.completed && b.faction === FACTION_PLAYER) {
      if (b.stats.canTrain) {
        for (const unitId of b.stats.canTrain) {
          const ustat = UNIT_STATS[unitId];
          const canAfford = G.player.ore >= ustat.cost && (G.player.usedSupply + ustat.supply <= G.player.maxSupply);
          addCommandButton(
            unitId[0].toUpperCase(),
            `Train ${ustat.name.split(' ')[0]}`,
            () => b.queueProduction(unitId, false),
            `${ustat.cost} Ore (${ustat.supply}s)`,
            false,
            !canAfford
          );
        }
      }
      if (b.stats.canResearch) {
        for (const upId of b.stats.canResearch) {
          const upStat = UPGRADE_STATS[upId];
          const canAfford = G.player.ore >= upStat.cost;
          addCommandButton(
            'U',
            upStat.name,
            () => b.queueProduction(upId, true),
            `${upStat.cost} Ore`,
            false,
            !canAfford
          );
        }
      }

      // Render Queue slots
      b.queue.forEach((item, idx) => {
        const slot = document.createElement('div');
        slot.className = `queue-slot ${idx === 0 ? 'active' : ''}`;
        slot.title = 'Click to cancel and refund';
        slot.textContent = item.isResearch ? 'UP' : item.id[0].toUpperCase();
        slot.onclick = () => b.cancelQueue(idx);
        queueBarEl.appendChild(slot);
      });
    }
    return;
  }

  // Case 4: No selection
  infoTitleEl.textContent = 'Tactical Overview';
  infoCountEl.textContent = '';
  infoBodyEl.innerHTML = `
    <p style="margin-bottom:6px;">Select units or buildings to issue orders. Drag a box to command squads.</p>
    <p style="color:#8198b5;">Hint: Construct <strong>Solar Reactors</strong> to expand your unit supply ceiling.</p>
  `;
}

function addCommandButton(key, label, onClick, costText = '', isActive = false, isDisabled = false) {
  const btn = document.createElement('button');
  btn.className = `cmd-btn ${isActive ? 'active' : ''}`;
  btn.disabled = isDisabled;
  btn.innerHTML = `<span class="key">${key}</span><span>${label}</span>${costText ? `<span class="cost">${costText}</span>` : ''}`;
  btn.onclick = () => {
    sfx.playClick();
    onClick();
  };
  actionGridEl.appendChild(btn);
}

/* =========================================================================
   MINIMAP RENDERING & INTERACTION
   ========================================================================= */
const minimapCanvas = document.getElementById('minimap');
const minimapCtx = minimapCanvas.getContext('2d');

function updateMinimap() {
  const w = minimapCanvas.width;
  const h = minimapCanvas.height;

  // Background
  minimapCtx.fillStyle = '#060c18';
  minimapCtx.fillRect(0, 0, w, h);

  // Draw Fog / Explored Terrain overlay on minimap
  const mapImg = minimapCtx.createImageData(w, h);
  const data = mapImg.data;
  const scale = FOG_RES / w;

  for (let y = 0; y < h; y++) {
    const gy = Math.floor(y * scale);
    const rowOffset = gy * FOG_RES;
    for (let x = 0; x < w; x++) {
      const gx = Math.floor(x * scale);
      const val = G.fogGrid[rowOffset + gx];
      const p = (y * w + x) * 4;

      if (val === 0) {
        // Unexplored
        data[p] = 3;
        data[p + 1] = 6;
        data[p + 2] = 12;
        data[p + 3] = 255;
      } else if (val === 1) {
        // Explored / Shroud
        data[p] = 18;
        data[p + 1] = 28;
        data[p + 2] = 40;
        data[p + 3] = 255;
      } else {
        // Visible
        data[p] = 38;
        data[p + 1] = 48;
        data[p + 2] = 62;
        data[p + 3] = 255;
      }
    }
  }
  minimapCtx.putImageData(mapImg, 0, 0);

  // World to Minimap coords conversion
  const toMinimap = (wx, wz) => {
    return {
      mx: ((wx + MAP_SIZE / 2) / MAP_SIZE) * w,
      my: ((wz + MAP_SIZE / 2) / MAP_SIZE) * h
    };
  };

  // Draw Resources if explored
  for (const res of G.resources) {
    const { gx, gz } = worldToFogGrid(res.x, res.z);
    if (G.fogGrid[gz * FOG_RES + gx] > 0 && res.amount > 0) {
      const { mx, my } = toMinimap(res.x, res.z);
      minimapCtx.fillStyle = '#00e5ff';
      minimapCtx.fillRect(mx - 1.5, my - 1.5, 3, 3);
    }
  }

  // Draw Buildings
  for (const b of G.buildings) {
    const { gx, gz } = worldToFogGrid(b.x, b.z);
    const fogState = G.fogGrid[gz * FOG_RES + gx];
    if (b.faction === FACTION_PLAYER || fogState === 2 || (fogState === 1 && b.lastSeen)) {
      const { mx, my } = toMinimap(b.x, b.z);
      minimapCtx.fillStyle = b.faction === FACTION_PLAYER ? COLOR_PLAYER_HEX : COLOR_ENEMY_HEX;
      const size = Math.max(3, (b.stats.size / MAP_SIZE) * w);
      minimapCtx.fillRect(mx - size / 2, my - size / 2, size, size);
    }
  }

  // Draw Units
  for (const u of G.units) {
    if (u.faction === FACTION_PLAYER) {
      const { mx, my } = toMinimap(u.x, u.z);
      minimapCtx.fillStyle = '#90caf9';
      minimapCtx.fillRect(mx - 1, my - 1, 2.5, 2.5);
    } else {
      const { gx, gz } = worldToFogGrid(u.x, u.z);
      if (G.fogGrid[gz * FOG_RES + gx] === 2) {
        const { mx, my } = toMinimap(u.x, u.z);
        minimapCtx.fillStyle = '#ff5252';
        minimapCtx.fillRect(mx - 1.5, my - 1.5, 3, 3);
      }
    }
  }

  // Draw Camera Frustum Viewport Rectangle
  const { mx: cx, my: cy } = toMinimap(camCtrl.target.x, camCtrl.target.z);
  const viewW = (camCtrl.distance / MAP_SIZE) * w * 1.4;
  const viewH = (camCtrl.distance / MAP_SIZE) * h * 0.9;
  minimapCtx.strokeStyle = '#ffffff';
  minimapCtx.lineWidth = 1.2;
  minimapCtx.strokeRect(cx - viewW / 2, cy - viewH / 2, viewW, viewH);
}

// Minimap Clicking & Panning
function handleMinimapClick(e) {
  const rect = minimapCanvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;
  const wx = (mx / minimapCanvas.width) * MAP_SIZE - MAP_SIZE / 2;
  const wz = (my / minimapCanvas.height) * MAP_SIZE - MAP_SIZE / 2;

  if (e.buttons === 1) {
    // Left-click moves camera
    camCtrl.target.set(wx, 0, wz);
    camCtrl.updatePosition();
  } else if (e.button === 2) {
    // Right-click issues move to army
    for (const u of G.selectedUnits) {
      u.issueMove(wx, wz);
    }
    sfx.playOrder();
  }
}
minimapCanvas.addEventListener('mousedown', handleMinimapClick);
minimapCanvas.addEventListener('mousemove', (e) => {
  if (e.buttons === 1) handleMinimapClick(e);
});

/* =========================================================================
   MODAL DIALOGS & TOP CONTROLS
   ========================================================================= */
const modalScreen = document.getElementById('modal-screen');
const modalContent = document.getElementById('modal-content');

document.getElementById('btn-start-game').onclick = () => {
  sfx.init();
  modalScreen.style.display = 'none';
  sfx.playClick();
};

document.getElementById('btn-sound').onclick = (e) => {
  sfx.enabled = !sfx.enabled;
  e.target.textContent = `AUDIO: ${sfx.enabled ? 'ON' : 'OFF'}`;
  sfx.playClick();
};

document.getElementById('btn-pause').onclick = togglePause;
document.getElementById('btn-help').onclick = toggleHelp;

function togglePause() {
  if (G.gameOver) return;
  G.paused = !G.paused;
  if (G.paused) {
    modalContent.innerHTML = `
      <h1>OPERATION PAUSED</h1>
      <p>Mission is currently suspended. Adjust strategic plans.</p>
      <button class="modal-btn" onclick="togglePause()">RESUME MISSION</button>
      <button class="modal-btn secondary" onclick="restartMatch()">RESTART MISSION</button>
    `;
    modalScreen.style.display = 'flex';
  } else {
    modalScreen.style.display = 'none';
  }
}

function toggleHelp() {
  const isOpen = modalScreen.style.display === 'flex';
  if (isOpen) {
    modalScreen.style.display = 'none';
  } else {
    modalContent.innerHTML = `
      <h1>TACTICAL FIELD GUIDE</h1>
      <h2>COMMAND ESSENTIALS</h2>
      <ul>
        <li><strong>Left-Click / Box-Drag:</strong> Select units or structures. Double click a unit to select all visible matching units.</li>
        <li><strong>Right-Click:</strong> Context command. Move units, attack enemies, or order Mining Rovers to harvest crystal deposits.</li>
        <li><strong>[A] + Click:</strong> Attack-Move. Units move forward engaging any hostiles encountered along the route.</li>
        <li><strong>W/A/S/D or Mouse Pan:</strong> Pan camera. Wheel: Zoom. Q/E: Rotate. Space: Center on base.</li>
      </ul>
      <h2>COMBINED-ARMS COUNTERS</h2>
      <ul>
        <li><strong>Scout Raider:</strong> Fast light buggy; slices light infantry & workers.</li>
        <li><strong>Rifle Marine:</strong> Rapid fire kinetic; versatile anti-air and anti-buggy.</li>
        <li><strong>Siege Titan:</strong> Heavy armor, twin plasma; pulverizes ground vehicles & defense bases.</li>
        <li><strong>Valkyrie Gunship:</strong> High mobility VTOL; bypasses terrain and cliffs; countered by Marines & Turrets.</li>
      </ul>
      <button class="modal-btn" onclick="toggleHelp()">RETURN TO OPERATION</button>
    `;
    modalScreen.style.display = 'flex';
  }
}

/* =========================================================================
   MISSION INITIALIZATION & RESET
   ========================================================================= */
function setupMatch() {
  // Clear entities
  for (const u of G.units) { scene.remove(u.mesh); scene.remove(u.hpBar); }
  for (const b of G.buildings) { scene.remove(b.mesh); scene.remove(b.hpBar); }
  for (const p of G.projectiles) scene.remove(p.mesh);
  for (const pt of G.particles) scene.remove(pt.mesh);

  G.units = [];
  G.buildings = [];
  G.projectiles = [];
  G.particles = [];
  G.selectedUnits = [];
  G.selectedBuilding = null;
  G.controlGroups = {};
  G.time = 0;
  G.paused = false;
  G.gameOver = false;
  G.victory = false;

  // Reset Fog
  G.fogGrid.fill(0);
  G.enemyFogGrid.fill(0);

  // Player Starting Base at (-80, -80)
  G.player = {
    ore: 340,
    usedSupply: 4,
    maxSupply: 0,
    weaponLevel: 0,
    armorLevel: 0,
    researched: {}
  };
  const playerHQ = new Building('hq', FACTION_PLAYER, -80, -80, true);
  G.buildings.push(playerHQ);

  // 4 Starting Workers
  G.units.push(new Unit('worker', FACTION_PLAYER, -84, -76));
  G.units.push(new Unit('worker', FACTION_PLAYER, -82, -74));
  G.units.push(new Unit('worker', FACTION_PLAYER, -78, -76));
  G.units.push(new Unit('worker', FACTION_PLAYER, -76, -74));

  // Enemy Starting Base at (80, 80)
  G.enemy = {
    ore: 340,
    usedSupply: 4,
    maxSupply: 0,
    weaponLevel: 0,
    armorLevel: 0,
    researched: {},
    strategyTimer: 0,
    lastAttackTime: 0,
    attackWave: 1,
    rallyPoint: new THREE.Vector3(75, 0, 75)
  };
  const enemyHQ = new Building('hq', FACTION_ENEMY, 80, 80, true);
  G.buildings.push(enemyHQ);

  // 4 Enemy Starting Workers
  G.units.push(new Unit('worker', FACTION_ENEMY, 84, 76));
  G.units.push(new Unit('worker', FACTION_ENEMY, 82, 74));
  G.units.push(new Unit('worker', FACTION_ENEMY, 78, 76));
  G.units.push(new Unit('worker', FACTION_ENEMY, 76, 74));

  // Center Camera on Player HQ
  camCtrl.target.set(-80, 0, -80);
  camCtrl.updatePosition();

  updateTopBar();
  updateHudSelection();
}

function restartMatch() {
  modalScreen.style.display = 'none';
  setupMatch();
}

/* =========================================================================
   MAIN GAME LOOP
   ========================================================================= */
let lastFrameTime = performance.now();
let fogTimer = 0;

function gameLoop(now) {
  requestAnimationFrame(gameLoop);

  const dt = Math.min((now - lastFrameTime) / 1000, 0.1);
  lastFrameTime = now;

  if (!G.paused && !G.gameOver) {
    G.time += dt;

    // Update mission timer
    const m = Math.floor(G.time / 60);
    const s = Math.floor(G.time % 60);
    document.getElementById('game-timer').textContent = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;

    // Camera movement
    camCtrl.update(dt);

    // Update simulation entities
    for (let i = G.units.length - 1; i >= 0; i--) {
      G.units[i].update(dt);
    }
    for (let i = G.buildings.length - 1; i >= 0; i--) {
      G.buildings[i].update(dt);
    }
    updateProjectiles(dt);
    updateParticles(dt);

    // AI Simulation
    updateAI(dt);

    // Periodic Fog of war calculation & Minimap (runs every 0.12s for speed)
    fogTimer += dt;
    if (fogTimer >= 0.12) {
      fogTimer = 0;
      updateFogOfWar();
      updateMinimap();
    }
  }

  // Render 3D Scene
  renderer.render(scene, camera);
}

// Window Resize Handling
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

// Initial boot
setupMatch();
requestAnimationFrame(gameLoop);

</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.8-flash","provider":"openrouter","persona":"full","sessionId":"21da9ca4-083f-4a6c-a51c-295dfbb9da6d","tokensIn":2119133,"tokensOut":45588,"tokensTotal":2164721,"cost":0.5034743249999999,"turns":33,"toolCalls":32,"failedToolCalls":0,"timestamp":"2026-10-01T15:10:57.234Z"} -->