← Gorillaz results

Gorillaz

muse spark 1.2 contributor · full

Initial view of Gorillaz

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 42,993 bytes · SHA-256 ca1a9899aa5e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gorillaz — Banana Bomb Duel</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Space+Grotesk:wght@500;700&family=JetBrains+Mono:wght@700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#0a0e1a;
  --panel:#131a2e;
  --accent:#ff3b30;
  --accent2:#2ec4ff;
  --yellow:#ffd23f;
  --green:#2ecc71;
}
body{
  background: radial-gradient(1200px 600px at 50% -10%, #1a2a5a 0%, #0a0e1a 60%, #060912 100%);
  color:#e6eefc;
  font-family:'Space Grotesk',system-ui,sans-serif;
  min-height:100vh;
  overflow-x:hidden;
}
.topbar{
  max-width:1100px;margin:0 auto;
  padding:12px 16px;
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
}
.brand{
  display:flex;align-items:center;gap:12px;
}
.logo{
  font-family:'Press Start 2P',monospace;
  font-size:18px;letter-spacing:1px;
  background:linear-gradient(90deg,#ffd23f,#ff7a00);
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;
  text-shadow:0 0 18px rgba(255,210,63,.35);
}
.logo span{ -webkit-text-fill-color:#ff3b30; }
.badge{
  font-family:'JetBrains Mono',monospace;
  font-size:10px;letter-spacing:.12em;
  background:#1b2442;border:1px solid #2a3a6a;color:#8ea6d6;
  padding:4px 8px;border-radius:999px;
}
.badge.live{ background:#1a2e1e; border-color:#2ecc71; color:#a8f0c0; box-shadow:0 0 10px rgba(46,204,113,.4)}
.hud{
  max-width:1100px;margin:0 auto;
  padding:0 16px 10px;
  display:grid;grid-template-columns:1fr auto 1fr;gap:12px;align-items:stretch;
}
@media(max-width:760px){ .hud{grid-template-columns:1fr;}}
.hud-card{
  background:linear-gradient(180deg,#152042,#0f1730);
  border:1px solid #24325e;border-radius:14px;
  padding:10px 14px;display:flex;align-items:center;justify-content:space-between;gap:10px;
  box-shadow:0 8px 24px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.06);
}
.hud-label{font-size:10px;letter-spacing:.14em;color:#7d90b8;font-family:'JetBrains Mono',monospace}
.hud-value{font-family:'JetBrains Mono',monospace;font-weight:700;font-size:18px}
.wind-arrow{font-size:22px;transition:transform .4s}
.turn-card{
  text-align:center;flex-direction:column;justify-content:center;
  position:relative;overflow:hidden;
}
.turn-card.p1{ border-color:#ff3b30; box-shadow:0 0 0 1px rgba(255,59,48,.3),0 8px 24px rgba(0,0,0,.35)}
.turn-card.p2{ border-color:#2ec4ff; box-shadow:0 0 0 1px rgba(46,196,255,.3),0 8px 24px rgba(0,0,0,.35)}
.turn-name{font-family:'Press Start 2P',monospace;font-size:11px}
.turn-sub{font-family:'JetBrains Mono',monospace;font-size:11px;color:#9fb0d6}
.score-pill{
  background:#0d1328;border:1px solid #2a3a6a;border-radius:999px;
  padding:6px 10px;display:flex;gap:10px;align-items:center;
  font-family:'JetBrains Mono',monospace;font-size:12px;
}
.dot{width:10px;height:10px;border-radius:50%;display:inline-block}
.stage{
  max-width:1100px;margin:8px auto 0;padding:0 16px;
}
.canvas-wrap{
  position:relative;
  background:#0b1020;
  border:1px solid #24325e;border-radius:16px;overflow:hidden;
  box-shadow:0 18px 50px rgba(0,0,0,.5);
}
canvas{display:block;width:100%;height:auto;image-rendering:pixelated}
.controls{
  display:grid;grid-template-columns:1fr auto 1fr;gap:12px;
  margin-top:12px;
}
@media(max-width:860px){ .controls{grid-template-columns:1fr;}}
.ctrl-card{
  background:linear-gradient(180deg,#131c3a,#0f1730);
  border:1px solid #24325e;border-radius:14px;padding:12px;
}
.ctrl-head{ display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.ctrl-title{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.12em;color:#8ea0c6}
.ctrl-val{font-family:'JetBrains Mono',monospace;font-weight:700;font-size:20px}
.slider-row{display:flex;align-items:center;gap:8px}
.btn-s{
  width:36px;height:36px;border-radius:10px;border:1px solid #2a3a6a;background:#1a2444;color:#cfe0ff;
  font-weight:800;cursor:pointer;transition:.15s;
}
.btn-s:hover{background:#22305a;transform:translateY(-1px)}
.btn-s:active{transform:translateY(0)}
input[type=range]{ flex:1; accent-color:#ffd23f; height:6px}
.fire-btn{
  width:100%;height:100%;min-height:86px;
  border-radius:14px;border:none;
  background:linear-gradient(180deg,#ffd23f,#ff8c00);
  color:#1a0f00;font-family:'Press Start 2P',monospace;font-size:14px;
  cursor:pointer;box-shadow:0 8px 20px rgba(255,140,0,.35), inset 0 1px 0 rgba(255,255,255,.6);
  transition:.15s;
}
.fire-btn:hover{transform:translateY(-2px);filter:brightness(1.05)}
.fire-btn:active{transform:translateY(0)}
.fire-btn:disabled{opacity:.45;cursor:not-allowed;transform:none}
.hint{font-family:'JetBrains Mono',monospace;font-size:10px;color:#6d82ad;text-align:center;margin-top:6px}
.action-row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.ghost-btn{
  flex:1;background:#0f1730;border:1px solid #24325e;color:#a8b9dd;
  padding:10px;border-radius:10px;font-family:'JetBrains Mono',monospace;font-size:12px;cursor:pointer;
}
.ghost-btn:hover{background:#17244a;color:#fff}
.tuning-btn{
  background:linear-gradient(180deg,#1e2d5a,#162040);border:1px solid #2a3a6a;color:#cfe0ff;
  padding:10px 14px;border-radius:10px;font-family:'JetBrains Mono',monospace;font-size:12px;cursor:pointer;
}
.tuning-btn.active{border-color:#ffd23f;color:#ffd23f;box-shadow:0 0 12px rgba(255,210,63,.3)}
/* tuning panel */
.tuning-overlay{
  position:fixed;inset:0;background:rgba(3,6,16,.72);backdrop-filter:blur(6px);
  display:none;align-items:center;justify-content:center;z-index:50;padding:16px;
}
.tuning-overlay.open{display:flex}
.tuning-panel{
  width:min(720px,100%);max-height:90vh;overflow:auto;
  background:linear-gradient(180deg,#0f1730,#0a1022);
  border:1px solid #2a3a6a;border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.6);
}
.tuning-head{
  position:sticky;top:0;background:linear-gradient(180deg,#111d3d,#0f1730);
  padding:16px;border-bottom:1px solid #24325e;display:flex;justify-content:space-between;align-items:center;z-index:1;
}
.tuning-head h2{font-family:'Press Start 2P',monospace;font-size:12px;color:#ffd23f}
.close-x{background:#1a2444;border:1px solid #2a3a6a;color:#fff;width:36px;height:36px;border-radius:10px;cursor:pointer}
.tuning-body{padding:16px;display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:640px){.tuning-body{grid-template-columns:1fr}}
.field{
  background:#0d1430;border:1px solid #24325e;border-radius:12px;padding:12px;
}
.field.live{border-color:#ffd23f;box-shadow:0 0 0 1px rgba(255,210,63,.25)}
.field label{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.1em;color:#8ea0c6;display:block;margin-bottom:6px}
.field .row{display:flex;gap:8px;align-items:center}
.field input[type=number], .field input[type=range]{width:100%;background:#0a1022;border:1px solid #24325e;color:#e6eefc;border-radius:8px;padding:8px;font-family:'JetBrains Mono',monospace}
.field input[type=range]{padding:0;border:none}
.mini{font-family:'JetBrains Mono',monospace;font-size:10px;color:#6d82ad;margin-top:6px}
.toggle{ display:flex;align-items:center;gap:8px; font-family:'JetBrains Mono',monospace;font-size:12px}
.toggle input{accent-color:#ffd23f}
.foot-actions{display:flex;gap:8px;padding:0 16px 16px}
.primary{background:#ffd23f;color:#1a1200;border:none;padding:10px 14px;border-radius:10px;font-family:'JetBrains Mono',monospace;font-weight:700;cursor:pointer;flex:1}
.secondary{background:#162040;border:1px solid #24325e;color:#a8b9dd;padding:10px 14px;border-radius:10px;font-family:'JetBrains Mono',monospace;cursor:pointer;flex:1}
/* win overlay */
.win-overlay{
  position:absolute;inset:0;display:none;align-items:center;justify-content:center;
  background:radial-gradient(600px 400px at 50% 50%, rgba(255,210,63,.18), transparent 70%), rgba(6,9,20,.55);
  backdrop-filter:blur(2px);
}
.win-overlay.show{display:flex}
.win-card{
  background:linear-gradient(180deg,#162040,#0e1530);
  border:1px solid #2a3a6a;border-radius:16px;padding:20px;text-align:center;
  box-shadow:0 18px 40px rgba(0,0,0,.5);min-width:300px;
}
.win-card h3{font-family:'Press Start 2P',monospace;font-size:14px;margin-bottom:8px}
.win-card p{font-family:'JetBrains Mono',monospace;font-size:12px;color:#9fb0d6;margin-bottom:14px}
.trajectory{ position:absolute; inset:0; pointer-events:none}
</style>
</head>
<body>
<div class="topbar">
  <div class="brand">
    <div class="logo">GORILLA<span>Z</span> 🍌💥</div>
    <span class="badge" id="liveBadge">CLASSIC TUNING</span>
  </div>
  <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
    <button class="tuning-btn" id="openTuning">⚙ LIVE TUNING</button>
    <button class="ghost-btn" style="padding:10px 14px" id="newMatchBtn">↻ NEW SKYLINE</button>
  </div>
</div>

<div class="hud">
  <div class="hud-card" id="windCard">
    <div>
      <div class="hud-label">WIND VECTOR</div>
      <div style="display:flex;align-items:center;gap:8px">
        <span class="wind-arrow" id="windArrow">→</span>
        <span class="hud-value" id="windVal">+2.4</span>
        <span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#7d90b8" id="windLabel">EAST</span>
      </div>
      <div class="mini" id="windDetail">gravity 0.35 • blast 42px</div>
    </div>
    <div style="text-align:right">
      <div class="hud-label">TRAJECTORY</div>
      <div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ffd23f" id="trajInfo">parabola • no penetration</div>
    </div>
  </div>

  <div class="hud-card turn-card p1" id="turnCard">
    <div class="turn-name" id="turnName">PLAYER 1 TURN</div>
    <div class="turn-sub" id="turnSub">Red Gorilla — Aim & Fire!</div>
    <div style="font-size:18px" id="turnIcon">🦍🔴</div>
  </div>

  <div class="hud-card">
    <div>
      <div class="hud-label">SESSION SCORE</div>
      <div class="score-pill">
        <span><span class="dot" style="background:#ff3b30"></span> P1 <b id="scoreP1">0</b></span>
        <span style="opacity:.4">—</span>
        <span><span class="dot" style="background:#2ec4ff"></span> P2 <b id="scoreP2">0</b></span>
      </div>
      <div class="mini">first to win • blast kills count</div>
    </div>
    <div style="text-align:right">
      <div class="hud-label">ROUND</div>
      <div class="hud-value" id="roundNum">#1</div>
    </div>
  </div>
</div>

<div class="stage">
  <div class="canvas-wrap" id="canvasWrap">
    <canvas id="game" width="1100" height="560"></canvas>
    <div class="win-overlay" id="winOverlay">
      <div class="win-card">
        <h3 id="winTitle">PLAYER 1 WINS!</h3>
        <p id="winDesc">Direct banana impact — 38px from blast center</p>
        <div style="display:flex;gap:8px">
          <button class="primary" id="rematchBtn">REMATCH ↻</button>
          <button class="secondary" id="keepScoreBtn">KEEP SCORE</button>
        </div>
        <div class="mini" style="margin-top:8px">New skyline generated • wind reshuffled</div>
      </div>
    </div>
  </div>

  <div class="controls">
    <div class="ctrl-card">
      <div class="ctrl-head">
        <span class="ctrl-title">◢ ANGLE</span>
        <span class="ctrl-val" id="angleVal">45°</span>
      </div>
      <div class="slider-row">
        <button class="btn-s" id="angleMinus">−</button>
        <input type="range" id="angleSlider" min="0" max="90" value="45">
        <button class="btn-s" id="anglePlus">+</button>
      </div>
      <div class="hint">A / D or ← →  •  0° flat → 90° vertical</div>
    </div>

    <div class="ctrl-card" style="display:flex;flex-direction:column;justify-content:center">
      <button class="fire-btn" id="fireBtn">🍌 FIRE!</button>
      <div class="hint">SPACE to fire • Turn-based, no penetration</div>
      <div class="action-row">
        <button class="ghost-btn" id="randomAimBtn">🎲 Random Aim</button>
        <button class="ghost-btn" id="swapBtn">⇄ Swap Turn</button>
      </div>
    </div>

    <div class="ctrl-card">
      <div class="ctrl-head">
        <span class="ctrl-title">⚡ VELOCITY / POWER</span>
        <span class="ctrl-val" id="powerVal">52</span>
      </div>
      <div class="slider-row">
        <button class="btn-s" id="powerMinus">−</button>
        <input type="range" id="powerSlider" min="1" max="100" value="52">
        <button class="btn-s" id="powerPlus">+</button>
      </div>
      <div class="hint">W / S or ↑ ↓  •  Scale <span id="powerScaleHint">1–100</span></div>
    </div>
  </div>
  <div class="mini" style="text-align:center;margin:10px 0 20px">Retro arcade • Buildings indestructible • Explosion is circular • Wind shifts each turn if enabled</div>
</div>

<!-- Tuning Overlay -->
<div class="tuning-overlay" id="tuningOverlay">
  <div class="tuning-panel">
    <div class="tuning-head">
      <h2>⚙ LIVE TUNING — Gorillaz Constants</h2>
      <button class="close-x" id="closeTuning">✕</button>
    </div>
    <div class="tuning-body">
      <div class="field" id="f-blast">
        <label>💥 BLAST / EXPLOSION RADIUS (px)</label>
        <div class="row">
          <input type="range" id="cfgBlast" min="10" max="120" value="42">
          <input type="number" id="cfgBlastN" min="10" max="120" value="42" style="width:80px">
        </div>
        <div class="mini">Circular damage zone. Default 42px. Applies next explosion.</div>
      </div>
      <div class="field" id="f-gravity">
        <label>🌍 GRAVITY STRENGTH (px/frame²)</label>
        <div class="row">
          <input type="range" id="cfgGrav" min="0.05" max="1.2" step="0.01" value="0.35">
          <input type="number" id="cfgGravN" min="0.05" max="1.2" step="0.01" value="0.35" style="width:80px">
        </div>
        <div class="mini">Downward acceleration. Default 0.35. Higher = tighter arc.</div>
      </div>
      <div class="field" id="f-wind">
        <label>💨 WIND VARIANCE / RANGE</label>
        <div class="row">
          <input type="number" id="cfgWindMin" value="-6" style="width:70px"> <span style="font-family:'JetBrains Mono',monospace">to</span> <input type="number" id="cfgWindMax" value="6" style="width:70px">
        </div>
        <div class="row" style="margin-top:8px">
          <label class="toggle"><input type="checkbox" id="cfgWindShift" checked> Allow direction shifts between turns</label>
        </div>
        <div class="mini">Current wind sampled uniformly in range. Default −6 to +6.</div>
      </div>
      <div class="field" id="f-buildings">
        <label>🏙 BUILDING COUNT</label>
        <div class="row">
          <input type="range" id="cfgBCount" min="8" max="14" value="10">
          <input type="number" id="cfgBCountN" min="8" max="14" value="10" style="width:80px">
        </div>
        <div class="mini">8–14 per spec. Takes effect on next skyline.</div>
      </div>
      <div class="field" id="f-height">
        <label>📏 BUILDING HEIGHT RANGE (px)</label>
        <div class="row">
          <input type="number" id="cfgHMin" value="80" style="width:80px"> <span>—</span> <input type="number" id="cfgHMax" value="280" style="width:80px">
        </div>
        <div class="mini">Min/max building heights. Default 80–280.</div>
      </div>
      <div class="field" id="f-vel">
        <label>🚀 PROJECTILE VELOCITY SCALE</label>
        <div class="row">
          <input type="number" id="cfgVMin" value="1" style="width:70px"> <span>to</span> <input type="number" id="cfgVMax" value="100" style="width:70px">
        </div>
        <div class="mini">Power slider bounds. Default 1–100. Scale factor 0.22×.</div>
      </div>
      <div class="field" style="grid-column:1 / -1" id="f-damage">
        <label>🎯 EXPLOSION DAMAGE THRESHOLD</label>
        <div class="row" style="flex-wrap:wrap">
          <label class="toggle"><input type="radio" name="dmg" id="cfgDmgInside" checked> Splash kills if inside radius (classic)</label>
          <label class="toggle"><input type="radio" name="dmg" id="cfgDmgFalloff"> Falloff — damage fades to edge (requires closer)</label>
        </div>
        <div class="mini">Classic: any gorilla within blast radius = eliminated. Falloff: effective radius 70%.</div>
      </div>
    </div>
    <div class="foot-actions">
      <button class="secondary" id="resetCfg">Reset to Classic</button>
      <button class="primary" id="applyCfg">Apply & Close — Live!</button>
    </div>
    <div class="mini" style="padding:0 16px 16px;text-align:center">Changes apply immediately or on next turn/launch without restart. Deterministic within a turn.</div>
  </div>
</div>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

// Config defaults (classic)
const defaults = {
  blastRadius: 42,
  gravity: 0.35,
  windMin: -6,
  windMax: 6,
  windShift: true,
  buildingCount: 10,
  hMin: 80,
  hMax: 280,
  vMin: 1,
  vMax: 100,
  damageMode: 'inside', // 'inside' | 'falloff'
  velocityScale: 0.22
};
let cfg = {...defaults};

// Game state
let buildings = [];
let gorillas = []; // 0: P1, 1: P2
let wind = 2.4;
let turn = 0; // 0 or 1
let angle = 45;
let power = 52;
let score = [0,0];
let round = 1;
let projectile = null;
let particles = [];
let scorchMarks = [];
let animId = null;
let gameOver = false;
let trajectoryPoints = [];

// DOM refs
const angleSlider = document.getElementById('angleSlider');
const powerSlider = document.getElementById('powerSlider');
const angleVal = document.getElementById('angleVal');
const powerVal = document.getElementById('powerVal');
const windVal = document.getElementById('windVal');
const windArrow = document.getElementById('windArrow');
const windLabel = document.getElementById('windLabel');
const windDetail = document.getElementById('windDetail');
const turnCard = document.getElementById('turnCard');
const turnName = document.getElementById('turnName');
const turnSub = document.getElementById('turnSub');
const turnIcon = document.getElementById('turnIcon');
const scoreP1 = document.getElementById('scoreP1');
const scoreP2 = document.getElementById('scoreP2');
const roundNum = document.getElementById('roundNum');
const fireBtn = document.getElementById('fireBtn');
const winOverlay = document.getElementById('winOverlay');
const winTitle = document.getElementById('winTitle');
const winDesc = document.getElementById('winDesc');
const liveBadge = document.getElementById('liveBadge');
const powerScaleHint = document.getElementById('powerScaleHint');

function rand(min,max){ return Math.random()*(max-min)+min; }
function randInt(min,max){ return Math.floor(rand(min,max+1)); }

function isLiveTuned(){
  return cfg.blastRadius!==defaults.blastRadius || cfg.gravity!==defaults.gravity ||
         cfg.windMin!==defaults.windMin || cfg.windMax!==defaults.windMax || cfg.windShift!==defaults.windShift ||
         cfg.buildingCount!==defaults.buildingCount || cfg.hMin!==defaults.hMin || cfg.hMax!==defaults.hMax ||
         cfg.vMin!==defaults.vMin || cfg.vMax!==defaults.vMax || cfg.damageMode!==defaults.damageMode;
}
function updateLiveBadge(){
  if(isLiveTuned()){
    liveBadge.textContent='● LIVE TUNING ACTIVE';
    liveBadge.className='badge live';
  } else {
    liveBadge.textContent='CLASSIC TUNING';
    liveBadge.className='badge';
  }
  windDetail.textContent=`gravity ${cfg.gravity.toFixed(2)} • blast ${cfg.blastRadius}px • ${cfg.damageMode}`;
  powerScaleHint.textContent=`${cfg.vMin}–${cfg.vMax}`;
  // highlight fields
  document.getElementById('f-blast').classList.toggle('live', cfg.blastRadius!==defaults.blastRadius);
  document.getElementById('f-gravity').classList.toggle('live', cfg.gravity!==defaults.gravity);
  document.getElementById('f-wind').classList.toggle('live', cfg.windMin!==defaults.windMin || cfg.windMax!==defaults.windMax || cfg.windShift!==defaults.windShift);
  document.getElementById('f-buildings').classList.toggle('live', cfg.buildingCount!==defaults.buildingCount);
  document.getElementById('f-height').classList.toggle('live', cfg.hMin!==defaults.hMin || cfg.hMax!==defaults.hMax);
  document.getElementById('f-vel').classList.toggle('live', cfg.vMin!==defaults.vMin || cfg.vMax!==defaults.vMax);
  document.getElementById('f-damage').classList.toggle('live', cfg.damageMode!==defaults.damageMode);
}

function generateSkyline(){
  buildings=[];
  scorchMarks=[];
  const W = canvas.width;
  const groundY = canvas.height - 36;
  const count = cfg.buildingCount;
  const gap = 6;
  // compute widths to fill ~ W - margins
  const totalGap = (count-1)*gap;
  const avail = W - 40 - totalGap;
  let widths=[];
  let sum=0;
  for(let i=0;i<count;i++){ let w=randInt(58,96); widths.push(w); sum+=w; }
  // scale to fit avail
  let scale = avail / sum;
  widths = widths.map(w=> Math.floor(w*scale));
  // adjust rounding
  let curX = 20;
  const colors = ['#1e2a4a','#24365e','#1a2747','#2a3d6b','#1b2e55','#22355f'];
  for(let i=0;i<count;i++){
    let w = widths[i];
    let h = randInt(cfg.hMin, cfg.hMax);
    let x = curX;
    let y = groundY - h;
    buildings.push({x,y,w,h,color: colors[i%colors.length], windows: []});
    curX += w + gap;
  }
  // add windows
  buildings.forEach(b=>{
    const cols = Math.floor(b.w/18);
    const rows = Math.floor(b.h/20);
    for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){
      if(Math.random()<0.55) b.windows.push({r,c, on: Math.random()<0.7});
    }
  });
  // place gorillas
  let leftIdx = randInt(0, Math.floor(count/2)-1);
  let rightIdx = randInt(Math.ceil(count/2), count-1);
  // ensure not adjacent too close? enforce distance
  if(Math.abs(leftIdx-rightIdx)<3){
    rightIdx = count-1; leftIdx=0;
  }
  gorillas = [
    {bIdx:leftIdx, x: buildings[leftIdx].x + buildings[leftIdx].w/2, y: buildings[leftIdx].y, color:'#ff3b30', name:'P1'},
    {bIdx:rightIdx, x: buildings[rightIdx].x + buildings[rightIdx].w/2, y: buildings[rightIdx].y, color:'#2ec4ff', name:'P2'}
  ];
  // wind initial
  wind = rand(cfg.windMin, cfg.windMax);
  updateHUD();
  computeTrajectory();
}

function updateHUD(){
  windVal.textContent = (wind>=0?'+':'')+wind.toFixed(1);
  windArrow.textContent = wind>0.3?'→': wind<-0.3?'←':'↔';
  windArrow.style.transform = `scaleX(${wind>=0?1:-1})`;
  windLabel.textContent = wind>0.8?'EAST': wind<-0.8?'WEST':'CALM';
  windLabel.style.color = Math.abs(wind)>4 ? '#ffd23f' : '#7d90b8';
  turnCard.className = 'hud-card turn-card ' + (turn===0?'p1':'p2');
  turnName.textContent = turn===0 ? 'PLAYER 1 TURN' : 'PLAYER 2 TURN';
  turnSub.textContent = turn===0 ? 'Red Gorilla — Aim & Fire!' : 'Blue Gorilla — Aim & Fire!';
  turnIcon.textContent = turn===0 ? '🦍🔴' : '🦍🔵';
  scoreP1.textContent = score[0];
  scoreP2.textContent = score[1];
  roundNum.textContent = '#'+round;
  angleVal.textContent = angle+'°';
  powerVal.textContent = power;
  angleSlider.value = angle;
  powerSlider.value = power;
  // clamp sliders to cfg range
  powerSlider.min = cfg.vMin;
  powerSlider.max = cfg.vMax;
  angleSlider.min=0; angleSlider.max=90;
  fireBtn.disabled = !!projectile || gameOver;
}

function computeTrajectory(){
  // preview dotted parabola
  trajectoryPoints=[];
  if(projectile) return;
  let g = gorillas[turn];
  if(!g) return;
  let startX = g.x;
  let startY = g.y - 18;
  let dir = turn===0?1:-1;
  let rad = angle*Math.PI/180;
  let vx = Math.cos(rad)*power*cfg.velocityScale * dir;
  let vy = -Math.sin(rad)*power*cfg.velocityScale;
  let x=startX, y=startY;
  let tVx=vx, tVy=vy;
  for(let i=0;i<140;i++){
    trajectoryPoints.push({x,y});
    tVy += cfg.gravity;
    tVx += wind*0.015; // wind influence preview matching flight
    x+= tVx; y+= tVy;
    if(y>canvas.height-36) break;
    // stop at building hit for preview
    let hit=false;
    for(let b of buildings){
      if(x>=b.x && x<=b.x+b.w && y>=b.y && y<=b.y+b.h){ hit=true; break; }
    }
    if(hit) break;
    if(x<0||x>canvas.width) break;
  }
}

function draw(){
  const W=canvas.width, H=canvas.height;
  // sky gradient
  let grad = ctx.createLinearGradient(0,0,0,H);
  grad.addColorStop(0,'#0d1a3a');
  grad.addColorStop(0.45,'#1a2a5a');
  grad.addColorStop(0.75,'#2a3a6b');
  grad.addColorStop(1,'#0f1a33');
  ctx.fillStyle=grad;
  ctx.fillRect(0,0,W,H);
  // sun/moon
  ctx.fillStyle='#ffd23f';
  ctx.beginPath(); ctx.arc(W-110, 90, 32,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='rgba(255,210,63,.18)';
  ctx.beginPath(); ctx.arc(W-110,90,48,0,Math.PI*2); ctx.fill();
  // clouds
  ctx.fillStyle='rgba(255,255,255,.08)';
  for(let i=0;i<4;i++){
    let cx = 80 + i*220 + Math.sin(Date.now()*0.0003+i)*10;
    let cy = 70 + (i%2)*22;
    drawCloud(cx,cy, 1 - i*0.05);
  }
  // ground
  ctx.fillStyle='#0a0f1e';
  ctx.fillRect(0,H-36,W,36);
  ctx.fillStyle='#1a2a4a';
  ctx.fillRect(0,H-36,W,4);
  // scorch marks
  scorchMarks.forEach(s=>{
    ctx.fillStyle='rgba(20,10,0,.55)';
    ctx.beginPath(); ctx.ellipse(s.x, H-36, s.r*0.9, s.r*0.35,0,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='rgba(60,30,0,.35)';
    ctx.beginPath(); ctx.ellipse(s.x, H-36, s.r*0.6, s.r*0.22,0,0,Math.PI*2); ctx.fill();
  });
  // buildings
  buildings.forEach(b=>{
    // shadow
    ctx.fillStyle='rgba(0,0,0,.25)';
    ctx.fillRect(b.x+4, b.y+4, b.w, b.h);
    // main
    ctx.fillStyle=b.color;
    ctx.fillRect(b.x,b.y,b.w,b.h);
    // top highlight
    ctx.fillStyle='rgba(255,255,255,.08)';
    ctx.fillRect(b.x,b.y,b.w,6);
    ctx.fillStyle='rgba(255,255,255,.04)';
    ctx.fillRect(b.x,b.y,4,b.h);
    // windows
    const cols = Math.floor(b.w/18);
    b.windows.forEach(win=>{
      let wx = b.x + 6 + win.c*18;
      let wy = b.y + 10 + win.r*20;
      ctx.fillStyle = win.on ? '#ffd23f' : '#0a1022';
      if(win.on){
        ctx.fillStyle='#ffeb8a';
        ctx.fillRect(wx,wy,10,12);
        ctx.fillStyle='rgba(255,255,255,.5)';
        ctx.fillRect(wx,wy,10,2);
      } else {
        ctx.fillStyle='#0d1a33';
        ctx.fillRect(wx,wy,10,12);
      }
    });
    // antenna
    if(Math.random()<0.0){}
    // building outline
    ctx.strokeStyle='rgba(0,0,0,.35)';
    ctx.lineWidth=1;
    ctx.strokeRect(b.x,b.y,b.w,b.h);
  });
  // trajectory preview
  if(!projectile && !gameOver){
    ctx.save();
    ctx.setLineDash([4,6]);
    ctx.strokeStyle='rgba(255,210,63,.55)';
    ctx.lineWidth=1.5;
    ctx.beginPath();
    trajectoryPoints.forEach((p,i)=>{
      if(i===0) ctx.moveTo(p.x,p.y);
      else ctx.lineTo(p.x,p.y);
    });
    ctx.stroke();
    ctx.setLineDash([]);
    // dots
    trajectoryPoints.forEach((p,i)=>{
      if(i%6===0){
        ctx.fillStyle=i===0?'#ffd23f':'rgba(255,210,63,.9)';
        ctx.beginPath(); ctx.arc(p.x,p.y, i===0?4:2,0,Math.PI*2); ctx.fill();
      }
    });
    ctx.restore();
  }
  // gorillas
  gorillas.forEach((g,idx)=>{
    let isActive = idx===turn && !gameOver && !projectile;
    let bx = g.x, by = g.y;
    // shadow on roof
    ctx.fillStyle='rgba(0,0,0,.3)';
    ctx.fillRect(bx-18, by-2, 36, 4);
    // legs
    ctx.fillStyle= idx===0?'#b91c1c':'#0e7490';
    ctx.fillRect(bx-10, by-14, 8,14);
    ctx.fillRect(bx+2, by-14, 8,14);
    // body
    ctx.fillStyle= idx===0?'#ff3b30':'#2ec4ff';
    ctx.fillRect(bx-14, by-34, 28,22);
    // belly
    ctx.fillStyle='rgba(255,255,255,.18)';
    ctx.fillRect(bx-8, by-28, 16,12);
    // arms
    ctx.fillStyle= idx===0?'#d9241a':'#1a9bc4';
    ctx.fillRect(bx-18, by-30, 6,16);
    ctx.fillRect(bx+12, by-30, 6,16);
    // banana arm aiming
    let rad = angle*Math.PI/180;
    let dir = idx===0?1:-1;
    // if active, show arm angle
    if(isActive){
      ctx.save();
      ctx.translate(bx+ (dir>0?12:-12), by-26);
      ctx.rotate(dir>0 ? -rad : Math.PI+rad);
      ctx.fillStyle='#ffd23f';
      ctx.fillRect(0,-3,18,6);
      // banana tip
      ctx.fillStyle='#ff8c00';
      ctx.beginPath(); ctx.ellipse(18,0,6,3,0,0,Math.PI*2); ctx.fill();
      ctx.restore();
    } else {
      // idle arm
      ctx.fillStyle='#ffd23f';
      ctx.fillRect(bx+8, by-26, 12,4);
    }
    // head
    ctx.fillStyle='#1a1200';
    ctx.fillRect(bx-10, by-46, 20,16);
    ctx.fillStyle= idx===0?'#ff3b30':'#2ec4ff';
    ctx.fillRect(bx-10, by-46, 20,4);
    // eyes
    ctx.fillStyle='#fff';
    ctx.fillRect(bx-7, by-40, 5,4);
    ctx.fillRect(bx+2, by-40, 5,4);
    ctx.fillStyle='#000';
    ctx.fillRect(bx-5, by-39, 2,2);
    ctx.fillRect(bx+4, by-39, 2,2);
    // nose
    ctx.fillStyle='#6b3a1a';
    ctx.fillRect(bx-2, by-36, 4,3);
    // active glow
    if(isActive){
      ctx.strokeStyle= idx===0?'rgba(255,59,48,.9)':'rgba(46,196,255,.9)';
      ctx.lineWidth=2;
      ctx.setLineDash([6,4]);
      ctx.strokeRect(bx-18, by-48, 36, 48);
      ctx.setLineDash([]);
    }
    // label
    ctx.fillStyle='rgba(0,0,0,.6)';
    ctx.fillRect(bx-18, by-58, 36, 10);
    ctx.fillStyle='#fff';
    ctx.font='700 9px JetBrains Mono';
    ctx.textAlign='center';
    ctx.fillText(idx===0?'P1':'P2', bx, by-51);
  });
  // projectile
  if(projectile){
    // trail
    projectile.trail.forEach((p,i)=>{
      let a = i/projectile.trail.length;
      ctx.fillStyle=`rgba(255,210,63,${a*0.7})`;
      ctx.beginPath(); ctx.arc(p.x,p.y, 3*a+1,0,Math.PI*2); ctx.fill();
    });
    // banana
    ctx.save();
    ctx.translate(projectile.x, projectile.y);
    ctx.rotate(projectile.rot);
    ctx.fillStyle='#ffd23f';
    // curved banana shape
    ctx.beginPath();
    ctx.ellipse(0,0,10,5,0,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#ff8c00';
    ctx.beginPath(); ctx.ellipse(6,0,3,2,0,0,Math.PI*2); ctx.fill();
    ctx.fillStyle='#1a1200';
    ctx.beginPath(); ctx.arc(-8,0,2,0,Math.PI*2); ctx.fill();
    ctx.restore();
  }
  // particles / explosion
  particles.forEach(p=>{
    ctx.globalAlpha = p.life;
    ctx.fillStyle=p.color;
    ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
    // glow
    ctx.fillStyle=p.glow;
    ctx.beginPath(); ctx.arc(p.x,p.y,p.r*1.8,0,Math.PI*2); ctx.fill();
  });
  ctx.globalAlpha=1;
  // wind indicator on canvas
  // blast radius hint when tuning
  if(isLiveTuned()){
    ctx.fillStyle='rgba(255,210,63,.08)';
    ctx.font='10px JetBrains Mono';
    ctx.textAlign='left';
    ctx.fillText('LIVE TUNING ACTIVE', 14, H-46);
  }
}

function drawCloud(x,y,s){
  ctx.beginPath();
  ctx.ellipse(x,y,28*s,14*s,0,0,Math.PI*2);
  ctx.ellipse(x+18*s,y-6*s,20*s,12*s,0,0,Math.PI*2);
  ctx.ellipse(x-16*s,y-4*s,18*s,10*s,0,0,Math.PI*2);
  ctx.fill();
}

function fire(){
  if(projectile || gameOver) return;
  let g = gorillas[turn];
  let dir = turn===0?1:-1;
  let rad = angle*Math.PI/180;
  let vx = Math.cos(rad)*power*cfg.velocityScale * dir;
  let vy = -Math.sin(rad)*power*cfg.velocityScale;
  projectile = {
    x: g.x + dir*14,
    y: g.y - 26,
    vx, vy,
    rot: dir>0? -rad : Math.PI+rad,
    trail: []
  };
  fireBtn.disabled=true;
  // sound placeholder via WebAudio beep
  try{ beep(180,0.12,'square',0.2);}catch(e){}
  // launch loop
  requestAnimationFrame(tick);
}

function tick(){
  if(!projectile) return;
  // physics
  projectile.vy += cfg.gravity;
  projectile.vx += wind*0.018; // continuous horizontal force
  projectile.x += projectile.vx;
  projectile.y += projectile.vy;
  projectile.rot += 0.22;
  projectile.trail.push({x:projectile.x,y:projectile.y});
  if(projectile.trail.length>18) projectile.trail.shift();

  // collisions
  const H = canvas.height;
  // ground
  if(projectile.y >= H-36){
    explode(projectile.x, H-36);
    return;
  }
  // building collision (stop at first)
  for(let b of buildings){
    if(projectile.x>=b.x && projectile.x<=b.x+b.w && projectile.y>=b.y && projectile.y<=b.y+b.h){
      explode(projectile.x, projectile.y);
      return;
    }
  }
  // gorilla direct hit (small radius)
  for(let i=0;i<gorillas.length;i++){
    let g = gorillas[i];
    let dx = projectile.x - g.x;
    let dy = projectile.y - (g.y-24);
    if(Math.sqrt(dx*dx+dy*dy) < 18){
      explode(projectile.x, projectile.y);
      return;
    }
  }
  // out of bounds
  if(projectile.x< -20 || projectile.x> canvas.width+20 || projectile.y< -40){
    // miss, next turn
    projectile=null;
    nextTurn();
    return;
  }
  // continue
  requestAnimationFrame(tick);
}

function explode(x,y){
  // create particles
  let blast = cfg.blastRadius;
  for(let i=0;i<28;i++){
    let ang = Math.random()*Math.PI*2;
    let sp = rand(2,7);
    particles.push({
      x,y,
      vx: Math.cos(ang)*sp,
      vy: Math.sin(ang)*sp - rand(0,3),
      r: rand(3,7),
      life:1,
      color: `hsl(${rand(18,48)},100%,${rand(50,65)}%)`,
      glow: `hsla(${rand(18,35)},100%,60%,0.25)`
    });
  }
  // scorch
  scorchMarks.push({x, r: blast*0.7});
  // check kill
  let killed = null;
  let effectiveRadius = cfg.damageMode==='falloff' ? blast*0.7 : blast;
  gorillas.forEach((g,idx)=>{
    let dx = g.x - x;
    let dy = (g.y-20) - y;
    let dist = Math.sqrt(dx*dx+dy*dy);
    // also consider ground explosion distance to gorilla feet
    if(dist <= effectiveRadius){
      killed = idx;
    }
  });
  projectile=null;
  // animate explosion
  let expLife=0;
  let expMax=22;
  let baseParticles = [...particles];
  function expFrame(){
    expLife++;
    particles.forEach(p=>{
      p.x+=p.vx; p.y+=p.vy; p.vy+=0.22; p.vx*=0.98; p.life-=0.045; p.r*=0.985;
    });
    particles = particles.filter(p=>p.life>0);
    // expanding ring
    if(expLife<expMax){
      requestAnimationFrame(expFrame);
    } else {
      particles=[];
      if(killed!==null){
        // winner is the player who fired (turn)
        let winner = turn;
        // if suicide? if killed is same as turn, then opponent wins
        if(killed===turn){
          winner = turn===0?1:0;
          winDesc.textContent = `Self-destruct! Banana blew up your own gorilla — ${effectiveRadius.toFixed(0)}px blast`;
        } else {
          let dx = gorillas[killed].x - x;
          let dy = (gorillas[killed].y-20)-y;
          let d = Math.sqrt(dx*dx+dy*dy).toFixed(0);
          winDesc.textContent = `${killed===0?'Blue':'Red'} gorilla eliminated — ${d}px from blast center (radius ${blast}px)`;
        }
        score[winner]++;
        winTitle.textContent = `PLAYER ${winner+1} WINS!`;
        winTitle.style.color = winner===0?'#ff3b30':'#2ec4ff';
        winOverlay.classList.add('show');
        gameOver=true;
        try{ beep(440,0.5,'sawtooth',0.25); setTimeout(()=>beep(660,0.4,'square',0.2),180);}catch(e){}
      } else {
        // no kill, next turn
        try{ beep(90,0.25,'sine',0.2);}catch(e){}
        nextTurn();
      }
      updateHUD();
    }
  }
  // draw expanding ring during explosion via temporary overlay in draw loop
  // we hook into main draw by pushing a temporary ring particle
  let ring={x,y,r:4, life:1, color:'rgba(255,210,63,0)', glow:'rgba(255,210,63,0)'};
  // Instead, animate ring via particles loop above with custom draw
  // Quick ring animation using interval
  let ringR=4;
  let ringInt = setInterval(()=>{
    ringR+= blast/14;
    if(ringR>blast) clearInterval(ringInt);
  },30);
  // Override draw to show ring
  let oldDraw = draw;
  let ringDraw = function(){
    oldDraw();
    if(expLife<expMax){
      ctx.strokeStyle=`rgba(255,210,63,${1-expLife/expMax})`;
      ctx.lineWidth=3;
      ctx.beginPath(); ctx.arc(x,y, ringR,0,Math.PI*2); ctx.stroke();
      ctx.fillStyle=`rgba(255,120,0,${0.25*(1-expLife/expMax)})`;
      ctx.beginPath(); ctx.arc(x,y, ringR*0.6,0,Math.PI*2); ctx.fill();
    }
  };
  // Temporarily replace draw for explosion duration
  let savedDraw = draw;
  draw = ringDraw;
  setTimeout(()=>{ draw=savedDraw; }, 700);
  expFrame();
}

function nextTurn(){
  if(gameOver) return;
  turn = turn===0?1:0;
  // wind shift if enabled
  if(cfg.windShift){
    // small random walk + clamp
    let n = wind + rand(-1.8,1.8);
    // occasionally resample fully
    if(Math.random()<0.25) n = rand(cfg.windMin, cfg.windMax);
    wind = Math.max(cfg.windMin, Math.min(cfg.windMax, n));
  }
  computeTrajectory();
  updateHUD();
}

// Audio beep
let audioCtx=null;
function beep(freq,dur,type,vol){
  if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)();
  let o = audioCtx.createOscillator();
  let g = audioCtx.createGain();
  o.type=type; o.frequency.value=freq;
  g.gain.value=vol;
  o.connect(g); g.connect(audioCtx.destination);
  o.start();
  g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime+dur);
  o.stop(audioCtx.currentTime+dur);
}

// Controls bindings
angleSlider.addEventListener('input', e=>{ angle=parseInt(e.target.value); updateHUD(); computeTrajectory(); });
powerSlider.addEventListener('input', e=>{ power=parseInt(e.target.value); updateHUD(); computeTrajectory(); });
document.getElementById('angleMinus').onclick=()=>{ angle=Math.max(0,angle-2); updateHUD(); computeTrajectory(); };
document.getElementById('anglePlus').onclick=()=>{ angle=Math.min(90,angle+2); updateHUD(); computeTrajectory(); };
document.getElementById('powerMinus').onclick=()=>{ power=Math.max(cfg.vMin,power-2); updateHUD(); computeTrajectory(); };
document.getElementById('powerPlus').onclick=()=>{ power=Math.min(cfg.vMax,power+2); updateHUD(); computeTrajectory(); };
fireBtn.onclick=fire;
document.getElementById('randomAimBtn').onclick=()=>{
  angle=randInt(15,75); power=randInt(cfg.vMin+10,cfg.vMax-10);
  updateHUD(); computeTrajectory();
};
document.getElementById('swapBtn').onclick=()=>{
  if(projectile||gameOver) return;
  turn=turn===0?1:0; computeTrajectory(); updateHUD();
};
document.getElementById('newMatchBtn').onclick=()=> startNewMatch(false);
document.getElementById('rematchBtn').onclick=()=> startNewMatch(true);
document.getElementById('keepScoreBtn').onclick=()=> startNewMatch(true);

function startNewMatch(keepScore){
  winOverlay.classList.remove('show');
  gameOver=false;
  projectile=null;
  particles=[];
  if(!keepScore){ /* keep score anyway per spec optional */ }
  round++;
  generateSkyline();
  // alternate starting turn randomly
  turn = Math.random()<0.5?0:1;
  // keep angle/power but recompute
  computeTrajectory();
  updateHUD();
  updateLiveBadge();
}

// Keyboard
window.addEventListener('keydown', e=>{
  if(e.code==='Space'){ e.preventDefault(); fire(); }
  if(e.key==='ArrowLeft' || e.key==='a' || e.key==='A'){ angle=Math.max(0,angle-2); updateHUD(); computeTrajectory(); }
  if(e.key==='ArrowRight' || e.key==='d' || e.key==='D'){ angle=Math.min(90,angle+2); updateHUD(); computeTrajectory(); }
  if(e.key==='ArrowUp' || e.key==='w' || e.key==='W'){ power=Math.min(cfg.vMax,power+2); updateHUD(); computeTrajectory(); }
  if(e.key==='ArrowDown' || e.key==='s' || e.key==='S'){ power=Math.max(cfg.vMin,power-2); updateHUD(); computeTrajectory(); }
});

// Tuning panel logic
const overlay = document.getElementById('tuningOverlay');
document.getElementById('openTuning').onclick=()=>{
  overlay.classList.add('open');
  document.getElementById('openTuning').classList.add('active');
};
document.getElementById('closeTuning').onclick=()=>{
  overlay.classList.remove('open');
  document.getElementById('openTuning').classList.remove('active');
};
overlay.addEventListener('click', e=>{ if(e.target===overlay){ overlay.classList.remove('open'); }});

function bindCfg(){
  const b = document.getElementById('cfgBlast'), bn=document.getElementById('cfgBlastN');
  const g=document.getElementById('cfgGrav'), gn=document.getElementById('cfgGravN');
  const bc=document.getElementById('cfgBCount'), bcn=document.getElementById('cfgBCountN');
  b.value=cfg.blastRadius; bn.value=cfg.blastRadius;
  g.value=cfg.gravity; gn.value=cfg.gravity;
  document.getElementById('cfgWindMin').value=cfg.windMin;
  document.getElementById('cfgWindMax').value=cfg.windMax;
  document.getElementById('cfgWindShift').checked=cfg.windShift;
  bc.value=cfg.buildingCount; bcn.value=cfg.buildingCount;
  document.getElementById('cfgHMin').value=cfg.hMin;
  document.getElementById('cfgHMax').value=cfg.hMax;
  document.getElementById('cfgVMin').value=cfg.vMin;
  document.getElementById('cfgVMax').value=cfg.vMax;
  document.getElementById('cfgDmgInside').checked=cfg.damageMode==='inside';
  document.getElementById('cfgDmgFalloff').checked=cfg.damageMode==='falloff';
  // sync
  b.oninput=()=>{ bn.value=b.value; };
  bn.oninput=()=>{ b.value=bn.value; };
  g.oninput=()=>{ gn.value=g.value; };
  gn.oninput=()=>{ g.value=gn.value; };
  bc.oninput=()=>{ bcn.value=bc.value; };
  bcn.oninput=()=>{ bc.value=bcn.value; };
}
bindCfg();

document.getElementById('applyCfg').onclick=()=>{
  let nb = parseInt(document.getElementById('cfgBlastN').value)||42;
  let ng = parseFloat(document.getElementById('cfgGravN').value)||0.35;
  let wmin = parseFloat(document.getElementById('cfgWindMin').value);
  let wmax = parseFloat(document.getElementById('cfgWindMax').value);
  let wshift = document.getElementById('cfgWindShift').checked;
  let bc = parseInt(document.getElementById('cfgBCountN').value)||10;
  let hmin = parseInt(document.getElementById('cfgHMin').value)||80;
  let hmax = parseInt(document.getElementById('cfgHMax').value)||280;
  let vmin = parseInt(document.getElementById('cfgVMin').value)||1;
  let vmax = parseInt(document.getElementById('cfgVMax').value)||100;
  let dmg = document.getElementById('cfgDmgFalloff').checked ? 'falloff':'inside';
  // clamp
  cfg.blastRadius = Math.max(10,Math.min(120,nb));
  cfg.gravity = Math.max(0.05,Math.min(1.2,ng));
  cfg.windMin = Math.min(wmin,wmax); cfg.windMax=Math.max(wmin,wmax);
  cfg.windShift=wshift;
  cfg.buildingCount=Math.max(8,Math.min(14,bc));
  cfg.hMin=Math.min(hmin,hmax); cfg.hMax=Math.max(hmin,hmax);
  cfg.vMin=Math.min(vmin,vmax); cfg.vMax=Math.max(vmin,vmax);
  cfg.damageMode=dmg;
  // clamp current power/angle to new bounds
  power = Math.max(cfg.vMin, Math.min(cfg.vMax, power));
  // wind re-clamp
  wind = Math.max(cfg.windMin, Math.min(cfg.windMax, wind));
  updateLiveBadge();
  updateHUD();
  computeTrajectory();
  overlay.classList.remove('open');
  document.getElementById('openTuning').classList.remove('active');
  // if building count/height changed, regenerate on next match — but also offer immediate?
  // we keep current skyline until next match per spec (takes effect next skyline) — but update preview
};
document.getElementById('resetCfg').onclick=()=>{
  cfg={...defaults};
  bindCfg();
  updateLiveBadge();
  updateHUD();
  computeTrajectory();
};

// init
generateSkyline();
updateLiveBadge();
updateHUD();

function loop(){
  draw();
  requestAnimationFrame(loop);
}
loop();

// responsive canvas scaling is CSS, but keep internal res fixed
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"df2a93b6-053e-4b03-9cb2-2a64f5db7d9f","tokensIn":46744,"tokensOut":14352,"tokensTotal":61096,"cost":0.0070884139999999995,"turns":3,"toolCalls":2,"failedToolCalls":0,"timestamp":"2026-08-23T01:43:44.496Z"} -->