← Gorillaz results

Gorillaz

Mimo v2.6 Flash · 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,393 bytes · SHA-256 65d3dda9669f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GORILLAZ — Artillery Duel</title>
<style>
  :root{
    --bg:#0b0b16; --panel:#151528; --panel2:#1d1d38; --edge:#3a3a63;
    --ink:#d8d8f0; --dim:#8a8ab5; --p1:#e8483f; --p2:#3f7be8;
    --acc:#7dff5a; --warn:#ffcf3f; --hot:#ff4a6a;
  }
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;background:
    repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 3px), var(--bg);
    color:var(--ink); font-family:"Lucida Console",Monaco,"Courier New",monospace; font-size:13px;}
  body{display:flex;justify-content:center;padding:10px;min-height:100vh;}
  .wrap{width:100%;max-width:1000px;}

  /* ---------- top HUD ---------- */
  .topbar{display:flex;flex-wrap:wrap;gap:6px;align-items:stretch;background:var(--panel);
    border:2px solid var(--edge);box-shadow:0 0 0 2px #000, inset 1px 1px 0 #4a4a80;padding:6px;}
  .logo{background:linear-gradient(180deg,#2a1140,#12082a);border:2px solid #6a3ba0;padding:4px 10px;
    display:flex;flex-direction:column;justify-content:center;min-width:150px}
  .logo b{font-size:20px;letter-spacing:3px;color:#ffd94a;text-shadow:2px 2px 0 #a02a2a,4px 4px 0 #000;line-height:1}
  .logo span{font-size:9px;color:#b48cff;letter-spacing:2px;text-transform:uppercase}
  .cell{background:#0d0d1c;border:2px solid var(--edge);padding:4px 9px;display:flex;flex-direction:column;
    justify-content:center;min-width:96px;flex:1 1 auto}
  .cell .k{font-size:9px;color:var(--dim);letter-spacing:1px;text-transform:uppercase}
  .cell .v{font-size:15px;font-weight:bold;letter-spacing:1px}
  .windv{display:flex;align-items:center;gap:7px}
  .windarrow{font-size:19px;color:var(--acc);text-shadow:0 0 8px rgba(125,255,90,.7);transition:transform .15s}
  .turnv.p1{color:var(--p1)} .turnv.p2{color:var(--p2)}
  .s1{color:var(--p1)} .s2{color:var(--p2)}
  button{font-family:inherit;font-size:12px;letter-spacing:1px;color:var(--ink);background:var(--panel2);
    border:2px solid var(--edge);box-shadow:inset 1px 1px 0 #55558a, 2px 2px 0 #000;padding:6px 10px;cursor:pointer}
  button:hover{background:#282850;color:#fff}
  button:active{box-shadow:inset 2px 2px 0 #000;transform:translate(1px,1px)}
  button:disabled{opacity:.35;cursor:not-allowed}
  .tbtn{min-width:132px;font-weight:bold;color:var(--acc);border-color:#2f7a2a}
  .tbtn.on{background:#123a12;color:#b6ff9a;box-shadow:inset 2px 2px 0 #06180a,2px 2px 0 #000}

  /* ---------- stage ---------- */
  .stage{position:relative;margin-top:8px;border:3px solid var(--edge);box-shadow:0 0 0 2px #000;background:#000;
    aspect-ratio:8/5;}
  canvas{display:block;width:100%;height:100%;image-rendering:pixelated;image-rendering:crisp-edges}
  .overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:14px;background:rgba(6,6,18,.86);text-align:center;padding:20px}
  .overlay.hidden{display:none}
  .ovtitle{font-size:40px;letter-spacing:6px;font-weight:bold;text-shadow:3px 3px 0 #000}
  .ovsub{font-size:13px;color:var(--dim);letter-spacing:2px}
  .ovscore{font-size:18px;letter-spacing:3px}
  .ovbtns{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
  .ovbtns button{font-size:14px;padding:10px 16px;border-color:#7a7ab0}
  .badge{position:absolute;top:8px;left:8px;background:#000a;border:2px solid var(--warn);color:var(--warn);
    font-size:10px;letter-spacing:2px;padding:4px 8px}
  .badge.hidden{display:none}
  .flyind{position:absolute;top:8px;right:8px;background:#000a;border:2px solid var(--hot);color:var(--hot);
    font-size:10px;letter-spacing:1px;padding:4px 8px}
  .flyind.hidden{display:none}

  /* ---------- readouts ---------- */
  .readouts{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
  .ro{flex:1 1 240px;background:var(--panel);border:2px solid var(--edge);padding:6px 9px;box-shadow:inset 1px 1px 0 #4a4a80}
  .ro .top{display:flex;justify-content:space-between;font-size:11px;letter-spacing:1px;color:var(--dim)}
  .ro .top b{font-size:15px;color:#ffe36a}
  .bar{height:14px;background:#0a0a18;border:2px solid #000;margin-top:5px;position:relative;overflow:hidden}
  .bar i{position:absolute;left:0;top:0;bottom:0;display:block}
  .bar.a i{background:repeating-linear-gradient(90deg,#7dff5a 0 6px,#3fae2a 6px 10px)}
  .bar.p i{background:repeating-linear-gradient(90deg,#ffcf3f 0 6px,#c98a10 6px 10px)}
  .bar em{position:absolute;right:4px;top:-1px;font-style:normal;font-size:10px;color:#fff;text-shadow:1px 1px 0 #000}

  /* ---------- controls ---------- */
  .controls{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;align-items:center}
  .controls .grp{display:flex;gap:4px;background:var(--panel);border:2px solid var(--edge);padding:5px}
  .controls .grp span{align-self:center;font-size:10px;color:var(--dim);padding:0 6px;letter-spacing:1px}
  .cbtn{min-width:44px;font-size:15px;font-weight:bold}
  .fire{flex:1 1 200px;font-size:17px;letter-spacing:4px;background:#3a0e18;border-color:#a02a3a;color:#ff8f9a;
    box-shadow:inset 1px 1px 0 #7a2a3a,3px 3px 0 #000;padding:11px}
  .fire:hover{background:#5a1626;color:#fff}
  .hint{margin-top:7px;font-size:10px;color:var(--dim);letter-spacing:1px;line-height:1.7}
  .hint b{color:#b6ff9a}
  .livechip{display:none;align-items:center;gap:6px;background:#0e2a0e;border:2px solid var(--acc);color:var(--acc);
    font-size:10px;letter-spacing:2px;padding:4px 8px;margin-top:8px}
  .livechip.on{display:inline-flex}
  .dot{width:8px;height:8px;background:var(--acc);border-radius:50%;box-shadow:0 0 8px var(--acc);animation:blip 1s infinite}
  @keyframes blip{50%{opacity:.25}}

  /* ---------- tuning panel ---------- */
  #tuning{position:fixed;top:0;right:0;height:100%;width:420px;max-width:100%;background:#10101f;
    border-left:3px solid #6a3ba0;box-shadow:-6px 0 24px #000;overflow-y:auto;z-index:50;padding:0 0 24px;
    transform:translateX(0);transition:transform .18s ease}
  #tuning.hidden{transform:translateX(105%)}
  #tuning .hd{position:sticky;top:0;background:#1c0f38;border-bottom:2px solid #6a3ba0;padding:10px 12px;z-index:2}
  #tuning .hd h2{margin:0;font-size:15px;letter-spacing:3px;color:#ffd94a}
  #tuning .hd p{margin:4px 0 0;font-size:9px;color:#b48cff;letter-spacing:1px}
  #tuning .hd .hdx{position:absolute;right:10px;top:8px;padding:4px 9px;font-size:13px;
    background:#2a1140;border-color:#6a3ba0;color:#ffd94a}
  .livebadge{display:none;margin:10px 12px 0;padding:7px 9px;background:#0e2a0e;border:2px solid var(--acc);
    color:var(--acc);font-size:10px;letter-spacing:2px;align-items:center;gap:8px}
  .livebadge.on{display:flex}
  .trow{border-bottom:1px solid #26264a;padding:9px 12px}
  .trow .lab{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:11px;color:#c9c9ee}
  .trow .lab .tag{font-size:8px;color:var(--warn);border:1px solid var(--warn);padding:1px 4px;letter-spacing:1px}
  .tctl{display:flex;gap:5px;align-items:center;margin-top:6px}
  .tctl button{padding:3px 9px;font-size:14px;font-weight:bold;min-width:32px}
  .tctl input[type=number]{width:78px;background:#07070f;border:2px solid var(--edge);color:#ffe36a;
    font-family:inherit;font-size:14px;padding:4px 5px;text-align:center}
  .tctl input.edited{color:var(--acc);border-color:var(--acc);background:#07180a}
  .tctl .unit{font-size:10px;color:var(--dim)}
  .tctl input[type=range]{flex:1;accent-color:#7dff5a}
  .tctl select{background:#07070f;border:2px solid var(--edge);color:#ffe36a;font-family:inherit;font-size:12px;padding:4px}
  .tctl input[type=checkbox]{width:20px;height:20px;accent-color:#7dff5a}
  .tctl .note{font-size:9px;color:var(--dim);margin-left:auto}
  .tfoot{display:flex;gap:6px;padding:12px;flex-wrap:wrap}
  .tfoot button{flex:1 1 auto;font-size:11px}
  .tinfo{padding:0 12px;font-size:9px;color:var(--dim);line-height:1.7}
  @media (max-width:560px){ body{padding:4px} .logo b{font-size:16px} .ovtitle{font-size:26px} }
</style>
</head>
<body>
<div class="wrap">

  <div class="topbar">
    <div class="logo"><b>GORILLAZ</b><span>artillery duel</span></div>
    <div class="cell"><span class="k">Wind</span><span class="v windv"><i class="windarrow" id="windArrow">▶</i><span id="windVal">0.0</span></span></div>
    <div class="cell" style="flex:2 1 190px"><span class="k">Turn</span><span class="v turnv p1" id="turnVal">PLAYER 1 TURN</span></div>
    <div class="cell"><span class="k">Score P1 / P2</span><span class="v"><span class="s1" id="score1">0</span> : <span class="s2" id="score2">0</span></span></div>
    <div class="cell"><span class="k">Round / Seed</span><span class="v" id="seedVal">1 · 000000</span></div>
    <button class="tbtn" id="btnTune">⚙ LIVE TUNING</button>
    <button id="btnMute" title="sound">♪ ON</button>
  </div>

  <div class="stage">
    <canvas id="cv" width="960" height="600"></canvas>
    <div class="badge hidden" id="pauseBadge">⚙ TUNING OPEN — SIM PAUSED</div>
    <div class="flyind hidden" id="flyInd">BANANA ▲ OFF-SCREEN</div>
    <div class="overlay hidden" id="winOv">
      <div class="ovtitle" id="winTitle">PLAYER 1 WINS!</div>
      <div class="ovsub" id="winSub">DIRECT HIT — BANANA BOMB DETONATION</div>
      <div class="ovscore" id="winScore">P1 0 — 0 P2</div>
      <div class="ovbtns">
        <button id="btnRematch">▶ REMATCH · NEW SKYLINE</button>
        <button id="btnSame">↻ SAME CITY</button>
        <button id="btnScores">RESET SCORES</button>
      </div>
    </div>
  </div>

  <div class="readouts">
    <div class="ro"><div class="top"><span>ANGLE</span><b id="angVal">45°</b></div>
      <div class="bar a"><i id="angBar" style="width:50%"></i><em id="angDir">→ RIGHT</em></div></div>
    <div class="ro"><div class="top"><span>THROW POWER</span><b id="powVal">60</b></div>
      <div class="bar p"><i id="powBar" style="width:60%"></i><em id="powScale">10–100</em></div></div>
    <div class="ro"><div class="top"><span>STATUS</span><b id="statVal">AIMING</b></div>
      <div class="bar" style="background:#0a0a18"><i id="statBar" style="width:0%;background:#7dff5a"></i></div></div>
  </div>

  <div class="controls">
    <div class="grp"><span>ANGLE</span>
      <button class="cbtn" data-act="ang-">−</button><button class="cbtn" data-act="ang+">+</button></div>
    <div class="grp"><span>POWER</span>
      <button class="cbtn" data-act="pow-">−</button><button class="cbtn" data-act="pow+">+</button></div>
    <button class="fire" id="btnFire">🍌 FIRE!</button>
    <button id="btnTune2" style="flex:0 1 auto">⚙ TUNING</button>
  </div>
  <div class="livechip" id="liveChip"><span class="dot"></span> LIVE TUNING ACTIVE — EDITED VALUES IN EFFECT</div>
  <div class="hint">
    KEYS: <b>←/→</b> or <b>A/D</b> angle ±1 (hold SHIFT = ±5) · <b>↑/↓</b> or <b>W/S</b> power · <b>SPACE</b> fire ·
    <b>C</b> live tuning panel · <b>ESC</b> close panel. Two players share one keyboard — take turns.
  </div>
</div>

<aside id="tuning" class="hidden" aria-label="Live tuning panel">
  <div class="hd"><h2>⚙ LIVE TUNING</h2><p>RUNTIME CONFIG — NO RESTART REQUIRED</p>
    <button id="btnCloseTop" class="hdx" title="close panel">✕</button></div>
  <div class="livebadge" id="liveBadge"><span class="dot"></span> LIVE TUNING ACTIVE — <span id="editCount">0</span> VALUE(S) EDITED</div>
  <div id="tuningRows"></div>
  <div class="tfoot">
    <button id="btnRegen">⟳ APPLY / REGEN CITY</button>
    <button id="btnDefaults">⟲ RESET DEFAULTS</button>
    <button id="btnCloseTune">✕ CLOSE</button>
  </div>
  <div class="tinfo">
    Panel is non-blocking: the match state is preserved and the simulation pauses while it is open.
    Physics values apply to the very next projectile launch; skyline values apply on the next city generation.
    Defaults match the classic reference experience.
  </div>
</aside>
<script>
"use strict";
/* ============================================================
   GORILLAZ — two-player turn-based artillery duel
   single-file build · deterministic fixed-step physics
   ============================================================ */

const W = 960, H = 600, GROUND = 540, STEP = 1000/60;

/* ---------------- live configuration layer ---------------- */
const CFG = {
  blastRadius : 70,      // circular damage radius on impact
  gravity     : 0.25,    // downward acceleration (px / frame^2)
  windMin     : -12,     // min wind speed (negative = leftward)
  windMax     : 12,      // max wind speed
  windShift   : true,    // reroll wind between turns (off = constant per match)
  buildingCount: 11,     // skyline structures (classic range 8-14)
  buildHMin   : 80,      // min building height px
  buildHMax   : 340,     // max building height px
  velMin      : 10,      // throw power input lower bound
  velMax      : 100,     // throw power input upper bound
  dmgMode     : 'inside',// 'inside' radius kill | 'falloff' scaled damage
  dmgThreshold: 0.35,    // falloff: fraction of blast needed for a kill
  seed        : 0        // visible reproducible city seed
};
const DEFAULTS = Object.assign({}, CFG);
delete DEFAULTS.seed;
CFG.seed = 100000 + Math.floor(Math.random()*899999);

const FIELDS = [
  {k:'blastRadius',   lab:'Blast / Explosion Radius', min:16, max:240, step:1, unit:'px'},
  {k:'gravity',       lab:'Gravity Strength',         min:0.04,max:0.90,step:0.01,unit:'px/f²'},
  {k:'windMin',       lab:'Wind Variance — MIN',      min:-40, max:0,   step:1, unit:'mph'},
  {k:'windMax',       lab:'Wind Variance — MAX',      min:0,   max:40,  step:1, unit:'mph'},
  {k:'windShift',     lab:'Wind Direction Shifts Allowed', type:'bool'},
  {k:'buildingCount', lab:'Building Count',           min:6,   max:16,  step:1, unit:'bld', next:true},
  {k:'buildHMin',     lab:'Building Height — MIN',    min:40,  max:460, step:5, unit:'px', next:true},
  {k:'buildHMax',     lab:'Building Height — MAX',    min:80,  max:520, step:5, unit:'px', next:true},
  {k:'velMin',        lab:'Projectile Velocity Scale — MIN', min:1,  max:99,  step:1, unit:'pwr'},
  {k:'velMax',        lab:'Projectile Velocity Scale — MAX', min:2,  max:200, step:1, unit:'pwr'},
  {k:'dmgMode',       lab:'Explosion Damage Rule', type:'select',
     options:[['inside','INSIDE RADIUS (classic)'],['falloff','FALLOFF BY DISTANCE']]},
  {k:'dmgThreshold',  lab:'Damage Threshold (falloff)', min:0.05, max:1, step:0.05, unit:'×', only:'falloff'}
];

/* ---------------- helpers ---------------- */
const $ = id => document.getElementById(id);
const clamp = (v,a,b) => v<a?a:(v>b?b:v);
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;};}

/* ---------------- audio ---------------- */
const Audio_ = {
  ctx:null, on:true, windGain:null, windSrc:null, noiseBuf:null,
  init(){
    if(this.ctx) return;
    try{ this.ctx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ return; }
    const c = this.ctx;
    const len = Math.floor(c.sampleRate*2), buf = c.createBuffer(1,len,c.sampleRate), d = buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i] = Math.random()*2-1;
    this.noiseBuf = buf;
    this.windSrc = c.createBufferSource(); this.windSrc.buffer = buf; this.windSrc.loop = true;
    const lp = c.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=420;
    this.windGain = c.createGain(); this.windGain.gain.value = 0;
    this.windSrc.connect(lp); lp.connect(this.windGain); this.windGain.connect(c.destination);
    this.windSrc.start();
  },
  resume(){ this.init(); if(this.ctx && this.ctx.state==='suspended') this.ctx.resume(); },
  tone(f0,f1,dur,type,vol){
    if(!this.on) return; this.resume(); const c=this.ctx; if(!c) return;
    const o=c.createOscillator(), g=c.createGain(), t=c.currentTime;
    o.type=type||'square'; o.frequency.setValueAtTime(f0,t);
    o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t+dur);
    g.gain.setValueAtTime(vol||0.18,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur);
    o.connect(g); g.connect(c.destination); o.start(t); o.stop(t+dur+0.02);
  },
  noise(dur,vol,freq){
    if(!this.on) return; this.resume(); const c=this.ctx; if(!c) return;
    const s=c.createBufferSource(); s.buffer=this.noiseBuf;
    const f=c.createBiquadFilter(); f.type='lowpass'; f.frequency.value=freq||900;
    const g=c.createGain(); const t=c.currentTime;
    g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur);
    s.connect(f); f.connect(g); g.connect(c.destination); s.start(t); s.stop(t+dur+0.02);
  },
  throw_(){ this.tone(760,180,0.28,'square',0.16); this.noise(0.18,0.10,1600); },
  boom(){ this.noise(0.55,0.45,700); this.tone(140,40,0.5,'sawtooth',0.22); },
  win(){ if(!this.on) return; [523,659,784,1046,1318].forEach((f,i)=>setTimeout(()=>this.tone(f,f,0.16,'square',0.15),i*110)); },
  lose(){ if(!this.on) return; [440,392,330,262].forEach((f,i)=>setTimeout(()=>this.tone(f,f*0.9,0.2,'sawtooth',0.14),i*130)); },
  setWind(w){ if(this.windGain&&this.ctx) this.windGain.gain.value = this.on ? Math.min(0.05, Math.abs(w)/40*0.05) : 0; }
};

/* ---------------- pixel sprite ---------------- */
const SPR = [
"...BBBBBBBBBB...",
"..BKKKKKKKKB...",
"..BKWKKKKWKB...",
"..BKWBKKWBKB...",
"..BKKDDDDKKB...",
"...BKDDDDDDKB...",
".BKKKKKKKKKKKKB",
"BKKKKKKKKKKKKKKB",
"BKKKKKKKKKKKKKKB",
"BKDDDDDDDDDDDDKB",
"..BKKKKKKKKKKB..",
"..BKKKKBBKKKKB..",
"..BKKKB..BKKKB..",
"..BKKB....BKKB..",
"..BKB......BKB..",
"..BBBB....BBBB.."];
const SPR_W = 16, SPR_H = SPR.length, SCALE = 3;
const PAL = [ {body:'#e8483f', dark:'#8f2018'}, {body:'#3f7be8', dark:'#1c3f8f'} ];
function drawGorilla(c,g){
  const s = SCALE, x = Math.round(g.x - SPR_W*s/2), y = Math.round(g.y - SPR_H*s);
  const pal = PAL[g.p];
  for(let r=0;r<SPR_H;r++){
    const row = SPR[r];
    for(let col=0;col<SPR_W;col++){
      const ch = row[col]; if(ch==='.') continue;
      c.fillStyle = ch==='B' ? '#12121e' : ch==='W' ? '#f4f4ff' : ch==='D' ? pal.dark : pal.body;
      c.fillRect(x+col*s, y+r*s, s, s);
    }
  }
}

/* ---------------- world state ---------------- */
const cv = $('cv'), ctx = cv.getContext('2d');
let buildings = [], clouds = [], scorches = [], particles = [];
let gorillas = [], turn = 0, state = 'aim';   // aim | flight | boom | over
let angle = 45, power = 60, wind = 0, proj = null, trail = [], boom = null;
let scores = [0,0], round = 1, roundTurns = 0, pendingKill = -1, killMsg = '', flightFrames = 0;
let tuneOpen = false;

function genCity(seed){
  const R = mulberry32(seed>>>0);
  const n = clamp(Math.round(CFG.buildingCount), 4, 20);
  const gaps = []; let gapSum = 0;
  for(let i=0;i<n-1;i++){ const g = 5 + Math.floor(R()*14); gaps.push(g); gapSum += g; }
  const avail = W - gapSum;
  const w = []; let wSum = 0;
  for(let i=0;i<n;i++){ const v = 0.65 + R()*1.0; w.push(v); wSum += v; }
  buildings = [];
  let x = 0;
  const COLORS = ['#c94f4f','#4f7bc9','#c9a44f','#4fc98f','#b45fc9','#c97b4f','#5fc9c9','#8f8fc9',
                  '#6bb44f','#c95f8f','#9aa0ad','#e0d060','#d0604f','#5f8fd0'];
  const hmin = Math.min(CFG.buildHMin, CFG.buildHMax-20);
  const hmax = Math.max(CFG.buildHMax, hmin+20);
  for(let i=0;i<n;i++){
    const bw = Math.max(38, Math.round(avail * w[i]/wSum));
    const bh = Math.round(hmin + R()*(hmax-hmin));
    const b = {x, w:bw, h:bh, y:GROUND-bh, col:COLORS[Math.floor(R()*COLORS.length)], win:[]};
    const cols = Math.max(1, Math.floor((bw-12)/14)), rows = Math.max(1, Math.floor((bh-16)/18));
    for(let r=0;r<rows;r++) for(let c=0;c<cols;c++)
      b.win.push({x:8+c*14, y:10+r*18, lit:R()<0.45});
    buildings.push(b);
    x += bw + (gaps[i]||0);
  }
  // gorilla placement: left third vs right third, never inside blast range of each other
  const third = Math.max(1, Math.floor(n/3));
  const li = Math.floor(R()*third);
  const ri = (n-1) - Math.floor(R()*third);
  placeGorilla(0, buildings[clamp(li,0,n-1)]);
  placeGorilla(1, buildings[clamp(ri,0,n-1)]);
  if(Math.abs(gorillas[0].x - gorillas[1].x) < CFG.blastRadius*2 + 60){
    placeGorilla(0, buildings[0]);
    placeGorilla(1, buildings[n-1]);
  }
  clouds = [];
  for(let i=0;i<6;i++) clouds.push({x:R()*W, y:30+R()*180, s:0.6+R()*0.9, v:0.06+R()*0.12});
}
function placeGorilla(p, b){
  gorillas[p] = {p, x: Math.round(b.x + b.w/2), y: b.y, b, alive:true, face: p===0 ? 1 : -1, dead:0};
}
function newMatch(reseed){
  if(reseed) CFG.seed = 100000 + Math.floor(Math.random()*899999);
  genCity(CFG.seed);
  scorches = []; particles = []; trail = []; proj = null; boom = null; pendingKill = -1; pendingList = [];
  turn = Math.random()<0.5?0:1;
  angle = 45; power = Math.round((CFG.velMin+CFG.velMax)/2);
  roundTurns = 0;
  rollWind(true);
  state = 'aim';
  $('winOv').classList.add('hidden');
  if(tuneOpen) syncTuning();
  updateHUD();
}
function rollWind(force){
  if(CFG.windShift || force){
    const lo = Math.min(CFG.windMin, CFG.windMax), hi = Math.max(CFG.windMin, CFG.windMax);
    wind = Math.round((lo + Math.random()*(hi-lo))*10)/10;
  }
  Audio_.setWind(wind);
}
</script>
<script>
/* ---------------- physics (fixed step, deterministic per turn) ---------------- */
const windAccel = () => wind * 0.008;
const speedOf = p => 4 + p * 0.14;              // power 10→5.4, 100→18 px/frame

function fire(){
  if(state!=='aim' || tuneOpen) return;
  const g = gorillas[turn];
  const a = angle*Math.PI/180, dir = g.face;
  proj = {x: g.x + dir*26, y: g.y - SPR_H*SCALE*0.55,
          vx: Math.cos(a)*speedOf(power)*dir,
          vy: -Math.sin(a)*speedOf(power)};
  trail = []; flightFrames = 0;
  state = 'flight';
  Audio_.throw_();
  burst(proj.x, proj.y, 8, '#ffe36a', 1.4);
  updateHUD();
}

function pointInBuilding(x,y){
  for(const b of buildings) if(x>=b.x && x<=b.x+b.w && y>=b.y && y<=GROUND) return b;
  return null;
}
function hitGorilla(x,y){
  for(const g of gorillas){
    if(!g.alive) continue;
    const gx = g.x - SPR_W*SCALE/2, gy = g.y - SPR_H*SCALE;
    if(x>=gx && x<=gx+SPR_W*SCALE && y>=gy && y<=gy+SPR_H*SCALE) return g;
  }
  return null;
}

function step(){
  // ambient animation
  for(const c of clouds){ c.x += c.v; if(c.x > W+90) c.x = -90; }
  for(let i=particles.length-1;i>=0;i--){
    const p = particles[i];
    p.vy += p.g||0.06; p.x += p.vx; p.y += p.vy; p.life--;
    if(p.life<=0) particles.splice(i,1);
  }
  if(state==='flight' && proj){
    // sub-step integration for accuracy at high power
    const SUB = 3;
    for(let s=0;s<SUB;s++){
      proj.vx += windAccel()/SUB;
      proj.vy += CFG.gravity/SUB;
      proj.x  += proj.vx/SUB;
      proj.y  += proj.vy/SUB;
      if(s===0){ trail.push({x:proj.x,y:proj.y}); if(trail.length>46) trail.shift(); }
      // --- collisions: first contact stops the projectile ---
      const gg = hitGorilla(proj.x, proj.y);
      if(gg){ explode(proj.x, proj.y, true); return; }
      if(proj.y >= GROUND){ explode(proj.x, GROUND, false); return; }
      const b = pointInBuilding(proj.x, proj.y);
      if(b){ explode(proj.x, proj.y, false); return; }
      if(proj.x < -30 || proj.x > W+30){ miss(); return; }
      if(proj.y > H+40){ explode(proj.x, GROUND, false); return; }
    }
    flightFrames++;
    if(flightFrames > 900) miss();
  }
  else if(state==='boom' && boom){
    boom.t++;
    if(boom.t === 1) Audio_.boom();
    if(boom.t >= 34) resolveBoom();
  }
}

function explode(x,y,direct){
  const R = CFG.blastRadius;
  boom = {x,y,t:0,R,direct};
  proj = null;
  state = 'boom';
  // scorch on solid surfaces only
  if(y >= GROUND-2 || pointInBuilding(x,y)) scorches.push({x,y,r:R*0.85});
  // particles
  const cols = ['#ffffff','#ffe36a','#ff9a2a','#ff4a2a','#8a8a8a'];
  for(let i=0;i<42;i++){
    const a = Math.random()*Math.PI*2, sp = 1.5+Math.random()*5.5;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-1.2,life:22+Math.random()*26,
      g:0.10, r:2+Math.random()*5, col:cols[Math.floor(Math.random()*cols.length)]});
  }
  for(let i=0;i<14;i++){
    const a = Math.random()*Math.PI*2, sp = 0.4+Math.random()*1.6;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-0.5,life:40+Math.random()*30,
      g:-0.012, r:6+Math.random()*10, col:'rgba(70,66,74,0.55)'});
  }
  // --- kill evaluation (deterministic, uses live config) ---
  pendingKill = -1; pendingList = [];
  killMsg = direct ? 'DIRECT HIT — BANANA BOMB DETONATION' : 'BLAST KILL — SPLASH DAMAGE';
  for(const g of gorillas){
    if(!g.alive) continue;
    const cx = g.x, cy = g.y - SPR_H*SCALE*0.5;
    const d = Math.hypot(cx-x, cy-y);
    let dead = false;
    if(d <= R){
      if(CFG.dmgMode === 'inside') dead = true;
      else dead = (1 - d/R) >= CFG.dmgThreshold;
    }
    if(dead){
      pendingList.push(g.p);
      pendingKill = g.p;
      if(g.p === turn) killMsg = 'SELF-DIRECTED BLAST — OWN GOAL!';
    }
  }
}
let pendingList = [];
function resolveBoom(){
  boom = null;
  if(pendingList.length === 1){
    const g = gorillas[pendingList[0]];
    g.alive = false; g.dead = 1;
    for(let i=0;i<34;i++){
      const a=Math.random()*Math.PI*2, sp=1+Math.random()*4;
      particles.push({x:g.x,y:g.y-24,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-2,life:34+Math.random()*30,
        g:0.14,r:2+Math.random()*4,col:PAL[pendingList[0]].body});
    }
    // shooter wins — unless the shooter blew himself up
    const winner = pendingList[0] === turn ? 1-turn : turn;
    scores[winner]++;
    state = 'over';
    $('winTitle').textContent = 'PLAYER '+(winner+1)+' WINS!';
    $('winTitle').style.color = winner===0 ? 'var(--p1)' : 'var(--p2)';
    $('winSub').textContent = killMsg;
    $('winScore').textContent = 'P1 '+scores[0]+' — '+scores[1]+' P2   ·   ROUND '+round;
    $('winOv').classList.remove('hidden');
    (winner===0 ? Audio_.win() : Audio_.lose());
    updateHUD();
    pendingList = []; pendingKill = -1;
    return;
  }
  if(pendingList.length > 1){
    state = 'over';
    $('winTitle').textContent = 'DOUBLE KILL!';
    $('winTitle').style.color = '#ffcf3f';
    $('winSub').textContent = 'MUTUAL DESTRUCTION — ROUND VOID';
    $('winScore').textContent = 'P1 '+scores[0]+' — '+scores[1]+' P2   ·   ROUND '+round;
    $('winOv').classList.remove('hidden');
    Audio_.boom();
    updateHUD();
    pendingList = []; pendingKill = -1;
    return;
  }
  pendingList = []; pendingKill = -1;
  // no kill -> next turn, wind may shift
  turn = 1-turn;
  roundTurns++;
  if(CFG.windShift) rollWind(false);
  angle = 45;
  power = clamp(power, CFG.velMin, CFG.velMax);
  state = 'aim';
  updateHUD();
}
function miss(){
  proj = null; trail = [];
  turn = 1-turn; roundTurns++;
  if(CFG.windShift) rollWind(false);
  state = 'aim';
  killMsg = '';
  updateHUD();
}

function burst(x,y,n,col,sp){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:14+Math.random()*12,g:0.05,
      r:2+Math.random()*3,col});
  }
}

/* ---------------- rendering ---------------- */
function render(){
  // sky
  const sky = ctx.createLinearGradient(0,0,0,GROUND);
  sky.addColorStop(0,'#221a4d'); sky.addColorStop(0.45,'#5a3a7a');
  sky.addColorStop(0.8,'#c9644a'); sky.addColorStop(1,'#f0aa55');
  ctx.fillStyle = sky; ctx.fillRect(0,0,W,GROUND);

  // sun
  ctx.save();
  ctx.fillStyle = '#ffd76a';
  ctx.beginPath(); ctx.arc(790,96,44,0,Math.PI*2); ctx.fill();
  ctx.clip();
  ctx.fillStyle = 'rgba(255,110,80,0.55)';
  for(let i=0;i<5;i++) ctx.fillRect(790-46, 96+2+i*9, 92, 4+i*1.4);
  ctx.restore();

  // clouds (pixel blocks)
  for(const c of clouds){
    ctx.fillStyle = 'rgba(255,225,235,0.5)';
    const s = 8*c.s, x = Math.round(c.x), y = Math.round(c.y);
    ctx.fillRect(x, y, s*8, s*2);
    ctx.fillRect(s*1+x, y-s, s*6, s);
    ctx.fillRect(s*2+x, y-s*2, s*3, s);
    ctx.fillRect(s*0.5+x, y+s*2, s*7, s);
  }

  // ground
  ctx.fillStyle = '#2a2a38'; ctx.fillRect(0,GROUND,W,H-GROUND);
  ctx.fillStyle = '#3d3d52'; ctx.fillRect(0,GROUND,W,6);
  ctx.fillStyle = '#1e1e2a';
  for(let x=0;x<W;x+=24) ctx.fillRect(x, GROUND+16, 14, 4);

  // buildings
  for(const b of buildings){
    ctx.fillStyle = b.col; ctx.fillRect(b.x, b.y, b.w, b.h);
    ctx.fillStyle = 'rgba(0,0,0,0.28)'; ctx.fillRect(b.x+b.w-8, b.y, 8, b.h);
    ctx.fillStyle = 'rgba(255,255,255,0.30)'; ctx.fillRect(b.x, b.y, b.w, 5);
    ctx.fillStyle = '#171722'; ctx.fillRect(b.x, b.y-5, b.w, 5);
    for(const w of b.win){
      ctx.fillStyle = w.lit ? '#ffe9a0' : '#2b2b3e';
      ctx.fillRect(b.x+w.x, b.y+w.y, 8, 11);
    }
  }

  // scorch markers (clipped to solid geometry)
  if(scorches.length){
    ctx.save(); ctx.beginPath();
    ctx.rect(0,GROUND,W,H-GROUND);
    for(const b of buildings) ctx.rect(b.x,b.y,b.w,b.h);
    ctx.clip();
    for(const s of scorches){
      const g = ctx.createRadialGradient(s.x,s.y,0,s.x,s.y,s.r);
      g.addColorStop(0,'rgba(10,8,10,0.75)'); g.addColorStop(1,'rgba(10,8,10,0)');
      ctx.fillStyle = g;
      ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
    }
    ctx.restore();
  }

  // aim indicator
  if(state==='aim' && !tuneOpen){
    const g = gorillas[turn], a = angle*Math.PI/180, dir = g.face;
    const ox = g.x + dir*26, oy = g.y - SPR_H*SCALE*0.55;
    ctx.strokeStyle = turn===0 ? 'rgba(232,72,63,.9)' : 'rgba(63,123,232,.9)';
    ctx.lineWidth = 3; ctx.setLineDash([7,6]);
    ctx.beginPath(); ctx.moveTo(ox,oy);
    ctx.lineTo(ox + Math.cos(a)*dir*46, oy - Math.sin(a)*46); ctx.stroke();
    ctx.setLineDash([]);
    ctx.fillStyle = '#fff';
    ctx.fillRect(ox + Math.cos(a)*dir*46-3, oy - Math.sin(a)*46-3, 6, 6);
  }

  // gorillas
  for(const g of gorillas) if(g.alive) drawGorilla(ctx,g);

  // trail
  for(let i=0;i<trail.length;i++){
    const t = trail[i], f = i/trail.length;
    ctx.fillStyle = 'rgba(255,'+Math.round(150+80*f)+',60,'+(0.12+0.6*f)+')';
    ctx.fillRect(t.x-2, t.y-2, 4, 4);
  }

  // projectile — banana bomb
  if(proj){
    ctx.save();
    ctx.translate(proj.x, proj.y);
    ctx.rotate(Math.atan2(proj.vy, proj.vx));
    ctx.fillStyle = '#ffe36a';
    ctx.beginPath(); ctx.arc(0,0,6,Math.PI*0.15,Math.PI*1.1); ctx.lineTo(0,0); ctx.fill();
    ctx.fillStyle = '#7a5a10'; ctx.fillRect(-7,-2,3,4);
    ctx.restore();
    ctx.strokeStyle = 'rgba(255,227,106,0.5)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(proj.x,proj.y);
    ctx.lineTo(proj.x - proj.vx*4, proj.y - proj.vy*4); ctx.stroke();
  }

  // explosion ring
  if(boom){
    const p = Math.min(1, boom.t/22), r = boom.R*(0.25+p*1.15);
    const g = ctx.createRadialGradient(boom.x,boom.y,0,boom.x,boom.y,r);
    g.addColorStop(0,'rgba(255,255,255,'+(0.95*(1-p*0.7))+')');
    g.addColorStop(0.45,'rgba(255,205,60,'+(0.85*(1-p))+')');
    g.addColorStop(0.8,'rgba(255,80,40,'+(0.6*(1-p))+')');
    g.addColorStop(1,'rgba(60,20,20,0)');
    ctx.fillStyle = g; ctx.beginPath(); ctx.arc(boom.x,boom.y,r,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,'+(1-p)+')'; ctx.lineWidth = 3;
    ctx.beginPath(); ctx.arc(boom.x,boom.y,r*1.12,0,Math.PI*2); ctx.stroke();
    if(CFG.dmgMode==='inside'){
      ctx.strokeStyle = 'rgba(125,255,90,'+(0.5*(1-p))+')'; ctx.lineWidth = 1.5;
      ctx.setLineDash([5,5]);
      ctx.beginPath(); ctx.arc(boom.x,boom.y,CFG.blastRadius,0,Math.PI*2); ctx.stroke();
      ctx.setLineDash([]);
    }
  }

  // particles
  for(const p of particles){
    ctx.globalAlpha = Math.max(0, Math.min(1, p.life/30));
    ctx.fillStyle = p.col;
    ctx.fillRect(p.x-p.r/2, p.y-p.r/2, p.r, p.r);
  }
  ctx.globalAlpha = 1;

  // off-screen projectile marker
  const off = proj && (proj.x<0||proj.x>W||proj.y<0);
  $('flyInd').classList.toggle('hidden', !off);
  if(off){
    const px = clamp(proj.x, 14, W-14), py = clamp(proj.y, 14, H-14);
    ctx.fillStyle = '#ff4a6a';
    ctx.beginPath();
    const dx = proj.x-px, dy = proj.y-py, len = Math.hypot(dx,dy)||1;
    ctx.save(); ctx.translate(px,py); ctx.rotate(Math.atan2(dy,dx));
    ctx.beginPath(); ctx.moveTo(9,0); ctx.lineTo(-6,-6); ctx.lineTo(-6,6); ctx.closePath(); ctx.fill();
    ctx.restore();
  }

  // frame vignette
  ctx.strokeStyle = 'rgba(0,0,0,0.35)'; ctx.lineWidth = 6;
  ctx.strokeRect(3,3,W-6,H-6);
}
</script>
<script>
/* ---------------- HUD ---------------- */
function updateHUD(){
  const dir = wind >= 0 ? '▶' : '◀';
  $('windArrow').textContent = dir;
  $('windArrow').style.transform = 'scaleX('+(wind>=0?1:-1)+')';
  $('windVal').textContent = Math.abs(wind).toFixed(1)+' mph';
  const tv = $('turnVal');
  tv.textContent = state==='over' ? 'ROUND OVER' : 'PLAYER '+(turn+1)+' TURN';
  tv.className = 'v turnv '+(turn===0?'p1':'p2');
  $('score1').textContent = scores[0];
  $('score2').textContent = scores[1];
  $('seedVal').textContent = round+' · '+CFG.seed;

  $('angVal').textContent = angle+'°';
  $('angBar').style.width = (angle/90*100)+'%';
  $('angDir').textContent = gorillas[turn] && gorillas[turn].face===1 ? '→ RIGHT' : '← LEFT';
  $('powVal').textContent = power;
  $('powBar').style.width = ((power-CFG.velMin)/Math.max(1,(CFG.velMax-CFG.velMin))*100)+'%';
  $('powScale').textContent = CFG.velMin+'–'+CFG.velMax;

  const stat = {aim:'AIMING',flight:'BANANA IN FLIGHT',boom:'DETONATION',over:'ROUND OVER'}[state];
  $('statVal').textContent = stat;
  $('statBar').style.width = state==='flight' ? Math.min(100, flightFrames/6)+'%' : (state==='boom'?'100%':'0%');
  $('btnFire').disabled = !(state==='aim' && !tuneOpen);
  document.querySelectorAll('.cbtn').forEach(b=>b.disabled = !(state==='aim' && !tuneOpen));
}

/* ---------------- input ---------------- */
function adjAngle(d){
  if(state!=='aim'||tuneOpen) return;
  angle = clamp(angle+d, 0, 90); updateHUD();
}
function adjPower(d){
  if(state!=='aim'||tuneOpen) return;
  power = clamp(power+d, CFG.velMin, CFG.velMax); updateHUD();
}
document.addEventListener('keydown', e=>{
  if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT') return;
  const k = e.key, sh = e.shiftKey ? 5 : 1;
  if(k==='ArrowLeft'||k==='a'||k==='A'){ adjAngle(-sh); e.preventDefault(); }
  else if(k==='ArrowRight'||k==='d'||k==='D'){ adjAngle(sh); e.preventDefault(); }
  else if(k==='ArrowUp'||k==='w'||k==='W'){ adjPower(sh); e.preventDefault(); }
  else if(k==='ArrowDown'||k==='s'||k==='S'){ adjPower(-sh); e.preventDefault(); }
  else if(k===' '||k==='Enter'){ fire(); e.preventDefault(); }
  else if(k==='c'||k==='C'){ toggleTune(); }
  else if(k==='Escape'){ if(tuneOpen) toggleTune(false); }
});
document.querySelectorAll('[data-act]').forEach(b=>{
  b.addEventListener('click', ()=>{
    const a = b.dataset.act;
    if(a==='ang-') adjAngle(-1); if(a==='ang+') adjAngle(1);
    if(a==='pow-') adjPower(-1); if(a==='pow+') adjPower(1);
  });
});
$('btnFire').addEventListener('click', ()=>{ Audio_.resume(); fire(); });
$('btnMute').addEventListener('click', ()=>{
  Audio_.on = !Audio_.on; $('btnMute').textContent = Audio_.on ? '♪ ON' : '♪ OFF';
  Audio_.setWind(wind);
});

/* ---------------- live tuning panel ---------------- */
function isEdited(k){
  if(k==='seed') return false;
  return CFG[k] !== DEFAULTS[k];
}
function editedKeys(){ return FIELDS.filter(f=>isEdited(f.k)).map(f=>f.k); }

function buildTuning(){
  const host = $('tuningRows'); host.innerHTML = '';
  FIELDS.forEach(f=>{
    const row = document.createElement('div'); row.className='trow'; row.dataset.k=f.k;
    const lab = document.createElement('div'); lab.className='lab';
    lab.innerHTML = '<span>'+f.lab+'</span>' + (f.next?'<span class="tag">NEXT SKYLINE</span>':'')
                  + (f.only?'<span class="tag" data-only="1">FALLOFF MODE</span>':'');
    row.appendChild(lab);
    const ctl = document.createElement('div'); ctl.className='tctl';

    if(f.type==='bool'){
      const cb = document.createElement('input'); cb.type='checkbox'; cb.checked = !!CFG[f.k];
      cb.addEventListener('change', ()=>{ CFG[f.k]=cb.checked; syncTuning(); });
      const l = document.createElement('span'); l.className='unit'; l.textContent='ENABLED / DISABLED';
      ctl.append(cb,l);
    } else if(f.type==='select'){
      const sel = document.createElement('select');
      f.options.forEach(([v,t])=>{ const o=document.createElement('option'); o.value=v; o.textContent=t; sel.appendChild(o); });
      sel.value = CFG[f.k];
      sel.addEventListener('change', ()=>{ CFG[f.k]=sel.value; syncTuning(); });
      ctl.appendChild(sel);
    } else {
      const dec = document.createElement('button'); dec.textContent='−';
      const num = document.createElement('input'); num.type='number';
      num.min=f.min; num.max=f.max; num.step=f.step; num.value=CFG[f.k];
      const inc = document.createElement('button'); inc.textContent='+';
      const unit = document.createElement('span'); unit.className='unit'; unit.textContent=f.unit||'';
      const rng = document.createElement('input'); rng.type='range';
      rng.min=f.min; rng.max=f.max; rng.step=f.step; rng.value=CFG[f.k]; rng.style.flex='1 1 90px';
      const apply = v=>{
        v = clamp(parseFloat(v)||f.min, f.min, f.max);
        if(f.step>=1) v = Math.round(v); else v = Math.round(v/f.step)*f.step;
        CFG[f.k] = v; syncTuning();
      };
      dec.addEventListener('click', ()=>apply(CFG[f.k]-f.step));
      inc.addEventListener('click', ()=>apply(CFG[f.k]+f.step));
      num.addEventListener('change', ()=>apply(num.value));
      num.addEventListener('focus', ()=>num.select());
      rng.addEventListener('input', ()=>apply(rng.value));
      ctl.append(dec,num,inc,unit,rng);
    }
    row.appendChild(ctl);
    host.appendChild(row);
  });

  /* city seed row */
  const row = document.createElement('div'); row.className='trow'; row.dataset.k='seed';
  row.innerHTML = '<div class="lab"><span>City Seed (reproducible skyline)</span></div>';
  const ctl = document.createElement('div'); ctl.className='tctl';
  const num = document.createElement('input'); num.type='number'; num.value=CFG.seed; num.step=1;
  const go = document.createElement('button'); go.textContent='⟳';
  const note = document.createElement('span'); note.className='note'; note.textContent='regenerate';
  num.addEventListener('change', ()=>{ CFG.seed = Math.abs(Math.round(num.value||1))%1000000; });
  go.addEventListener('click', ()=>{ CFG.seed = Math.abs(Math.round(num.value||1))%1000000; regenerate(); });
  ctl.append(num,go,note); row.appendChild(ctl);
  host.appendChild(row);
  syncTuning();
}

function syncTuning(){
  // cross-field validation
  if(CFG.windMin > CFG.windMax){ const t=CFG.windMin; CFG.windMin=CFG.windMax; CFG.windMax=t; }
  if(CFG.buildHMin > CFG.buildHMax) CFG.buildHMax = CFG.buildHMin + 20;
  if(CFG.velMin >= CFG.velMax) CFG.velMax = CFG.velMin + 1;
  power = clamp(power, CFG.velMin, CFG.velMax);

  document.querySelectorAll('#tuningRows .trow').forEach(row=>{
    const k = row.dataset.k, f = FIELDS.find(x=>x.k===k);
    const num = row.querySelector('input[type=number]');
    if(num && document.activeElement!==num) num.value = CFG[k];
    if(num) num.classList.toggle('edited', isEdited(k));
    const rng = row.querySelector('input[type=range]');
    if(rng && document.activeElement!==rng) rng.value = CFG[k];
    const cb = row.querySelector('input[type=checkbox]'); if(cb) cb.checked = !!CFG[k];
    const sel = row.querySelector('select'); if(sel) sel.value = CFG[k];
    if(f && f.only) row.style.display = CFG.dmgMode==='falloff' ? '' : 'none';
    if(f && f.k==='dmgThreshold') row.style.display = CFG.dmgMode==='falloff' ? '' : 'none';
  });

  const n = editedKeys().length;
  $('liveBadge').classList.toggle('on', n>0);
  $('editCount').textContent = n;
  $('liveChip').classList.toggle('on', n>0);
  $('btnTune').classList.toggle('on', tuneOpen);
  updateHUD();
}

function toggleTune(force){
  tuneOpen = (typeof force==='boolean') ? force : !tuneOpen;
  $('tuning').classList.toggle('hidden', !tuneOpen);
  $('pauseBadge').classList.toggle('hidden', !tuneOpen);
  syncTuning();
  updateHUD();
}
$('btnTune').addEventListener('click', ()=>toggleTune());
$('btnTune2').addEventListener('click', ()=>toggleTune());
$('btnCloseTune').addEventListener('click', ()=>toggleTune(false));
$('btnCloseTop').addEventListener('click', ()=>toggleTune(false));
$('btnDefaults').addEventListener('click', ()=>{
  Object.assign(CFG, DEFAULTS); buildTuning();
});
$('btnRegen').addEventListener('click', ()=>regenerate());
function regenerate(){ genCity(CFG.seed); scorches=[]; particles=[]; if(state==='over'){} updateHUD(); }

/* ---------------- session flow ---------------- */
$('btnRematch').addEventListener('click', ()=>{ round++; newMatch(true); });
$('btnSame').addEventListener('click', ()=>{ round++; newMatch(false); });
$('btnScores').addEventListener('click', ()=>{ scores=[0,0]; round=1; updateHUD(); });

/* ---------------- main loop ---------------- */
let acc = 0, last = performance.now();
function frame(now){
  let dt = now - last; last = now;
  if(dt > 250) dt = 250;
  if(!tuneOpen && state!=='over') acc += dt;
  let guard = 0;
  while(acc >= STEP && guard++ < 5){ step(); acc -= STEP; }
  render();
  requestAnimationFrame(frame);
}

/* ---------------- boot ---------------- */
newMatch(false);
buildTuning();
syncTuning();
updateHUD();
document.addEventListener('pointerdown', ()=>Audio_.resume(), {once:true});
requestAnimationFrame(frame);

/* debug / test hooks */
window.GZ = {
  get state(){return state}, get turn(){return turn}, get wind(){return wind},
  get angle(){return angle}, get power(){return power}, get scores(){return scores.slice()},
  CFG, fire, adjAngle, adjPower, toggleTune, newMatch,
  aim(a,p){ angle = clamp(a,0,90); power = clamp(p, CFG.velMin, CFG.velMax); updateHUD(); },
  simulate(n){ for(let i=0;i<n && state!=='over';i++) step(); render(); },
  reset(){ newMatch(false); },
  get gorillas(){return gorillas.map(g=>({p:g.p,x:g.x,y:g.y,alive:g.alive}))},
  get proj(){return proj ? {x:proj.x,y:proj.y} : null},
  get buildings(){return buildings.length}
};
</script>
</body>
</html>
<!-- agent-meta {"model":"xiaomi/mimo-v2.6-flash","provider":"openrouter","persona":"full","sessionId":"2e632355-6a3a-48f0-89dd-7ed5f3513d1f","tokensIn":4951496,"tokensOut":88394,"tokensTotal":5039890,"cost":0.1582803488,"turns":59,"toolCalls":63,"failedToolCalls":2,"timestamp":"2026-09-26T17:25:55.635Z"} -->