← Gorillaz results

Gorillaz

solar mini4 · full

Initial view of Gorillaz

Browser check: Artifact runtime error. 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 52,566 bytes · SHA-256 8d7f00411d43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gorillaz — Artillery Showdown</title>
<style>
  :root {
    --bg: #0d0d1a;
    --panel: #16162a;
    --panel2: #1f1f3a;
    --text: #f0f0f5;
    --text-dim: #8888aa;
    --accent: #0ff;
    --accent2: #f5e05a;
    --red: #ff4d4d;
    --blue: #4d4dff;
    --green: #4dff88;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    width: 100%; height: 100%;
    background: #000;
    font-family: "Courier New", monospace;
    overflow: hidden;
    user-select: none;
  }
  #app {
    display: flex; justify-content: center; align-items: center;
    width: 100%; height: 100%;
    padding: 12px;
  }
  #game-container {
    position: relative;
    width: 960px; max-width: 100%;
    aspect-ratio: 16/9;
    background: #0d0d1a;
    border: 4px solid #333;
    border-radius: 6px;
    box-shadow: 0 0 0 2px #000, 0 0 40px rgba(0,0,0,0.8);
    overflow: hidden;
  }
  #canvas {
    display: block;
    width: 100%; height: 100%;
    border-radius: 2px;
    background: #1a1a3a;
  }
  /* ---- HUD ---- */
  #hud {
    position: absolute; inset: 0;
    pointer-events: none;
  }
  .hud-top, .hud-bottom {
    position: absolute;
    left: 14px; right: 14px;
  }
  .hud-top { top: 10px; display: flex; justify-content: space-between; align-items: center; }
  .wind-box {
    display: flex; align-items: center; gap: 8px;
    background: rgba(0,20,40,0.85);
    border: 2px solid #3a6a8a;
    border-radius: 6px;
    padding: 6px 12px;
    color: var(--text);
    font-size: 15px;
    letter-spacing: 1px;
  }
  .wind-arrow {
    font-size: 22px; font-weight: bold;
    color: var(--accent2);
    transition: transform 0.3s;
  }
  .wind-box.dir-right .wind-arrow { transform: scaleX(1); }
  .wind-box.dir-left .wind-arrow { transform: scaleX(-1); }
  .wind-value { font-weight: bold; color: var(--accent2); }
  .turn-box {
    display: flex; align-items: center; gap: 8px;
    background: rgba(0,20,40,0.85);
    border: 2px solid #6a4a3a;
    border-radius: 6px;
    padding: 6px 14px;
    color: var(--text);
    font-size: 16px;
    font-weight: bold;
  }
  .turn-box .turn-label { color: var(--text-dim); font-size: 12px; letter-spacing: 2px; }
  .turn-box .turn-text { color: var(--accent2); }
  .score-box {
    display: flex; gap: 18px;
    color: var(--text);
    font-size: 16px;
    font-weight: bold;
  }
  .score-item { display: flex; align-items: center; gap: 6px; }
  .score-item .dot { width: 10px; height: 10px; border-radius: 50%; }
  .p1-dot { background: var(--red); }
  .p2-dot { background: var(--blue); }
  .score-item .num { color: var(--accent2); }
  .hud-bottom {
    bottom: 10px; display: flex; justify-content: space-between; align-items: center;
  }
  .aim-box {
    display: flex; flex-direction: column; align-items: center;
    background: rgba(0,20,40,0.85);
    border: 2px solid #6a8a3a;
    border-radius: 6px;
    padding: 6px 14px;
    color: var(--text);
    font-size: 15px;
  }
  .aim-box .aim-label { color: var(--text-dim); font-size: 12px; letter-spacing: 2px; }
  .aim-box .aim-value { color: var(--accent2); font-size: 20px; font-weight: bold; }
  /* ---- Overlays ---- */
  .overlay {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    pointer-events: auto;
    opacity: 0; visibility: hidden;
    transition: opacity 0.35s, visibility 0.35s;
  }
  .overlay.active { opacity: 1; visibility: visible; }
  .overlay-bg {
    position: absolute; inset: 0;
    background: rgba(0,0,0,0.65);
    backdrop-filter: blur(4px);
  }
  .panel {
    position: relative;
    background: linear-gradient(180deg, #1a1a2e, #12121e);
    border: 3px solid #3a3a5a;
    border-radius: 10px;
    padding: 30px 40px;
    text-align: center;
    max-width: 560px;
    width: 100%;
    box-shadow: 0 0 60px rgba(0,0,0,0.6);
  }
  .panel h1 {
    font-size: 52px;
    font-weight: 900;
    letter-spacing: 6px;
    color: var(--text);
  }
  .panel h1 .z { color: var(--accent2); }
  .panel .sub {
    color: var(--text-dim);
    font-size: 16px;
    margin-top: 6px;
    letter-spacing: 2px;
  }
  .panel .title-badge {
    margin-top: 12px;
    display: inline-block;
    background: rgba(0,20,40,0.8);
    border: 2px solid #3a6a8a;
    border-radius: 6px;
    padding: 4px 14px;
    font-size: 13px;
    color: var(--accent);
    letter-spacing: 2px;
  }
  .btn {
    margin-top: 18px;
    padding: 12px 32px;
    font-family: "Courier New", monospace;
    font-size: 18px;
    font-weight: bold;
    letter-spacing: 3px;
    border: 3px solid var(--accent);
    border-radius: 6px;
    background: linear-gradient(180deg, #1a3a5a, #102030);
    color: var(--text);
    cursor: pointer;
    pointer-events: auto;
    transition: transform 0.1s, background 0.2s;
  }
  .btn:hover { background: linear-gradient(180deg, #2a4a6a, #1a2a3a); }
  .btn:active { transform: scale(0.97); }
  .btn.red { border-color: var(--red); background: linear-gradient(180deg, #3a1a1a, #1a0a0a); }
  .btn.red:hover { background: linear-gradient(180deg, #4a1a1a, #2a0a0a); }
  .btn.blue { border-color: var(--blue); background: linear-gradient(180deg, #1a1a3a, #0a0a20); }
  .btn.blue:hover { background: linear-gradient(180deg, #2a2a4a, #1a1a30); }
  .btn.green { border-color: var(--green); background: linear-gradient(180deg, #1a2a1a, #0a100a); }
  .btn.green:hover { background: linear-gradient(180deg, #2a3a2a, #1a201a); }
  .round-end .winner { font-size: 40px; font-weight: bold; color: var(--accent2); }
  .round-end .round-label { font-size: 14px; color: var(--text-dim); letter-spacing: 3px; }
  .round-end .match-over .winner { font-size: 48px; }
  .round-end .match-over .round-label { font-size: 16px; }
  .round-end .sub { font-size: 18px; }
  .round-end .btn { margin-top: 24px; }
  /* ---- Controls panel ---- */
  #controlsPanel {
    position: absolute; bottom: 12px; left: 12px; right: 12px;
    pointer-events: auto;
    display: none;
  }
  #controlsPanel.active { display: block; }
  .controls-header {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 12px;
  }
  .player-selector { display: flex; gap: 8px; }
  .player-btn {
    padding: 6px 16px; font-family: "Courier New", monospace;
    font-size: 14px; font-weight: bold; letter-spacing: 2px;
    border: 2px solid #555; border-radius: 6px; background: #1a1a2e;
    color: var(--text-dim); cursor: pointer;
  }
  .player-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(0,20,40,0.8); }
  .ctrl-group {
    display: flex; align-items: center; gap: 12px; margin-bottom: 10px;
  }
  .ctrl-group label { color: var(--text-dim); font-size: 13px; letter-spacing: 1px; }
  .ctrl-group .value {
    font-family: "Courier New", monospace; font-size: 22px; font-weight: bold;
    color: var(--accent2); min-width: 60px; text-align: center;
  }
  .ctrl-group .unit { font-size: 12px; color: var(--text-dim); }
  .ctrl-group .btn-sm {
    width: 44px; height: 44px; font-size: 20px; font-weight: bold;
    border: 2px solid #555; border-radius: 6px; background: #1a1a2e; color: var(--text);
    cursor: pointer; font-family: "Courier New", monospace;
  }
  .ctrl-group .btn-sm:hover { border-color: var(--accent); }
  #fireBtn {
    padding: 14px 40px; font-size: 20px; font-weight: bold; letter-spacing: 4px;
    border: 3px solid var(--accent2); border-radius: 8px; background: linear-gradient(180deg, #3a2a0a, #2a1a00);
    color: var(--accent2); cursor: pointer; font-family: "Courier New", monospace;
    margin-top: 6px;
  }
  #fireBtn:hover { background: linear-gradient(180deg, #4a3a1a, #3a2a0a); }
  #fireBtn:active { transform: scale(0.97); }
  .switch-player {
    margin-top: 10px; padding: 6px 14px; font-size: 12px; font-family: "Courier New", monospace;
    border: 2px dashed #555; border-radius: 6px; color: var(--text-dim);
    cursor: pointer; background: transparent;
  }
  .switch-player:hover { border-color: var(--accent); color: var(--accent); }
  /* ---- Settings modal ---- */
  .modal {
    position: absolute; inset: 0;
    display: none; align-items: center; justify-content: center;
    pointer-events: auto;
    z-index: 10;
  }
  .modal.active { display: flex; }
  .modal-bg {
    position: absolute; inset: 0;
    background: rgba(0,0,0,0.7);
    backdrop-filter: blur(6px);
    cursor: pointer;
  }
  .modal-panel {
    position: relative;
    background: linear-gradient(180deg, #1c1c30, #141424);
    border: 3px solid #4a4a6a;
    border-radius: 10px;
    padding: 24px 28px 20px;
    width: 100%; max-width: 640px;
    max-height: 90vh; overflow-y: auto;
    box-shadow: 0 0 60px rgba(0,0,0,0.7);
  }
  .modal-panel h2 {
    font-size: 22px; font-weight: 900; letter-spacing: 2px;
    color: var(--text); margin-bottom: 4px;
  }
  .modal-panel .sub { color: var(--text-dim); font-size: 13px; letter-spacing: 1px; margin-bottom: 14px; }
  .live-badge {
    display: inline-block; background: #2a4a1a; border: 2px solid #4a8a2a;
    color: #8fca4a; border-radius: 6px; padding: 4px 12px;
    font-size: 12px; font-weight: bold; letter-spacing: 2px;
    margin-bottom: 14px; text-align: center;
  }
  .setting {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; padding: 10px 0; border-bottom: 1px solid #2a2a40;
  }
  .setting:last-child { border-bottom: none; }
  .setting .name { color: var(--text); font-size: 14px; }
  .setting .name .desc { color: var(--text-dim); font-size: 12px; }
  .setting .value-row { display: flex; align-items: center; gap: 8px; }
  .setting .value {
    font-family: "Courier New", monospace; font-size: 18px; font-weight: bold;
    color: var(--accent2); min-width: 70px; text-align: right;
  }
  .setting .value .unit { font-size: 12px; color: var(--text-dim); font-weight: normal; }
  .setting .spinner {
    display: flex; gap: 4px;
  }
  .setting .spinner button {
    width: 32px; height: 32px; font-size: 18px; font-weight: bold;
    border: 2px solid #555; border-radius: 6px; background: #1a1a2e;
    color: var(--text); cursor: pointer; font-family: "Courier New", monospace;
  }
  .setting .spinner button:hover { border-color: var(--accent); }
  .setting .input {
    font-family: "Courier New", monospace; font-size: 16px; font-weight: bold;
    color: var(--text); border: 2px solid #4a4a6a; border-radius: 6px;
    padding: 4px 8px; width: 90px; background: #0d0d1a;
  }
  .setting .input:focus { outline: none; border-color: var(--accent); }
  .setting .toggle-row { display: flex; align-items: center; gap: 8px; }
  .setting .toggle {
    display: flex; align-items: center; gap: 6px;
    color: var(--text); font-size: 13px; cursor: pointer;
    user-select: none;
  }
  .setting .toggle .dot {
    width: 18px; height: 18px; border-radius: 50%; border: 2px solid #555;
    background: #1a1a2e; position: relative;
  }
  .setting .toggle.on .dot { border-color: var(--green); background: #1a3a1a; }
  .setting .toggle.off .dot { border-color: var(--red); background: #3a1a1a; }
  .setting .sel {
    font-family: "Courier New", monospace; font-size: 16px; font-weight: bold;
    color: var(--text); border: 2px solid #4a4a6a; border-radius: 6px;
    padding: 4px 8px; background: #0d0d1a;
  }
  .modal-close {
    position: absolute; top: 12px; right: 14px;
    width: 36px; height: 36px; font-size: 22px; font-weight: bold;
    border: 2px solid #555; border-radius: 6px; background: #1a1a2e;
    color: var(--text); cursor: pointer; font-family: "Courier New", monospace;
  }
  .modal-close:hover { border-color: var(--accent); }
  .settings-list { margin-top: 16px; }
  /* ---- Canvas decorations (wind particles) ---- */
  .wind-canvas {
    position: absolute; inset: 0; pointer-events: none;
    overflow: hidden;
  }
</style>
</head>
<body>
<div id="app">
  <div id="game-container">
    <canvas id="canvas" width="960" height="540"></canvas>

    <div id="hud">
      <div class="hud-top">
        <div class="wind-box" id="windBox">
          <span class="wind-label">WIND</span>
          <span class="wind-arrow" id="windArrow">—</span>
          <span class="wind-value" id="windValue">0</span>
          <span class="wind-unit">px/s</span>
        </div>
        <div class="turn-box">
          <span class="turn-label">TURN</span>
          <span class="turn-text" id="turnText">Player 1</span>
        </div>
        <div class="score-box">
          <span class="score-item"><span class="dot p1-dot"></span><span class="num" id="score1">0</span></span>
          <span class="score-item"><span class="dot p2-dot"></span><span class="num" id="score2">0</span></span>
        </div>
      </div>
      <div class="hud-bottom">
        <div class="aim-box">
          <span class="aim-label">ANGLE</span>
          <span class="aim-value" id="angleValue">45°</span>
        </div>
        <div class="aim-box">
          <span class="aim-label">POWER</span>
          <span class="aim-value" id="velocityValue">50</span>
        </div>
      </div>
    </div>

    <!-- Title screen -->
    <div class="overlay" id="titleScreen">
      <div class="overlay-bg"></div>
      <div class="panel">
        <h1>GORILLA<span class="z">Z</span></h1>
        <p class="sub">ARTILLERY SHOWDOWN</p>
        <div class="title-badge">RETRO ARCADE · 2-PLAYER</div>
        <button class="btn" id="newMatchBtn">NEW MATCH</button>
      </div>
    </div>

    <!-- Round end screen -->
    <div class="overlay round-end" id="roundEndScreen">
      <div class="overlay-bg"></div>
      <div class="panel">
        <div class="winner" id="roundEndText">Player 1 Wins!</div>
        <p class="round-label" id="roundEndSub">Round <span id="roundEndRound">1</span> of 3</p>
        <p class="sub" id="roundEndScore">P1: 1 · P2: 0</p>
        <button class="btn green" id="rematchBtn">REMATCH</button>
      </div>
    </div>

    <!-- Controls panel -->
    <div id="controlsPanel">
      <div class="controls-header">
        <div class="player-selector">
          <button class="player-btn active" data-player="1" id="p1Selector">Player 1</button>
          <button class="player-btn" data-player="2" id="p2Selector">Player 2</button>
        </div>
        <span class="switch-player" id="switchPlayerBtn">← Switch Player →</span>
      </div>
      <div class="ctrl-group">
        <label>ANGLE</label>
        <div class="spinner">
          <button class="btn-sm" id="angleMinus">−</button>
          <span class="value" id="angleValueCtrl">45</span>
          <button class="btn-sm" id="anglePlus">+</button>
        </div>
        <span class="unit">°</span>
      </div>
      <div class="ctrl-group">
        <label>POWER</label>
        <div class="spinner">
          <button class="btn-sm" id="velocityMinus">−</button>
          <span class="value" id="velocityValueCtrl">50</span>
          <button class="btn-sm" id="velocityPlus">+</button>
        </div>
        <span class="unit">/100</span>
      </div>
      <button id="fireBtn">FIRE</button>
    </div>

    <!-- Settings modal -->
    <div class="modal" id="settingsModal">
      <div class="modal-bg" id="settingsBg"></div>
      <div class="modal-panel">
        <button class="modal-close" id="settingsClose">✕</button>
        <h2>⚙ LIVE TUNING</h2>
        <p class="sub">Edit physics &amp; game constants — changes apply on the next turn.</p>
        <div class="live-badge" id="liveBadge">LIVE TUNING ACTIVE</div>
        <div class="settings-list" id="settingsList"></div>
      </div>
    </div>
  </div>
</div>

<script>
"use strict";

// ─────────────────────────────────────────────
//  CONFIG  (defaults — overridable via live tuning)
// ─────────────────────────────────────────────
const CFG = {
  blastRadius: 60,        // px
  gravity: 400,           // px/s^2
  windVariance: 150,      // max |wind| px/s
  windShift: true,        // wind re-randomizes each turn
  buildingCount: 10,      // 8-14
  buildingHeightMin: 40,
  buildingHeightMax: 300,
  velocityMin: 1,
  velocityMax: 100,
  damageThreshold: 'inside', // 'inside' | 'falloff'
};

// ─────────────────────────────────────────────
//  STATE
// ─────────────────────────────────────────────
const S = {
  phase: 'title',          // title | playing | exploding | roundEnd | settings
  activePlayer: 1,
  angle: 45,
  velocity: 50,
  wind: 0,
  buildings: [],
  gorillas: [],            // 2 gorillas
  projectile: null,
  explosion: null,
  score: [0, 0],
  round: 0,
  seed: 0,
  rng: null,
  settingsOpen: false,
  settingsDirty: false,
  windHistory: [],
};

// ─────────────────────────────────────────────
//  SEEDED RNG (mulberry32)
// ─────────────────────────────────────────────
function mulberry32(a) {
  return function() {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

// ─────────────────────────────────────────────
//  DOM REFS
// ─────────────────────────────────────────────
const $ = id => document.getElementById(id);
const canvas = $('canvas');
const ctx = canvas.getContext('2d');
const W = 960, H = 540;
const GROUND_Y = 480;
const GORILLA_R = 22;
const PROJ_R = 7;
const GRAVITY = CFG.gravity;
let BLAST_RADIUS = CFG.blastRadius;

// ─────────────────────────────────────────────
//  BUILDING PALETTE
// ─────────────────────────────────────────────
const BUILD_COLORS = [
  '#e5e5e5','#d93636','#36d97d','#365ad9','#d9a336',
  '#a336d9','#36d9a3','#d9368a','#e5a336','#365ad9'
];

// ─────────────────────────────────────────────
//  HELPERS
// ─────────────────────────────────────────────
function rand(min, max) { return S.rng() * (max - min) + min; }
function randInt(min, max) { return Math.floor(rand(min, max + 1)); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

// circle vs axis-aligned rect
function circleRect(cx, cy, cr, rx, ry, rw, rh) {
  const nx = clamp(cx, rx, rx + rw);
  const ny = clamp(cy, ry, ry + rh);
  const dx = cx - nx, dy = cy - ny;
  return dx*dx + dy*dy < cr*cr;
}
function circleRectNormal(cx, cy, cr, rx, ry, rw, rh) {
  // returns closest point on rect surface to circle center
  const nx = clamp(cx, rx, rx + rw);
  const ny = clamp(cy, ry, ry + rh);
  return { x: nx, y: ny };
}
function circleCircle(cx1, cy1, r1, cx2, cy2, r2) {
  const dx = cx1 - cx2, dy = cy1 - cy2;
  return dx*dx + dy*dy < (r1 + r2)*(r1 + r2);
}

// ─────────────────────────────────────────────
//  CITY GENERATION
// ─────────────────────────────────────────────
function generateCity(seed) {
  const rng = mulberry32(seed);
  const count = randInt(CFG.buildingCount, CFG.buildingCount);
  const buildings = [];
  let x = 60;
  const maxX = W - 60;
  for (let i = 0; i < count; i++) {
    const w = rand(40, 110);
    const h = rand(CFG.buildingHeightMin, CFG.buildingHeightMax);
    const gap = rand(6, 24);
    const endX = x + w + gap;
    if (endX > maxX) { w = maxX - x; }
    const color = BUILD_COLORS[i % BUILD_COLORS.length];
    buildings.push({ x, y: 0, w, h, color });
    x = endX + gap;
  }
  // ensure at least one building reaches near the right edge
  if (buildings.length > 0 && buildings[buildings.length - 1].x + buildings[buildings.length - 1].w < maxX - 80) {
    buildings[buildings.length - 1].w = maxX - buildings[buildings.length - 1].x;
  }
  return buildings;
}

// ─────────────────────────────────────────────
//  GORILLA PLACEMENT
// ─────────────────────────────────────────────
function placeGorillas(buildings) {
  const left = buildings.filter(b => b.x + b.w / 2 < W / 2 - 40);
  const right = buildings.filter(b => b.x + b.w / 2 > W / 2 + 40);
  const rng = S.rng;
  let leftB = left[randInt(0, left.length - 1)];
  let rightB = right[randInt(0, right.length - 1)];
  // ensure not in blast range of each other
  while (leftB && rightB && Math.abs(leftB.x + leftB.w/2 - (rightB.x + rightB.w/2)) < BLAST_RADIUS + 30) {
    leftB = left[randInt(0, left.length - 1)];
    rightB = right[randInt(0, right.length - 1)];
  }
  return [
    { x: leftB.x + leftB.w/2, y: GROUND_Y - leftB.h, color: '#ff4d4d', alive: true, side: 'left', sway: 0 },
    { x: rightB.x + rightB.w/2, y: GROUND_Y - rightB.h, color: '#4d4dff', alive: true, side: 'right', sway: 0 },
  ];
}


// ─────────────────────────────────────────────
//  MATCH START
// ─────────────────────────────────────────────
function startMatch() {
  S.round = 0;
  S.score = [0, 0];
  S.phase = 'title';
  S.angle = 45;
  S.velocity = 50;
  S.wind = 0;
  S.projectile = null;
  S.explosion = null;
  S.settingsDirty = false;
  S.windHistory = [];
  S.seed = Date.now() ^ (Math.random() * 0xffffffff);
  S.rng = mulberry32(S.seed);
  S.buildings = generateCity(S.seed);
  S.gorillas = placeGorillas(S.buildings);
  $('score1').textContent = '0';
  $('score2').textContent = '0';
  $('angleValue').textContent = '45°';
  $('velocityValue').textContent = '50';
  $('turnText').textContent = 'Player 1';
  $('turnText').style.color = 'var(--accent)';
  setWind(S.wind, false);
  showOverlay('titleScreen');
  updateControls();
}

// ─────────────────────────────────────────────
//  WIND
// ─────────────────────────────────────────────
function setWind(speed, animate) {
  S.wind = speed;
  const arrow = $('windArrow');
  const box = $('windBox');
  box.classList.remove('dir-right', 'dir-left');
  if (speed > 0.5) { arrow.textContent = '▶'; box.classList.add('dir-right'); }
  else if (speed < -0.5) { arrow.textContent = '◀'; box.classList.add('dir-left'); }
  else { arrow.textContent = '—'; }
  $('windValue').textContent = Math.round(speed);
  S.windHistory.push(speed);
  if (S.windHistory.length > 8) S.windHistory.shift();
}

function nextWind() {
  if (CFG.windShift) {
    S.wind = rand(-CFG.windVariance, CFG.windVariance);
  }
  setWind(S.wind, true);
}

// ─────────────────────────────────────────────
//  PROJECTILE
// ─────────────────────────────────────────────
function launchProjectile() {
  const g = S.gorillas[S.activePlayer - 1];
  const rad = S.angle * Math.PI / 180;
  const speed = S.velocity * 6.5;
  S.projectile = {
    x: g.x, y: g.y,
    vx: Math.cos(rad) * speed,
    vy: -Math.sin(rad) * speed,
    radius: PROJ_R,
    trail: [],
    life: 0,
    active: true,
  };
  playSound('launch');
  S.phase = 'playing';
}

function updateProjectile(dt) {
  if (!S.projectile) return;
  const p = S.projectile;
  p.vx += S.wind * dt;
  p.vy += GRAVITY * dt;
  p.x += p.vx * dt;
  p.y += p.vy * dt;
  p.life += dt;
  p.trail.push({ x: p.x, y: p.y, life: 1 });
  if (p.trail.length > 14) p.trail.shift();
  if (p.y + p.radius > GROUND_Y) {
    p.y = GROUND_Y - p.radius;
    triggerExplosion(p.x, p.y, 'ground');
    return;
  }
  for (const b of S.buildings) {
    if (circleRect(p.x, p.y, p.radius, b.x, b.y, b.w, b.h)) {
      const closest = circleRectNormal(p.x, p.y, p.radius, b.x, b.y, b.w, b.h);
      p.x = closest.x; p.y = closest.y;
      triggerExplosion(p.x, p.y, 'building');
      return;
    }
  }
  for (const g of S.gorillas) {
    if (!g.alive) continue;
    if (circleCircle(p.x, p.y, p.radius, g.x, g.y, GORILLA_R)) {
      p.x = g.x; p.y = g.y;
      triggerExplosion(p.x, p.y, 'gorilla', g.side);
      return;
    }
  }
}

function triggerExplosion(x, y, source, gorillaSide) {
  const ex = x, ey = y;
  S.explosion = {
    x: ex, y: ey,
    radius: 0,
    maxRadius: BLAST_RADIUS,
    startTime: performance.now(),
    particles: [],
    scorch: { x: ex, y: ey, life: 0 },
    source: source,
    gorillaSide: gorillaSide,
    active: true,
  };
  const count = 26;
  for (let i = 0; i < count; i++) {
    const a = rand(0, Math.PI * 2);
    const sp = rand(60, 220);
    S.explosion.particles.push({
      x: ex, y: ey,
      vx: Math.cos(a) * sp,
      vy: Math.sin(a) * sp - 60,
      life: rand(0.4, 1.2),
      size: rand(3, 7),
      color: ['#ff6b35','#ffd06b','#ff2d2d','#fff','#ff8a3d'][randInt(0,4)],
    });
  }
  const gorilla = S.gorillas.find(g => g.side === gorillaSide);
  let damage = 0;
  if (gorilla) {
    if (CFG.damageThreshold === 'falloff') {
      const d = Math.hypot(ex - gorilla.x, ey - gorilla.y);
      damage = clamp(1 - d / BLAST_RADIUS, 0, 1);
    } else {
      const d = Math.hypot(ex - gorilla.x, ey - gorilla.y);
      damage = d <= BLAST_RADIUS ? 1 : 0;
    }
  }
  if (damage > 0) {
    gorilla.alive = false;
    playSound('explode');
    showRoundEnd(gorillaSide === 'left' ? 2 : 1);
    return;
  }
  playSound('impact');
  S.explosion.scorch = { x: ex, y: ey, life: 0 };
  S.projectile = null;
  S.phase = 'exploding';
}

// ─────────────────────────────────────────────
//  OVERLAY SHOW/HIDE
// ─────────────────────────────────────────────
function showOverlay(id) {
  document.querySelectorAll('.overlay').forEach(o => o.classList.remove('active'));
  $(id).classList.add('active');
}

// ─────────────────────────────────────────────
//  ROUND END
// ─────────────────────────────────────────────
function showRoundEnd(winner) {
  const p1Wins = S.score[0];
  const p2Wins = S.score[1];
  const totalRounds = 3;
  const matchOver = (winner === 1 && p1Wins >= 2) || (winner === 2 && p2Wins >= 2);
  $('roundEndText').textContent = winner === 1 ? 'Player 1 Wins!' : 'Player 2 Wins!';
  $('roundEndSub').textContent = `Round ${S.round} of ${totalRounds}`;
  $('roundEndScore').textContent = `P1: ${p1Wins} · P2: ${p2Wins}`;
  $('roundEndRound').textContent = S.round;
  $('roundEndScreen').classList.add('match-over');
  $('roundEndScreen').classList.add('active');
}

// ─────────────────────────────────────────────
//  CONTROLS
// ─────────────────────────────────────────────
function updateControls() {
  const p1 = $('p1Selector');
  const p2 = $('p2Selector');
  if (S.activePlayer === 1) {
    p1.classList.add('active');
    p2.classList.remove('active');
    $('switchPlayerBtn').textContent = '← Switch to Player 2 →';
  } else {
    p1.classList.remove('active');
    p2.classList.add('active');
    $('switchPlayerBtn').textContent = '← Switch to Player 1 →';
  }
}

// ─────────────────────────────────────────────
//  INPUT
// ─────────────────────────────────────────────
const KEY = { left: 37, right: 39, up: 38, down: 40, enter: 13, space: 32, m: 77, i: 73, k: 75, j: 74 };
const KeyState = {};
document.addEventListener('keydown', e => {
  KeyState[e.key] = true;
  if (e.key === 'Escape') { if (S.phase === 'settings') closeSettings(); }
  if (e.key === 'm' || e.key === 'M') { openSettings(); }
  if (S.phase === 'playing' && S.activePlayer === 1) {
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') S.angle = clamp(S.angle - 1, 0, 90);
    if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') S.angle = clamp(S.angle + 1, 0, 90);
    if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') S.velocity = clamp(S.velocity + 1, CFG.velocityMin, CFG.velocityMax);
    if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') S.velocity = clamp(S.velocity - 1, CFG.velocityMin, CFG.velocityMax);
    if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fire(); }
  }
  if (S.phase === 'playing' && S.activePlayer === 2) {
    if (e.key === 'j' || e.key === 'J') S.angle = clamp(S.angle - 1, 0, 90);
    if (e.key === 'l' || e.key === 'L') S.angle = clamp(S.angle + 1, 0, 90);
    if (e.key === 'i' || e.key === 'I') S.velocity = clamp(S.velocity + 1, CFG.velocityMin, CFG.velocityMax);
    if (e.key === 'k' || e.key === 'K') S.velocity = clamp(S.velocity - 1, CFG.velocityMin, CFG.velocityMax);
    if (e.key === 'm' || e.key === 'M') { e.preventDefault(); fire(); }
  }
});

// ─────────────────────────────────────────────
//  FIRE
// ─────────────────────────────────────────────
function fire() {
  if (S.projectile || S.phase !== 'playing') return;
  launchProjectile();
}

// ─────────────────────────────────────────────
//  AUDIO (Web Audio API)
// ─────────────────────────────────────────────
const Audio = {
  ctx: null,
  init() {
    if (Audio.ctx) return;
    try { Audio.ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
  },
  play(type) {
    Audio.init();
    if (!Audio.ctx) return;
    const ctx = Audio.ctx;
    const now = ctx.currentTime;
    if (type === 'launch') {
      // short noise whoosh + sine sweep
      const bufSize = ctx.sampleRate * 0.08;
      const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
      const data = buf.getChannelData(0);
      for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i/bufSize, 2);
      const src = ctx.createBufferSource(); src.buffer = buf;
      const filt = ctx.createBiquadFilter(); filt.type = 'highpass'; filt.frequency.value = 400;
      const sw = ctx.createOscillator(); sw.type = 'sine'; sw.frequency.setValueAtTime(250, now);
      sw.frequency.exponentialRampToValueAtTime(700, now + 0.08);
      const g = ctx.createGain(); g.gain.setValueAtTime(0.18, now); g.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
      src.connect(filt); filt.connect(g); g.connect(ctx.destination);
      src.start(now); sw.start(now); sw.stop(now + 0.08);
    } else if (type === 'impact') {
      const bufSize = ctx.sampleRate * 0.12;
      const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
      const data = buf.getChannelData(0);
      for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * (1 - i/bufSize);
      const src = ctx.createBufferSource(); src.buffer = buf;
      const filt = ctx.createBiquadFilter(); filt.type = 'bandpass'; filt.frequency.value = 1200; filt.Q.value = 0.8;
      const g = ctx.createGain(); g.gain.setValueAtTime(0.25, now); g.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
      src.connect(filt); filt.connect(g); g.connect(ctx.destination);
      src.start(now);
    } else if (type === 'explode') {
      const bufSize = ctx.sampleRate * 0.35;
      const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
      const data = buf.getChannelData(0);
      for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i/bufSize, 1.5);
      const src = ctx.createBufferSource(); src.buffer = buf;
      const filt = ctx.createBiquadFilter(); filt.type = 'lowpass'; filt.frequency.value = 800;
      const g = ctx.createGain(); g.gain.setValueAtTime(0.4, now); g.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
      src.connect(filt); filt.connect(g); g.connect(ctx.destination);
      src.start(now);
    } else if (type === 'victory') {
      const notes = [523, 659, 784, 1047];
      notes.forEach((f, i) => {
        const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
        const g = ctx.createGain(); g.gain.setValueAtTime(0, now + i * 0.18);
        g.gain.linearRampToValueAtTime(0.2, now + i * 0.18 + 0.02);
        g.gain.exponentialRampToValueAtTime(0.001, now + i * 0.18 + 0.4);
        o.connect(g); g.connect(ctx.destination);
        o.start(now + i * 0.18); o.stop(now + i * 0.18 + 0.45);
      });
    }
  }
};

// ─────────────────────────────────────────────
//  RENDERING
// ─────────────────────────────────────────────
function drawBackground() {
  const grad = ctx.createLinearGradient(0, 0, 0, GROUND_Y);
  grad.addColorStop(0, '#1a2a4a');
  grad.addColorStop(0.45, '#3a6a8a');
  grad.addColorStop(0.7, '#7a9aaa');
  grad.addColorStop(1, '#b8c8d8');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, GROUND_Y);

  // sun
  const sx = W - 120, sy = 70;
  const sGrad = ctx.createRadialGradient(sx, sy, 8, sx, sy, 60);
  sGrad.addColorStop(0, '#fff4c8');
  sGrad.addColorStop(0.4, '#ffd700');
  sGrad.addColorStop(1, '#ff8c00');
  ctx.fillStyle = sGrad;
  ctx.beginPath(); ctx.arc(sx, sy, 60, 0, Math.PI * 2); ctx.fill();
  // sun rays
  ctx.strokeStyle = 'rgba(255,200,100,0.35)';
  ctx.lineWidth = 2;
  for (let i = 0; i < 12; i++) {
    const a = i / 12 * Math.PI * 2;
    ctx.beginPath();
    ctx.moveTo(sx + Math.cos(a) * 40, sy + Math.sin(a) * 40);
    ctx.lineTo(sx + Math.cos(a) * 70, sy + Math.sin(a) * 70);
    ctx.stroke();
  }

  // clouds
  ctx.fillStyle = 'rgba(255,255,255,0.85)';
  for (const c of clouds) {
    ctx.beginPath();
    ctx.ellipse(c.x, c.y, c.w, c.h, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(c.x - c.w * 0.5, c.y + c.h * 0.2, c.w * 0.6, c.h * 0.7, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.ellipse(c.x + c.w * 0.5, c.y + c.h * 0.25, c.w * 0.55, c.h * 0.65, 0, 0, Math.PI * 2);
    ctx.fill();
  }
}

let clouds = [];
function initClouds() {
  clouds = [];
  for (let i = 0; i < 5; i++) {
    clouds.push({ x: rand(0, W), y: rand(20, 90), w: rand(80, 160), h: rand(20, 36) });
  }
}

function drawGround() {
  // dirt strip
  ctx.fillStyle = '#5a4028';
  ctx.fillRect(0, GROUND_Y, W, 16);
  // ground line
  ctx.fillStyle = '#3a2a1a';
  ctx.fillRect(0, GROUND_Y, W, 3);
  // grid lines (retro)
  ctx.strokeStyle = 'rgba(0,0,0,0.25)';
  ctx.lineWidth = 1;
  for (let x = 0; x < W; x += 40) {
    ctx.beginPath(); ctx.moveTo(x, GROUND_Y); ctx.lineTo(x, GROUND_Y + 16); ctx.stroke();
  }
}

function drawBuildings() {
  for (const b of S.buildings) {
    const x = b.x, y = GROUND_Y - b.h, w = b.w, h = b.h;
    // main body
    ctx.fillStyle = b.color;
    ctx.fillRect(x, y, w, h);
    // roof highlight
    ctx.fillStyle = 'rgba(255,255,255,0.18)';
    ctx.fillRect(x, y, w, h * 0.18);
    // dark top edge
    ctx.fillStyle = 'rgba(0,0,0,0.25)';
    ctx.fillRect(x, y, w, 3);
    // windows (retro)
    ctx.fillStyle = 'rgba(255,220,120,0.55)';
    for (let row = 1; row < 4; row++) {
      for (let col = 1; col < 6; col++) {
        const wx = x + 8 + col * (w - 16) / 5;
        const wy = y + 8 + row * (h - 16) / 4;
        ctx.fillRect(wx, wy, w * 0.18, h * 0.18);
      }
    }
  }
}

function drawGorilla(g, anim) {
  const t = anim || 0;
  ctx.save();
  ctx.translate(g.x, g.y);
  // sway
  const sway = g.side === 'left' ? Math.sin(t * 0.08) * 2 : Math.sin(t * 0.08) * -2;
  ctx.rotate(sway * 0.02);
  // body
  const bw = 52, bh = 72;
  ctx.fillStyle = g.color;
  roundRect(ctx, -bw/2, -bh/2 + 8, bw, bh, 8);
  // chest shading
  ctx.fillStyle = 'rgba(255,255,255,0.15)';
  roundRect(ctx, -bw/2 + 6, -bh/2 + 12, bw * 0.5, bh * 0.4, 5);
  // arms
  ctx.fillStyle = g.color;
  roundRect(ctx, -bw/2 - 6, -bh/2 + 10, 18, 40, 8);
  roundRect(ctx, bw/2 + 6, -bh/2 + 10, 18, 40, 8);
  // legs
  roundRect(ctx, -bw/2 + 10, bh/2 - 24, 18, 24, 8);
  roundRect(ctx, bw/2 - 8, bh/2 - 24, 18, 24, 8);
  // head
  ctx.fillStyle = g.color;
  ctx.beginPath(); ctx.arc(0, -bh/2 - 2, 22, 0, Math.PI * 2); ctx.fill();
  // ears
  ctx.fillStyle = g.color;
  ctx.beginPath(); ctx.ellipse(-16, -bh/2 - 14, 8, 10, 0, 0, Math.PI * 2); ctx.fill();
  ctx.beginPath(); ctx.ellipse(16, -bh/2 - 14, 8, 10, 0, 0, Math.PI * 2); ctx.fill();
  // eyes
  ctx.fillStyle = '#222';
  ctx.beginPath(); ctx.arc(-7, -bh/2 - 8, 3.5, 0, Math.PI * 2); ctx.fill();
  ctx.beginPath(); ctx.arc(7, -bh/2 - 8, 3.5, 0, Math.PI * 2); ctx.fill();
  // nose
  ctx.fillStyle = '#555';
  ctx.beginPath(); ctx.ellipse(0, -bh/2 - 2, 4, 3, 0, 0, Math.PI * 2); ctx.fill();
  // mouth
  ctx.strokeStyle = '#444'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(0, -bh/2 + 6, 8, 0.1, Math.PI - 0.1); ctx.stroke();
  // win pose: arms up
  if (g.winPose) {
    ctx.fillStyle = g.color;
    roundRect(ctx, -bw/2 - 6, -bh/2 - 20, 18, 40, 8);
    roundRect(ctx, bw/2 + 6, -bh/2 - 20, 18, 40, 8);
  }
  ctx.restore();
}

function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.arcTo(x + w, y, x + w, y + r, r);
  ctx.lineTo(x + w, y + h - r);
  ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
  ctx.lineTo(x + r, y + h);
  ctx.arcTo(x, y + h, x, y + h - r, r);
  ctx.lineTo(x, y + r);
  ctx.arcTo(x, y, x + r, y, r);
  ctx.closePath();
  ctx.fill();
}

function drawProjectile(p, t) {
  if (!p) return;
  // trail
  for (let i = 0; i < p.trail.length; i++) {
    const tr = p.trail[i];
    const alpha = (i / p.trail.length) * 0.6;
    ctx.fillStyle = `rgba(245, 215, 66, ${alpha})`;
    ctx.beginPath();
    ctx.arc(tr.x, tr.y, 4 * (i / p.trail.length) + 2, 0, Math.PI * 2);
    ctx.fill();
  }
  // banana
  const rad = Math.atan2(p.vy, p.vx);
  ctx.save();
  ctx.translate(p.x, p.y);
  ctx.rotate(rad);
  ctx.scale(1, 0.85);
  ctx.beginPath();
  ctx.moveTo(-4, -4);
  ctx.quadraticCurveTo(8, 2, 16, 3);
  ctx.quadraticCurveTo(22, 4, 20, 5);
  ctx.lineTo(20, 6);
  ctx.quadraticCurveTo(16, 5, 10, 3);
  ctx.quadraticCurveTo(4, 1, -4, -4);
  ctx.closePath();
  ctx.fillStyle = '#f5d742';
  ctx.fill();
  ctx.strokeStyle = 'rgba(200,160,60,0.6)';
  ctx.lineWidth = 1;
  ctx.stroke();
  // highlight
  ctx.beginPath();
  ctx.ellipse(4, -1, 5, 2, 0.2, 0, Math.PI * 2);
  ctx.fillStyle = 'rgba(255,255,255,0.55)';
  ctx.fill();
  ctx.restore();
}

function drawExplosion() {
  if (!S.explosion) return;
  const e = S.explosion;
  const now = performance.now();
  const age = (now - e.startTime) / 1000;
  // expanding ring
  const rad = Math.min(e.radius + age * 90, e.maxRadius);
  const alpha = clamp(1 - age / 0.6, 0, 1);
  ctx.strokeStyle = `rgba(255, 107, 53, ${alpha * 0.6})`;
  ctx.lineWidth = 3;
  ctx.beginPath(); ctx.arc(e.x, e.y, rad, 0, Math.PI * 2); ctx.stroke();
  // flash
  if (age < 0.15) {
    ctx.fillStyle = `rgba(255, 255, 200, ${alpha * 0.5})`;
    ctx.beginPath(); ctx.arc(e.x, e.y, rad * 0.5, 0, Math.PI * 2); ctx.fill();
  }
  // particles
  for (const p of e.particles) {
    p.x += p.vx * 0.016;
    p.y += p.vy * 0.016;
    p.vy += 120 * 0.016;
    p.life -= 0.016;
    if (p.life > 0) {
      const s = p.size * (p.life / 1.2);
      ctx.fillStyle = `rgba(255, ${Math.floor(180 + 75 * p.life)}, 60, ${p.life * 0.9})`;
      ctx.beginPath(); ctx.arc(p.x, p.y, s, 0, Math.PI * 2); ctx.fill();
    }
  }
  // scorch
  if (e.scorch.life < 1) {
    e.scorch.life += 0.02;
    const sl = clamp(e.scorch.life, 0, 1);
    ctx.fillStyle = `rgba(20, 10, 5, ${sl * 0.85})`;
    ctx.beginPath();
    ctx.ellipse(e.scorch.x, e.scorch.y, 16, 10, 0, 0, Math.PI * 2);
    ctx.fill();
  }
}

// ─────────────────────────────────────────────
//  GAME LOOP
// ─────────────────────────────────────────────
let lastTime = 0;

function gameLoop(timestamp) {
  const dt = Math.min((timestamp - lastTime) / 1000, 0.033);
  lastTime = timestamp;

  // phase logic
  if (S.phase === 'playing' && S.projectile) {
    updateProjectile(dt);
  }
  if (S.phase === 'exploding' && S.explosion) {
    const now = performance.now();
    const age = (now - S.explosion.startTime) / 1000;
    S.explosion.radius = age * 90;
    if (age > 0.9) {
      S.phase = 'playing';
      // pass turn
      S.activePlayer = S.activePlayer === 1 ? 2 : 1;
      nextWind();
      $('turnText').textContent = `Player ${S.activePlayer}`;
      $('turnText').style.color = S.activePlayer === 1 ? 'var(--accent)' : 'var(--accent)';
      updateControls();
      $('angleValue').textContent = `${Math.round(S.angle)}°`;
      $('velocityValue').textContent = S.velocity;
    }
  }
  if (S.phase === 'roundEnd') {
    // keep showing
  }

  // render
  ctx.save();
  drawBackground();
  drawGround();
  drawBuildings();
  // gorillas behind buildings? no — draw after buildings
  for (const g of S.gorillas) {
    drawGorilla(g, 0);
  }
  drawProjectile(S.projectile, 0);
  drawExplosion();
  ctx.restore();

  // HUD updates
  $('angleValue').textContent = `${Math.round(S.angle)}°`;
  $('velocityValue').textContent = S.velocity;

  requestAnimationFrame(gameLoop);
}

// ─────────────────────────────────────────────
//  SETTINGS PANEL
// ─────────────────────────────────────────────
const SETTINGS_DEF = [
  { key: 'blastRadius',   label: 'Blast Radius',        type: 'number', min: 20, max: 200, step: 1, unit: 'px',   defaultValue: 60 },
  { key: 'gravity',       label: 'Gravity',             type: 'number', min: 100, max: 800, step: 10, unit: 'px/s²', defaultValue: 400 },
  { key: 'windVariance',  label: 'Wind Variance',       type: 'range',  min: 10, max: 400, step: 5,  unit: 'px/s', defaultValue: 150 },
  { key: 'buildingCount', label: 'Building Count',      type: 'number', min: 4,  max: 20,  step: 1,  unit: 'buildings', defaultValue: 10 },
  { key: 'buildingHeightMin', label: 'Height Min',      type: 'number', min: 10, max: 150, step: 5,  unit: 'px',   defaultValue: 40 },
  { key: 'buildingHeightMax', label: 'Height Max',      type: 'number', min: 50, max: 500, step: 5,  unit: 'px',   defaultValue: 300 },
  { key: 'velocityMin',   label: 'Power Min',           type: 'number', min: 1,  max: 200, step: 1,  unit: '',     defaultValue: 1 },
  { key: 'velocityMax',   label: 'Power Max',           type: 'number', min: 1,  max: 200, step: 1,  unit: '',     defaultValue: 100 },
  { key: 'damageThreshold', label: 'Damage Threshold',  type: 'select', options: ['inside', 'falloff'], defaultValue: 'inside' },
];

// track which keys changed this session (for rebuilding city)
const CHANGED_KEYS = {};

function openSettings() {
  renderSettings();
  $('settingsModal').classList.add('active');
  S.settingsOpen = true;
  S.phase = 'settings';
  showOverlay('settingsBg');
  $('settingsBg').addEventListener('click', closeSettings);
}

function closeSettings() {
  $('settingsModal').classList.remove('active');
  S.settingsOpen = false;
  S.phase = 'playing';
  $('settingsBg').removeEventListener('click', closeSettings);
}

function renderSettings() {
  const list = $('settingsList');
  list.innerHTML = '';
  const container = document.createElement('div');
  container.style.cssText = 'display:grid; grid-template-columns: 1fr 1fr; gap: 10px 24px;';
  for (const def of SETTINGS_DEF) {
    const row = document.createElement('div');
    row.className = 'setting';
    row.dataset.key = def.key;
    const name = document.createElement('div');
    name.className = 'name';
    name.textContent = def.label;
    const val = document.createElement('div');
    val.className = 'value';
    val.innerHTML = `<span class="unit">${def.unit}</span>`;
    const valSpan = val.querySelector('.unit');
    if (def.type === 'range') {
      const wrap = document.createElement('div');
      wrap.className = 'value-row';
      const minSpan = document.createElement('span');
      minSpan.className = 'value'; minSpan.textContent = def.min;
      const mid = document.createElement('span'); mid.textContent = ' — ';
      const maxSpan = document.createElement('span');
      maxSpan.className = 'value'; maxSpan.textContent = def.max;
      wrap.appendChild(minSpan); wrap.appendChild(mid); wrap.appendChild(maxSpan);
      val.appendChild(wrap);
    }
    const sp = document.createElement('div');
    sp.className = 'spinner';
    const dec = document.createElement('button');
    dec.textContent = '−';
    const inc = document.createElement('button');
    inc.textContent = '+';
    sp.appendChild(dec); sp.appendChild(inc);
    if (def.type === 'number') {
      const input = document.createElement('input');
      input.type = 'number';
      input.className = 'input';
      input.value = CFG[def.key];
      input.addEventListener('change', () => {
        const v = parseFloat(input.value);
        if (!isNaN(v)) { CFG[def.key] = clamp(v, def.min, def.max); updateFromSettings(); };
      });
      sp.appendChild(input);
    } else if (def.type === 'select') {
      const sel = document.createElement('select');
      sel.className = 'sel';
      for (const o of def.options) {
        const opt = document.createElement('option');
        opt.value = o; opt.textContent = o;
        if (CFG[def.key] === o) opt.selected = true;
        sel.appendChild(opt);
      }
      sel.addEventListener('change', () => {
        CFG[def.key] = sel.value;
        updateFromSettings();
      });
      sp.appendChild(sel);
    } else {
      dec.addEventListener('click', () => { CFG[def.key] = clamp(CFG[def.key] - def.step, def.min, def.max); updateFromSettings(); });
      inc.addEventListener('click', () => { CFG[def.key] = clamp(CFG[def.key] + def.step, def.min, def.max); updateFromSettings(); });
    }
    row.appendChild(name);
    row.appendChild(val);
    row.appendChild(sp);
    container.appendChild(row);
  }
  list.appendChild(container);
  $('liveBadge').style.display = 'block';
  applySettingsToDOM();
}

function applySettingsToDOM() {
  for (const def of SETTINGS_DEF) {
    const row = document.querySelector(`.setting[data-key="${def.key}"]`);
    if (!row) continue;
    const val = row.querySelector('.value');
    if (def.type === 'range') {
      const spans = val.querySelectorAll('.value');
      // spans: [min, max] inside wrap; the outer .value holds them
      const wrap = val.querySelector('.value-row');
      if (wrap) {
        const children = wrap.querySelectorAll('.value');
        if (children.length >= 2) {
          children[0].textContent = CFG[def.key];
          children[1].textContent = CFG[def.key];
        }
      }
    } else if (def.type === 'number') {
      const input = row.querySelector('.input');
      if (input) input.value = CFG[def.key];
    } else if (def.type === 'select') {
      const sel = row.querySelector('.sel');
      if (sel) sel.value = CFG[def.key];
    }
  }
}

function updateFromSettings() {
  for (const def of SETTINGS_DEF) {
    const row = document.querySelector(`.setting[data-key="${def.key}"]`);
    if (!row) continue;
    if (def.type === 'number') {
      const input = row.querySelector('.input');
      if (input) {
        const v = parseFloat(input.value);
        if (!isNaN(v)) CFG[def.key] = clamp(v, def.min, def.max);
      }
    } else if (def.type === 'select') {
      const sel = row.querySelector('.sel');
      if (sel) CFG[def.key] = sel.value;
    } else if (def.type === 'range') {
      const wrap = row.querySelector('.value-row');
      if (wrap) {
        const children = wrap.querySelectorAll('.value');
        if (children.length >= 2) {
          CFG[def.key] = parseFloat(children[0].textContent);
        }
      }
    }
    // mark changed
    if (!CHANGED_KEYS[def.key]) CHANGED_KEYS[def.key] = false;
    CHANGED_KEYS[def.key] = true;
  }
  applySettingsToDOM();
  // update live constants
  GRAVITY = CFG.gravity;
  BLAST_RADIUS = CFG.blastRadius;
  S.settingsDirty = true;
  // re-render city if building count changed
  if (CHANGED_KEYS['buildingCount']) {
    S.buildings = generateCity(S.seed);
    S.gorillas = placeGorillas(S.buildings);
  }
}

function defChanged(key) {
  return CHANGED_KEYS[key] === true;
}

// ─────────────────────────────────────────────
//  GAME INIT
// ─────────────────────────────────────────────
function init() {
  initClouds();
  startMatch();
  requestAnimationFrame(gameLoop);
}

// wire up buttons
$('newMatchBtn').addEventListener('click', () => {
  startMatch();
  showOverlay('titleScreen');
});
$('rematchBtn').addEventListener('click', () => {
  S.seed = Date.now() ^ (Math.random() * 0xffffffff);
  S.rng = mulberry32(S.seed);
  S.buildings = generateCity(S.seed);
  S.gorillas = placeGorillas(S.buildings);
  $('turnText').textContent = `Player ${S.activePlayer}`;
  $('turnText').style.color = 'var(--accent)';
  updateControls();
  $('angleValue').textContent = `${Math.round(S.angle)}°`;
  $('velocityValue').textContent = S.velocity;
  showOverlay('roundEndScreen');
  setTimeout(() => {
    S.phase = 'playing';
    showOverlay('titleScreen');
  }, 1200);
});
$('fireBtn').addEventListener('click', fire);
$('angleMinus').addEventListener('click', () => { if (S.phase === 'playing') S.angle = clamp(S.angle - 1, 0, 90); });
$('anglePlus').addEventListener('click', () => { if (S.phase === 'playing') S.angle = clamp(S.angle + 1, 0, 90); });
$('velocityMinus').addEventListener('click', () => { if (S.phase === 'playing') S.velocity = clamp(S.velocity - 1, CFG.velocityMin, CFG.velocityMax); });
$('velocityPlus').addEventListener('click', () => { if (S.phase === 'playing') S.velocity = clamp(S.velocity + 1, CFG.velocityMin, CFG.velocityMax); });
$('switchPlayerBtn').addEventListener('click', () => {
  S.activePlayer = S.activePlayer === 1 ? 2 : 1;
  updateControls();
  $('turnText').textContent = `Player ${S.activePlayer}`;
  $('turnText').style.color = 'var(--accent)';
});
$('p1Selector').addEventListener('click', () => { S.activePlayer = 1; updateControls(); });
$('p2Selector').addEventListener('click', () => { S.activePlayer = 2; updateControls(); });
$('settingsBtn')?.addEventListener('click', openSettings);
$('settingsClose').addEventListener('click', closeSettings);

init();
</script>
</body>
</html>