← Gorillaz results

Gorillaz

Deepseek v4.1 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 51,746 bytes · SHA-256 1b350475c5f6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GORILLAZ — Banana Bomb Artillery</title>
<style>
  :root{
    --p1:#ff5252; --p2:#4d9dff;
    --ink:#e9e9f5; --panel:#12172a; --edge:#2b3255;
    --accent:#ffd166; --live:#54e39a;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;min-height:100%;}
  body{
    background:
      radial-gradient(1200px 600px at 50% -10%, #1a1f3d 0%, #0a0c17 60%, #06070d 100%);
    color:var(--ink);
    font-family:"Courier New", ui-monospace, SFMono-Regular, Menlo, monospace;
    -webkit-font-smoothing:none;
    overflow-x:hidden;
  }
  #app{
    display:flex; flex-direction:column; align-items:center;
    gap:9px; padding:10px 10px 26px;
  }
  .stage{
    position:relative;
    width:min(97vw, 1000px);
    aspect-ratio:1000 / 625;
    border:3px solid var(--edge);
    border-radius:7px;
    overflow:hidden;
    background:#05060c;
    box-shadow:0 0 0 3px #0c0f1c, 0 18px 50px rgba(0,0,0,.75), 0 0 60px rgba(80,110,255,.10);
    flex:0 0 auto;
  }
  canvas{display:block; width:100%; height:100%; image-rendering:pixelated;}
  .scan{
    position:absolute; inset:0; pointer-events:none; z-index:6;
    background:repeating-linear-gradient(to bottom, rgba(0,0,0,.20) 0 1px, transparent 1px 3px);
    opacity:.55; mix-blend-mode:multiply;
  }
  .vig{position:absolute;inset:0;pointer-events:none;z-index:5;
    background:radial-gradient(120% 100% at 50% 45%, transparent 55%, rgba(0,0,0,.55) 100%);}

  /* ---------- HUD ---------- */
  .hud-top{
    position:absolute; top:0; left:0; right:0; z-index:4;
    display:flex; justify-content:space-between; align-items:flex-start;
    gap:8px; padding:7px 10px; pointer-events:none;
    font-size:clamp(8.5px, 1.42vw, 14px); letter-spacing:.09em; font-weight:bold;
  }
  .chip{
    background:rgba(8,10,20,.72); border:2px solid var(--edge); border-radius:4px;
    padding:4px 9px; text-shadow:0 2px 0 rgba(0,0,0,.65); white-space:nowrap;
    backdrop-filter:blur(2px);
  }
  .turn-chip{border-color:var(--p1); color:#fff; transition:border-color .2s, color .2s;}
  .turn-chip.p2{border-color:var(--p2);}
  .turn-chip small{display:block;font-size:.72em;opacity:.6;letter-spacing:.14em;font-weight:normal;}
  .center-hud{display:flex;flex-direction:column;align-items:center;gap:4px;}
  .wind-box{display:flex;align-items:center;gap:7px;}
  .wind-arrow{
    display:inline-block; font-size:1.35em; line-height:1;
    color:var(--accent); transform-origin:50% 50%;
    transition:transform .35s cubic-bezier(.2,1.4,.4,1), color .3s;
    text-shadow:0 0 10px rgba(255,209,102,.55);
  }
  .wind-bar{width:80px;height:6px;background:#0a0d18;border:1px solid var(--edge);position:relative;}
  .wind-bar i{position:absolute;top:0;bottom:0;background:var(--accent);opacity:.85;}
  .score-box{display:flex;align-items:center;gap:8px;}
  .score-box .s1{color:var(--p1);} .score-box .s2{color:var(--p2);}
  .score-box b{font-size:1.45em;}
  .ro-label{font-size:.74em;opacity:.62;letter-spacing:.18em;}

  /* ---------- bottom bar ---------- */
  .bar{width:min(97vw,1000px);display:flex;gap:8px;flex-wrap:wrap;align-items:stretch;}
  .readout{
    flex:1 1 190px; min-width:165px;
    background:linear-gradient(180deg,#151b30,#0e1222);
    border:2px solid var(--edge); border-radius:5px; padding:6px 10px 8px;
    display:flex; flex-direction:column; gap:4px;
  }
  .ro-top{display:flex;justify-content:space-between;align-items:baseline;gap:6px;}
  .ro-val{font-size:clamp(16px,2.4vw,24px);font-weight:bold;color:#fff;text-shadow:0 0 12px rgba(120,170,255,.35);}
  .ro-val span{font-size:.55em;opacity:.65;}
  .meter{height:9px;background:#080b14;border:1px solid var(--edge);overflow:hidden;}
  .meter i{display:block;height:100%;width:0%;transition:width .08s linear;}
  .meter.ang i{background:linear-gradient(90deg,#6d8cff,#c9d6ff);}
  .meter.pow i{background:linear-gradient(90deg,#ffb347,#ff5252);}
  .keys{font-size:10px;opacity:.5;letter-spacing:.06em;}
  .btn{
    appearance:none; font-family:inherit; font-weight:bold; letter-spacing:.1em;
    background:linear-gradient(180deg,#1d2440,#111629); color:var(--ink);
    border:2px solid var(--edge); border-radius:5px; padding:8px 12px; cursor:pointer;
    font-size:clamp(9px,1.3vw,13px); text-transform:uppercase;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.07), 0 3px 0 #05070f;
    transition:transform .05s, filter .15s;
    user-select:none; touch-action:manipulation;
  }
  .btn:hover{filter:brightness(1.25);}
  .btn:active{transform:translateY(2px); box-shadow:inset 0 1px 0 rgba(255,255,255,.07), 0 1px 0 #05070f;}
  .btn:disabled{opacity:.35;cursor:not-allowed;transform:none;}
  .btn.fire{border-color:#ff8a3d; color:#ffd9a8; background:linear-gradient(180deg,#3a2013,#20120b);}
  .btn.tune{border-color:#4b7a6a; color:#b8f0d8;}
  .pad{display:flex;gap:2px;}
  .pad .btn{padding:8px 10px;min-width:40px;}
  .hint{width:min(97vw,1000px);font-size:10.5px;opacity:.42;letter-spacing:.05em;text-align:center;line-height:1.5;}

  /* ---------- banner ---------- */
  .banner{
    position:absolute; inset:0; z-index:8; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:12px; text-align:center;
    background:radial-gradient(circle at 50% 45%, rgba(10,12,22,.82), rgba(4,5,10,.95));
    backdrop-filter:blur(3px); padding:16px;
  }
  .banner.hidden{display:none;}
  .banner h2{
    margin:0; font-size:clamp(20px,4.4vw,44px); letter-spacing:.14em; color:#fff;
    text-shadow:0 0 24px currentColor, 0 4px 0 rgba(0,0,0,.6);
  }
  .banner .sub{font-size:clamp(9px,1.5vw,14px); letter-spacing:.22em; opacity:.75;}
  .banner .btns{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-top:6px;}
  .banner .trophy{font-size:clamp(28px,6vw,58px);line-height:1;filter:drop-shadow(0 0 18px rgba(255,209,102,.6));}
  .flash{animation:pop .5s cubic-bezier(.2,1.6,.4,1);}
  @keyframes pop{0%{transform:scale(.7);opacity:0}100%{transform:scale(1);opacity:1}}

  /* ---------- config panel ---------- */
  #cfgPanel{
    position:fixed; top:0; right:0; bottom:0; width:min(360px,94vw); z-index:20;
    background:linear-gradient(180deg,#101526,#0a0d18);
    border-left:3px solid var(--edge);
    box-shadow:-14px 0 44px rgba(0,0,0,.75);
    transform:translateX(102%); transition:transform .22s ease-out;
    display:flex; flex-direction:column;
  }
  #cfgPanel.open{transform:translateX(0);}
  .cfg-head{
    padding:11px 13px; border-bottom:2px solid var(--edge); display:flex;
    flex-direction:column; gap:7px; flex:0 0 auto; background:#0d1122;
  }
  .cfg-head .t{display:flex;justify-content:space-between;align-items:center;gap:8px;}
  .cfg-head .t b{letter-spacing:.16em;font-size:13px;}
  .badge{
    font-size:9px; letter-spacing:.14em; padding:3px 7px; border-radius:3px;
    border:1px solid #35406b; color:#7f8ac0; background:#0a0e1c; white-space:nowrap;
  }
  .badge.on{color:#06130d; background:var(--live); border-color:var(--live);
    box-shadow:0 0 14px rgba(84,227,154,.6); animation:pulse 1.6s ease-in-out infinite;}
  @keyframes pulse{0%,100%{box-shadow:0 0 10px rgba(84,227,154,.4)}50%{box-shadow:0 0 20px rgba(84,227,154,.85)}}
  .cfg-body{overflow-y:auto; padding:10px 13px 30px; flex:1 1 auto;}
  .cfg-group{margin-bottom:14px;}
  .cfg-group h3{
    margin:0 0 7px; font-size:10px; letter-spacing:.22em; color:#8f9bd6;
    border-bottom:1px dashed #2b3255; padding-bottom:4px; font-weight:bold;
  }
  .cfg-item{
    display:flex; flex-direction:column; gap:3px; padding:5px 6px 8px;
    border-left:2px solid transparent; border-radius:3px; transition:background .15s;
  }
  .cfg-item.changed{background:rgba(84,227,154,.055); border-left-color:var(--live);}
  .cfg-item.deferred.changed{background:rgba(255,209,102,.06); border-left-color:var(--accent);}
  .cfg-item .top{display:flex;justify-content:space-between;align-items:center;gap:6px;font-size:10.5px;letter-spacing:.07em;}
  .cfg-item .top .nm{opacity:.85;}
  .cfg-item .top .nm i{font-style:normal;color:var(--accent);font-size:.86em;margin-left:4px;}
  .cfg-item input[type=number]{
    width:64px; font-family:inherit; font-size:11px; font-weight:bold; text-align:right;
    background:#070a14; color:#fff; border:1px solid #2b3255; border-radius:3px; padding:2px 5px;
  }
  .cfg-item.changed input[type=number]{border-color:var(--live);color:var(--live);}
  .cfg-item input[type=range]{width:100%; accent-color:#6d8cff; height:16px;}
  .cfg-item.changed input[type=range]{accent-color:var(--live);}
  .cfg-item select{
    font-family:inherit;font-size:11px;background:#070a14;color:#fff;border:1px solid #2b3255;
    border-radius:3px;padding:3px;width:100%;
  }
  .cfg-item .cur{font-size:9.5px;opacity:.5;letter-spacing:.06em;}
  .switch{display:flex;align-items:center;gap:8px;font-size:10.5px;letter-spacing:.07em;padding:5px 6px;}
  .switch input{width:16px;height:16px;accent-color:var(--live);}
  .cfg-note{font-size:9.5px;opacity:.5;line-height:1.55;letter-spacing:.04em;
    border:1px dashed #2b3255;padding:7px;border-radius:4px;margin-top:6px;}
  .cfg-foot{flex:0 0 auto;padding:9px 13px;border-top:2px solid var(--edge);display:flex;gap:7px;background:#0d1122;}
  .cfg-foot .btn{flex:1;text-align:center;padding:9px 6px;}
  #cfgPanel::-webkit-scrollbar{width:9px;}
  #cfgPanel::-webkit-scrollbar-thumb{background:#242c4c;}
  #openCfg{align-self:stretch;}
  @media (max-width:720px){
    .keys{display:none;}
    .hud-top{font-size:9px;padding:6px 6px 0;}
    .chip{padding:3px 6px;}
    .chip small{font-size:.68em;}
    .wind-bar{width:44px;}
    .btn{padding:7px 9px;}
  }
</style>
</head>
<body>
<div id="app">
  <div class="stage">
    <canvas id="cv" width="1000" height="625"></canvas>
    <div class="vig"></div>
    <div class="scan"></div>

    <div class="hud-top">
      <div class="chip turn-chip" id="turnChip">
        <small>NOW THROWING</small>PLAYER 1 TURN
      </div>
      <div class="center-hud">
        <div class="chip wind-box">
          <span>WIND</span>
          <span class="wind-arrow" id="windArrow">➤</span>
          <b id="windVal">0</b>
          <span id="windUnit">MPH</span>
          <div class="wind-bar"><i id="windBar"></i></div>
        </div>
      </div>
      <div class="chip score-box">
        <span class="s1">P1 <b id="s1">0</b></span>
        <span style="opacity:.4">/</span>
        <span class="s2"><b id="s2">0</b> P2</span>
      </div>
    </div>

    <div class="banner hidden" id="banner">
      <div class="trophy" id="bTrophy">🏆</div>
      <h2 id="bTitle">PLAYER 1 WINS</h2>
      <div class="sub" id="bSub">DIRECT HIT — ROUND 1</div>
      <div class="btns" id="bBtns"></div>
    </div>
  </div>

  <div class="bar">
    <div class="readout">
      <div class="ro-top"><span class="ro-label">ANGLE</span><span class="ro-val" id="roAngle">45<span>°</span></span></div>
      <div class="meter ang"><i id="mAngle"></i></div>
      <div class="keys">◀ ▶ or A/D — SHIFT = ×5</div>
    </div>
    <div class="readout">
      <div class="ro-top"><span class="ro-label">POWER</span><span class="ro-val" id="roPower">70<span>%</span></span></div>
      <div class="meter pow"><i id="mPower"></i></div>
      <div class="keys">▲ ▼ or W/S — SHIFT = ×5</div>
    </div>
    <div class="readout" style="flex:0 1 auto;min-width:0;justify-content:center;gap:6px;">
      <div class="ro-top" style="justify-content:center;"><span class="ro-label">FIRE</span></div>
      <div style="display:flex;gap:6px;align-items:center;">
        <button class="btn fire" id="fireBtn" style="padding:11px 18px;">THROW 🍌</button>
      </div>
    </div>
    <div class="readout" style="flex:0 1 auto;min-width:0;justify-content:center;">
      <div class="ro-top" style="justify-content:center;"><span class="ro-label">CTRL</span></div>
      <div style="display:flex;gap:6px;align-items:center;justify-content:center;">
        <div class="pad"><button class="btn" data-hold="angle:-1" >◀</button><button class="btn" data-hold="angle:1">▶</button></div>
        <div class="pad"><button class="btn" data-hold="power:-1">▼</button><button class="btn" data-hold="power:1">▲</button></div>
      </div>
    </div>
    <button class="btn tune" id="openCfg" style="flex:0 0 auto;padding:10px 14px;">⚙ TUNE</button>
  </div>
  <div class="hint">SPACE = THROW &nbsp;·&nbsp; C = LIVE TUNING PANEL &nbsp;·&nbsp; R = REMATCH &nbsp;·&nbsp; TWO PLAYERS, ONE KEYBOARD — SAME KEYS, TAKE TURNS</div>
</div>

<div id="cfgPanel">
  <div class="cfg-head">
    <div class="t">
      <b>LIVE TUNING</b>
      <span class="badge" id="liveBadge">STOCK VALUES</span>
    </div>
    <div style="font-size:9.5px;opacity:.55;letter-spacing:.07em;">
      Edits take effect immediately — or at the next throw for layout.
    </div>
  </div>
  <div class="cfg-body" id="cfgBody"></div>
  <div class="cfg-foot">
    <button class="btn" id="resetCfg">RESET ALL</button>
    <button class="btn tune" id="closeCfg">CLOSE</button>
  </div>
</div>
<script>
"use strict";
/* ============================================================
   GORILLAZ — turn-based banana-bomb artillery
   Player 1 (red) vs Player 2 (blue), local, one keyboard.
   ============================================================ */

const cv  = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 1000, H = 625, GROUND_Y = 578;

/* ---------------- LIVE CONFIGURATION LAYER ---------------- */
const DEFAULTS = {
  blastRadius : 45,      // px
  falloff     : 0,       // % (extra partial-damage shell beyond blastRadius)
  gravity     : 320,     // px/s^2
  windMin     : 0,       // min wind magnitude
  windMax     : 18,      // max wind magnitude
  windShift   : true,    // may direction change between turns?
  windMult    : 4,       // px/s^2 per wind unit
  buildingCount: 11,
  bMinH       : 70,
  bMaxH       : 300,
  powerMin    : 10,
  powerMax    : 100,
  velMult     : 6.2,     // px/s of launch speed per power unit
  roundFormat : 'single'
};
const CFG = Object.assign({}, DEFAULTS);
const DEF_STR = JSON.stringify(DEFAULTS);
const deferredKeys = ['buildingCount','bMinH','bMaxH'];
let layoutDirty = false;

const SCHEMA = [
  { group:'EXPLOSION', items:[
    { k:'blastRadius', label:'Blast Radius', min:10, max:160, step:1, unit:'px',
      note:'circular damage zone on impact' },
    { k:'falloff', label:'Damage Threshold', min:0, max:100, step:1, unit:'%',
      note:'0% = hurl must land INSIDE the radius. Higher % adds a partial-damage shell that extends the kill radius.' }
  ]},
  { group:'PHYSICS', items:[
    { k:'gravity', label:'Gravity Strength', min:40, max:900, step:10, unit:'px/s²' },
    { k:'windMult', label:'Wind Force', min:0, max:14, step:0.5, unit:'×' },
    { k:'windMin', label:'Wind Range Min', min:0, max:60, step:1, unit:'mph' },
    { k:'windMax', label:'Wind Range Max', min:0, max:60, step:1, unit:'mph' },
    { k:'windShift', label:'Wind May Shift Each Turn', type:'bool' }
  ]},
  { group:'SKYLINE', deferred:true, items:[
    { k:'buildingCount', label:'Building Count', min:4, max:20, step:1, unit:'bldgs' },
    { k:'bMinH', label:'Building Height Min', min:20, max:400, step:5, unit:'px' },
    { k:'bMaxH', label:'Building Height Max', min:40, max:480, step:5, unit:'px' }
  ]},
  { group:'PROJECTILE', items:[
    { k:'powerMin', label:'Power Range Min', min:1, max:80, step:1, unit:'pts' },
    { k:'powerMax', label:'Power Range Max', min:20, max:150, step:1, unit:'pts' },
    { k:'velMult', label:'Velocity Scale', min:1, max:16, step:0.1, unit:'px/s per pt' }
  ]},
  { group:'MATCH', items:[
    { k:'roundFormat', label:'Session Format', type:'select',
      options:[['single','Single Round'],['bo3','Best of 3'],['bo5','Best of 5']],
      deferred:true }
  ]}
];

/* ---------------- AUDIO (tiny WebAudio synth) ---------------- */
let AC = null, windOsc = null, windGain = null;
function audio(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC = null; } } return AC; }
function beep(freq, dur, type, vol, slideTo){
  const a = audio(); if(!a) return;
  const o = a.createOscillator(), g = a.createGain();
  o.type = type||'square'; o.frequency.setValueAtTime(freq, a.currentTime);
  if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo), a.currentTime+dur);
  g.gain.setValueAtTime(0.0001, a.currentTime);
  g.gain.exponentialRampToValueAtTime(vol||0.12, a.currentTime+0.008);
  g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime+dur);
  o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime+dur+0.02);
}
function sfxLaunch(){ beep(160,0.22,'sawtooth',0.10,620); }
function sfxBoom(){
  const a = audio(); if(!a) return;
  const len = Math.floor(a.sampleRate*0.5);
  const buf = a.createBuffer(1, len, a.sampleRate);
  const d = buf.getChannelData(0);
  for(let i=0;i<len;i++){ d[i] = (Math.random()*2-1) * Math.pow(1-i/len, 2.6); }
  const s = a.createBufferSource(); s.buffer = buf;
  const f = a.createBiquadFilter(); f.type='lowpass'; f.frequency.setValueAtTime(1400,a.currentTime);
  f.frequency.exponentialRampToValueAtTime(120, a.currentTime+0.45);
  const g = a.createGain(); g.gain.value = 0.32;
  s.connect(f); f.connect(g); g.connect(a.destination); s.start();
  beep(70,0.35,'triangle',0.16,28);
}
function sfxWin(){ [523,659,784,1047].forEach((f,i)=> setTimeout(()=>beep(f,0.16,'square',0.10), i*110)); }
function sfxLose(){ [392,330,262].forEach((f,i)=> setTimeout(()=>beep(f,0.22,'sawtooth',0.09), i*150)); }
function syncWindTone(){
  const a = audio(); if(!a) return;
  if(!windOsc){
    windOsc = a.createOscillator(); windGain = a.createGain();
    windOsc.type='sine'; windOsc.frequency.value=70;
    windGain.gain.value = 0; windOsc.connect(windGain); windGain.connect(a.destination); windOsc.start();
  }
  const s = Math.abs(wind)/Math.max(1,CFG.windMax);
  windGain.gain.setTargetAtTime(s*0.035, a.currentTime, 0.4);
  windOsc.frequency.setTargetAtTime(55+s*60, a.currentTime, 0.4);
}

/* ---------------- STATE ---------------- */
let buildings = [];
let players = [];         // two gorillas
let turn = 0;             // 0 or 1
let wind = 0;             // signed
let proj = null;
let phase = 'aim';        // aim | fly | boom | over
let effects = [];         // explosions + particles
let decals = [];          // scorch marks
let clouds = [];
let timer = 0;
let roundNo = 1;
let scores = [0,0];
let sessionWins = [0,0];
let matchOver = false;
let shake = 0;
let lastImpact = null;
let flashT = 0;

const P_COL = [
  { body:'#ff5252', dark:'#a02121', light:'#ff9d9d', name:'PLAYER 1', hex:'#ff5252' },
  { body:'#4d9dff', dark:'#1f4f96', light:'#a9d4ff', name:'PLAYER 2', hex:'#4d9dff' }
];
const B_COLS = ['#39406e','#4a4f8c','#2f3560','#54528f','#3d4a7d','#5b4470','#2c4a6e','#474b7a'];

/* ---------------- HELPERS ---------------- */
const rand  = (a,b)=> a + Math.random()*(b-a);
const randi = (a,b)=> Math.floor(rand(a,b+1));
const pick  = arr => arr[Math.floor(Math.random()*arr.length)];
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);

/* ---------------- WORLD GENERATION ---------------- */
function genSkyline(){
  buildings = [];
  const n = clamp(Math.round(CFG.buildingCount), 3, 24);
  const maxH = Math.max(CFG.bMinH + 20, CFG.bMaxH);
  const minH = Math.min(CFG.bMinH, maxH - 20);

  const wid = [], gaps = [];
  let tw = 0, tg = 0;
  for(let i=0;i<n;i++){ const w = rand(48,118); wid.push(w); tw += w; }
  for(let i=0;i<n-1;i++){ const g = rand(5,18); gaps.push(g); tg += g; }

  const k = (W - tg) / tw;
  let x = 0;
  for(let i=0;i<n;i++){
    const bw = wid[i]*k;
    const h  = rand(minH, maxH);
    buildings.push({
      x: x, w: bw, top: GROUND_Y - h, h: h,
      col: B_COLS[i % B_COLS.length],
      lit: Math.random() < .5
    });
    x += bw + (gaps[i]||0);
  }
}
function roofAt(cx){
  for(const b of buildings){ if(cx >= b.x && cx <= b.x+b.w) return b.top; }
  return GROUND_Y;
}
function placeGorillas(){
  const leftPool  = buildings.map((b,i)=>i).filter(i => buildings[i].x + buildings[i].w/2 < W*0.38);
  const rightPool = buildings.map((b,i)=>i).filter(i => buildings[i].x + buildings[i].w/2 > W*0.62);
  const L = leftPool.length  ? leftPool  : [0];
  const R = rightPool.length ? rightPool : [buildings.length-1];

  const minSep = Math.max(300, CFG.blastRadius*4.5);
  let bi = pick(L), ci = pick(R), tries = 0;
  while(tries++ < 60){
    bi = pick(L); ci = pick(R);
    const ax = buildings[bi].x + buildings[bi].w/2;
    const bx = buildings[ci].x + buildings[ci].w/2;
    if(bx - ax >= minSep) break;
  }
  const A = buildings[bi], B = buildings[ci];
  const ax = clamp(A.x + A.w/2, 40, W-40);
  const bx = clamp(B.x + B.w/2, 40, W-40);

  players = [
    { id:0, x:ax, gy:A.top, dir: 1, angle:45,  power:Math.round((CFG.powerMin+CFG.powerMax)/2),
      dead:false, deadT:0, winT:-1, throwT:-1, blob:0, col:P_COL[0] },
    { id:1, x:bx, gy:B.top, dir:-1, angle:135, power:Math.round((CFG.powerMin+CFG.powerMax)/2),
      dead:false, deadT:0, winT:-1, throwT:-1, blob:0, col:P_COL[1] }
  ];
  if(A.top < B.top) players[0].id = 0;
}
function genClouds(){
  clouds = [];
  for(let i=0;i<6;i++){
    clouds.push({ x: rand(0,W), y: rand(40,230), s: rand(.6,1.5), v: rand(3,11)*(Math.random()<.5?-1:1), a: rand(.10,.26) });
  }
}
function newRound(keepScore){
  if(!keepScore){ scores = [0,0]; roundNo = 1; }
  else roundNo++;
  layoutDirty = false;
  genSkyline();
  placeGorillas();
  genClouds();
  decals = []; effects = []; proj = null;
  turn = Math.random() < .5 ? 0 : 1;
  wind = rollWind();
  phase = 'aim'; timer = 0; matchOver = false;
  hideBanner();
  syncWindTone();
  refreshPanelState();
  syncHUD();
}
function rollWind(){
  const lo = Math.min(CFG.windMin, CFG.windMax), hi = Math.max(CFG.windMin, CFG.windMax);
  const m = rand(lo, hi);
  const sign = Math.random() < .5 ? -1 : 1;
  return Math.round(m*sign*10)/10;
}

/* ---------------- TUNING PANEL ---------------- */
const cfgBody = document.getElementById('cfgBody');
const liveBadge = document.getElementById('liveBadge');
const inputsByKey = {};

function buildPanel(){
  cfgBody.innerHTML = '';
  SCHEMA.forEach(grp => {
    const g = document.createElement('div'); g.className = 'cfg-group';
    const h = document.createElement('h3'); h.textContent = grp.group + (grp.deferred ? '  ·  APPLIES NEXT ROUND' : '');
    g.appendChild(h);

    grp.items.forEach(it => {
      const row = document.createElement('div');
      row.className = 'cfg-item';
      if(grp.deferred || it.deferred) row.classList.add('deferred');

      if(it.type === 'bool'){
        const sw = document.createElement('label'); sw.className = 'switch';
        const cb = document.createElement('input'); cb.type = 'checkbox';
        cb.checked = !!CFG[it.k];
        cb.addEventListener('change', ()=>{ CFG[it.k] = cb.checked; onCfgChange(it.k); });
        const sp = document.createElement('span'); sp.textContent = it.label;
        sw.appendChild(cb); sw.appendChild(sp); row.appendChild(sw);
        inputsByKey[it.k] = { type:'bool', el:cb, row:row };
      } else if(it.type === 'select'){
        const top = document.createElement('div'); top.className = 'top';
        const nm = document.createElement('span'); nm.className = 'nm'; nm.textContent = it.label;
        top.appendChild(nm); row.appendChild(top);
        const sel = document.createElement('select');
        it.options.forEach(o => { const op = document.createElement('option'); op.value = o[0]; op.textContent = o[1]; sel.appendChild(op); });
        sel.value = CFG[it.k];
        sel.addEventListener('change', ()=>{ CFG[it.k] = sel.value; onCfgChange(it.k); });
        row.appendChild(sel);
        inputsByKey[it.k] = { type:'select', el:sel, row:row };
      } else {
        const top = document.createElement('div'); top.className = 'top';
        const nm = document.createElement('span'); nm.className = 'nm';
        nm.innerHTML = it.label + (it.unit ? ' <i>'+it.unit+'</i>' : '');
        const num = document.createElement('input'); num.type = 'number';
        num.min = it.min; num.max = it.max; num.step = it.step; num.value = CFG[it.k];
        top.appendChild(nm); top.appendChild(num); row.appendChild(top);

        const rng = document.createElement('input'); rng.type = 'range';
        rng.min = it.min; rng.max = it.max; rng.step = it.step; rng.value = CFG[it.k];
        row.appendChild(rng);

        const cur = document.createElement('div'); cur.className = 'cur';
        cur.textContent = it.note || '';
        if(it.note) row.appendChild(cur);

        const apply = (v, from)=>{
          v = clamp(parseFloat(v), it.min, it.max);
          if(isNaN(v)) v = CFG[it.k];
          CFG[it.k] = v;
          if(from !== 'num') num.value = v;
          if(from !== 'rng') rng.value = v;
          if(it.k === 'windMin' && CFG.windMin > CFG.windMax){ CFG.windMax = CFG.windMin; }
          if(it.k === 'windMax' && CFG.windMax < CFG.windMin){ CFG.windMin = CFG.windMax; }
          if(it.k === 'bMinH' && CFG.bMinH > CFG.bMaxH){ CFG.bMaxH = CFG.bMinH + 20; }
          if(it.k === 'bMaxH' && CFG.bMaxH < CFG.bMinH){ CFG.bMinH = Math.max(20, CFG.bMaxH - 20); }
          if(it.k === 'powerMin' && CFG.powerMin > CFG.powerMax){ CFG.powerMax = CFG.powerMin + 10; }
          if(it.k === 'powerMax' && CFG.powerMax < CFG.powerMin){ CFG.powerMin = Math.max(1, CFG.powerMax - 10); }
          onCfgChange(it.k);
        };
        num.addEventListener('input', ()=> apply(num.value,'num'));
        rng.addEventListener('input', ()=> apply(rng.value,'rng'));
        inputsByKey[it.k] = { type:'num', num:num, rng:rng, row:row, max:it.max, min:it.min };
      }
      g.appendChild(row);
    });
    cfgBody.appendChild(g);
  });
  const note = document.createElement('div');
  note.className = 'cfg-note';
  note.textContent = 'TIP: blast radius, gravity, wind and velocity take effect on the very next throw. Skyline values regenerate the city at the start of the next round.';
  cfgBody.appendChild(note);
}
function onCfgChange(key){
  if(deferredKeys.indexOf(key) >= 0) layoutDirty = true;
  if(key === 'windMult' || key === 'windMax' || key === 'windMin') syncWindTone();
  if(key === 'windMin' || key === 'windMax' || key === 'powerMin' || key === 'powerMax'){
    syncMirroredInputs();
    players.forEach(p => p.power = clamp(p.power, CFG.powerMin, CFG.powerMax));
  }
  refreshPanelState();
  syncHUD();
}
function syncMirroredInputs(){
  ['windMin','windMax','powerMin','powerMax','bMinH','bMaxH'].forEach(k=>{
    const io = inputsByKey[k]; if(!io) return;
    io.num.value = CFG[k]; io.rng.value = CFG[k];
  });
}
function refreshPanelState(){
  Object.keys(inputsByKey).forEach(k => {
    const io = inputsByKey[k];
    const changed = (k === 'roundFormat') ? (CFG[k] !== DEFAULTS[k])
                  : (typeof DEFAULTS[k] === 'boolean') ? (CFG[k] !== DEFAULTS[k])
                  : Math.abs(Number(CFG[k]) - Number(DEFAULTS[k])) > 1e-9;
    io.row.classList.toggle('changed', changed);
  });
  const tuned = JSON.stringify(CFG) !== DEF_STR || layoutDirty;
  liveBadge.classList.toggle('on', tuned);
  liveBadge.textContent = tuned ? '● LIVE TUNING ACTIVE' : 'STOCK VALUES';
}
function resetConfig(){
  Object.assign(CFG, DEFAULTS);
  layoutDirty = true;
  Object.keys(inputsByKey).forEach(k=>{
    const io = inputsByKey[k];
    if(io.type==='num'){ io.num.value = CFG[k]; io.rng.value = CFG[k]; }
    else if(io.type==='bool'){ io.el.checked = CFG[k]; }
    else if(io.type==='select'){ io.el.value = CFG[k]; }
  });
  players.forEach(p => p.power = clamp(p.power, CFG.powerMin, CFG.powerMax));
  refreshPanelState(); syncHUD();
}
const cfgPanel = document.getElementById('cfgPanel');
function togglePanel(force){
  const open = (force === undefined) ? !cfgPanel.classList.contains('open') : force;
  cfgPanel.classList.toggle('open', open);
}
document.getElementById('openCfg').addEventListener('click', ()=>togglePanel());
document.getElementById('closeCfg').addEventListener('click', ()=>togglePanel(false));
document.getElementById('resetCfg').addEventListener('click', resetConfig);

/* ---------------- HUD SYNC ---------------- */
const turnChip = document.getElementById('turnChip');
const windArrow = document.getElementById('windArrow');
const windVal  = document.getElementById('windVal');
const windBar  = document.getElementById('windBar');
const s1El = document.getElementById('s1'), s2El = document.getElementById('s2');
const roAngle = document.getElementById('roAngle'), roPower = document.getElementById('roPower');
const mAngle = document.getElementById('mAngle'), mPower = document.getElementById('mPower');

const _hud = {};
function setTxt(el, k, v){ if(_hud[k] !== v){ _hud[k] = v; el.textContent = v; } }
function setHTML(el, k, v){ if(_hud[k] !== v){ _hud[k] = v; el.innerHTML = v; } }
function setStyle(el, k, prop, v){ if(_hud[k] !== v){ _hud[k] = v; el.style[prop] = v; } }
function syncHUD(){
  if(!players.length) return;
  const p = players[turn];
  turnChip.className = 'chip turn-chip' + (turn === 1 ? ' p2' : '');
  setStyle(turnChip, 'tc', 'color', turn === 1 ? '#bcdcff' : '#ffd2d2');
  setHTML(turnChip, 'tcH', '<small>'+(phase==='fly'?'IN FLIGHT':'NOW THROWING')+'</small>' +
    (matchOver ? 'MATCH OVER' : P_COL[turn].name + ' TURN'));

  const sgn = wind > 0 ? 1 : -1;
  const mag = Math.abs(wind);
  const s = mag / Math.max(1, Math.max(CFG.windMin, CFG.windMax));
  setTxt(windVal, 'wv', mag.toFixed(1));
  setStyle(windArrow, 'wa', 'transform', 'rotate(' + (sgn > 0 ? -12 * s : 180 + 12 * s) + 'deg)');
  const wcol = mag < 0.6 ? '#5d6a92' : (mag > CFG.windMax * 0.7 ? '#ff5252' : '#ffd166');
  setStyle(windArrow, 'wac', 'color', wcol);
  const half = s * 50;
  setStyle(windBar, 'wb', 'left', (sgn > 0 ? 50 : 50 - half) + '%');
  setStyle(windBar, 'wbw', 'width', half + '%');
  setStyle(windBar, 'wbg', 'background', wcol);

  setTxt(s1El, 'sc1', scores[0]); setTxt(s2El, 'sc2', scores[1]);

  const p2 = players[turn];
  setHTML(roAngle, 'ra', Math.round(p2.angle) + '<span>°</span>');
  setHTML(roPower, 'rp', Math.round(p2.power) + '<span>/' + CFG.powerMax + '</span>');
  setStyle(mAngle, 'ma', 'width', (p2.angle / 180 * 100) + '%');
  const span = Math.max(1, CFG.powerMax - CFG.powerMin);
  setStyle(mPower, 'mp', 'width', clamp((p2.power - CFG.powerMin) / span * 100, 0, 100) + '%');
  const dis = (phase !== 'aim' || matchOver);
  if(_hud.fireDis !== dis){ _hud.fireDis = dis; document.getElementById('fireBtn').disabled = dis; }
}

/* ---------------- INPUT ---------------- */
function adjust(kind, dir, big){
  if(phase !== 'aim' || matchOver || !players.length) return;
  const p = players[turn];
  const step = big ? 5 : 1;
  if(kind === 'angle'){ p.angle = clamp(p.angle + dir*step, 0, 180); }
  else { p.power = clamp(p.power + dir*(big?5:1), CFG.powerMin, CFG.powerMax); }
  syncHUD();
}
document.addEventListener('keydown', e=>{
  if(AC && AC.state === 'suspended') AC.resume();
  const k = e.key, low = k.toLowerCase();
  if(k === ' ' || k === 'Enter'){ e.preventDefault(); fireBtn_click(); return; }
  if(low === 'c'){ e.preventDefault(); togglePanel(); return; }
  if(low === 'r' && phase === 'over'){ e.preventDefault(); nextRound(); return; }
  const big = e.shiftKey;
  if(k === 'ArrowLeft'  || low === 'a'){ e.preventDefault(); adjust('angle', -1, big); return; }
  if(k === 'ArrowRight' || low === 'd'){ e.preventDefault(); adjust('angle',  1, big); return; }
  if(k === 'ArrowUp'    || low === 'w'){ e.preventDefault(); adjust('power',  1, big); return; }
  if(k === 'ArrowDown'  || low === 's'){ e.preventDefault(); adjust('power', -1, big); return; }
});
/* hold-to-repeat pad buttons */
document.querySelectorAll('[data-hold]').forEach(btn=>{
  const [kind, d] = btn.dataset.hold.split(':');
  let iv = null, to = null;
  const start = e=>{
    e.preventDefault();
    if(AC && AC.state==='suspended') AC.resume();
    adjust(kind, parseInt(d,10), false);
    to = setTimeout(()=>{ iv = setInterval(()=>adjust(kind, parseInt(d,10), false), 55); }, 320);
  };
  const stop = ()=>{ clearTimeout(to); clearInterval(iv); iv=null; to=null; };
  btn.addEventListener('pointerdown', start);
  ['pointerup','pointerleave','pointercancel'].forEach(ev=>btn.addEventListener(ev, stop));
});
function fireBtn_click(){ fire(); }
document.getElementById('fireBtn').addEventListener('click', ()=>{ if(AC&&AC.state==='suspended')AC.resume(); fire(); });

/* ---------------- BANNER ---------------- */
const bannerEl = document.getElementById('banner');
const bTitle = document.getElementById('bTitle'), bSub = document.getElementById('bSub');
const bBtns = document.getElementById('bBtns'), bTrophy = document.getElementById('bTrophy');
function hideBanner(){ bannerEl.classList.add('hidden'); }
function showBanner(trophy, title, sub, color, buttons){
  bTrophy.textContent = trophy;
  bTitle.textContent = title;
  bTitle.style.color = color;
  bSub.textContent = sub;
  bBtns.innerHTML = '';
  buttons.forEach(bt=>{
    const el = document.createElement('button');
    el.className = 'btn ' + (bt.cls || '');
    el.textContent = bt.label;
    el.addEventListener('click', bt.fn);
    bBtns.appendChild(el);
  });
  bannerEl.classList.remove('hidden');
  bannerEl.classList.remove('flash'); void bannerEl.offsetWidth; bannerEl.classList.add('flash');
}
function nextRound(){
  newRound(true);
  syncHUD();
}

/* ---------------- FIRING & PHYSICS ---------------- */
function muzzleOf(p){ return { x: p.x + p.dir*15, y: p.gy - 33 }; }

function fire(){
  if(phase !== 'aim' || matchOver || !players.length) return;
  const p = players[turn];
  const rad = p.angle * Math.PI/180;
  const sp = p.power * CFG.velMult;
  const m = muzzleOf(p);
  proj = {
    x:m.x, y:m.y, vx:Math.cos(rad)*sp, vy:-Math.sin(rad)*sp,
    trail:[], t:0, owner:p
  };
  p.throwT = 0;
  phase = 'fly';
  syncHUD();
  sfxLaunch();
}

function stepProj(dt){
  const ax = CFG.windMult * wind;
  proj.vx += ax * dt;
  proj.vy += CFG.gravity * dt;
  const nx = proj.x + proj.vx*dt;
  const ny = proj.y + proj.vy*dt;
  const hit = castHit(proj.x, proj.y, nx, ny);
  if(hit){ proj.x = hit.x; proj.y = hit.y; impact(hit); return; }
  proj.x = nx; proj.y = ny; proj.t += dt;
  proj.trail.push({x:proj.x, y:proj.y});
  if(proj.trail.length > 30) proj.trail.shift();
  if(proj.x < -300 || proj.x > W+300 || proj.y > H+500){
    proj = null; phase='boom'; timer = 0.35; lastImpact = null;
  }
}
function castHit(x0,y0,x1,y1){
  const dx = x1-x0, dy = y1-y0;
  const dist = Math.hypot(dx,dy);
  const steps = Math.max(1, Math.ceil(dist/2.2));
  for(let i=1;i<=steps;i++){
    const t = i/steps, x = x0+dx*t, y = y0+dy*t;
    if(y >= GROUND_Y) return {x:x, y:GROUND_Y, type:'ground'};
    for(let b=0;b<buildings.length;b++){
      const bd = buildings[b];
      if(x >= bd.x && x <= bd.x+bd.w && y >= bd.top) return {x:x, y:y, type:'building'};
    }
    for(let pi=0; pi<players.length; pi++){
      const p = players[pi];
      if(p.dead) continue;
      if(proj && p === proj.owner && proj.t < 0.14) continue;  // clear our own muzzle
      if(x >= p.x-17 && x <= p.x+17 && y >= p.gy-44 && y <= p.gy){
        return {x:x, y:y, type:'gorilla', who:p};
      }
    }
  }
  return null;
}
function impact(hit){
  phase = 'boom'; timer = 1.2;
  lastImpact = hit;
  const R = CFG.blastRadius;
  effects.push({kind:'blast', x:hit.x, y:hit.y, t:0, life:0.65, R:R});
  effects.push({kind:'dust',  x:hit.x, y:hit.y, t:0, life:0.95, R:R*1.9});
  for(let i=0;i<34;i++){
    const a = rand(0, Math.PI*2), s = rand(50, R*6);
    effects.push({kind:'p', x:hit.x, y:hit.y, vx:Math.cos(a)*s, vy:Math.sin(a)*s*0.8 - 60,
      t:0, life:rand(.4,1.15), r:rand(1.5,4.5),
      col:pick(['#ffd166','#ff8a3d','#ff5252','#fff3c4','#b9b9cc'])});
  }
  decals.push({x:hit.x, y:hit.y, r:Math.max(10, R*0.55)});
  if(decals.length > 26) decals.shift();
  shake = 14; flashT = 1;
  sfxBoom();

  const killR = R * (1 + CFG.falloff/100);
  players.forEach(p=>{
    if(p.dead) return;
    const d = Math.hypot(p.x - hit.x, (p.gy-22) - hit.y);
    if(d <= killR || hit.who === p){
      p.dead = true; p.deadT = 0;
      p.kill = (hit.who === p) ? 'DIRECT HIT' : 'BLAST';
      p.selfHit = (hit.who === p);
    }
  });
}
let carry = 0;
const FIXED = 1/480;
function flyStep(dt){
  carry += dt;
  let guard = 0;
  while(carry >= FIXED && guard++ < 900){
    carry -= FIXED;
    stepProj(FIXED);
    if(phase !== 'fly') break;
  }
  if(carry > 0.4) carry = 0;
}

/* ---------------- TURN / ROUND RESOLUTION ---------------- */
function resolve(){
  const a = players[turn], b = players[1-turn];
  if(a.dead || b.dead){
    if(a.dead && b.dead) roundWin(-1);
    else if(b.dead)      roundWin(turn);
    else                 roundWin(1-turn, true);
  } else {
    nextTurn();
  }
}
function nextTurn(){
  turn = 1 - turn;
  if(CFG.windShift) wind = rollWind();
  syncWindTone();
  phase = 'aim'; proj = null; carry = 0; lastImpact = null;
  players.forEach(p=>{ if(!p.dead) p.throwT = -1; });
  players[turn].power = clamp(players[turn].power, CFG.powerMin, CFG.powerMax);
  players[turn].throwT = -1;
  syncHUD();
}
function roundWin(wIdx, suicide){
  phase = 'over';
  const target = CFG.roundFormat === 'bo3' ? 2 : CFG.roundFormat === 'bo5' ? 3 : 1;
  let color = '#ffffff';
  if(wIdx >= 0){ scores[wIdx]++; players[wIdx].winT = 0; color = P_COL[wIdx].hex; }
  if(wIdx >= 0) sfxWin(); else sfxLose();
  syncHUD();

  const done = (wIdx >= 0 && scores[wIdx] >= target);
  matchOver = done;
  if(done && wIdx >= 0) sessionWins[wIdx]++;
  const btns = [];
  const fmtTxt = CFG.roundFormat === 'bo3' ? 'BEST OF 3' : CFG.roundFormat === 'bo5' ? 'BEST OF 5' : 'SINGLE ROUND';

  if(done){
    showBanner('🏆', P_COL[wIdx].name + ' WINS THE MATCH',
      'MATCH ' + scores[0] + ' — ' + scores[1] + '   ·   ' + fmtTxt +
        '   ·   SESSION ' + sessionWins[0] + ' — ' + sessionWins[1],
      color,
      [{label:'NEW MATCH', cls:'fire', fn:()=>{ newRound(false); syncHUD(); }}]);
  } else if(wIdx < 0){
    showBanner('💥', 'DOUBLE KO — DRAW',
      'BOTH GORILLAS DOWN   ·   ROUND ' + roundNo,
      '#ffd166',
      [{label:'NEXT ROUND', cls:'fire', fn:nextRound}]);
  } else {
    showBanner('🍌', P_COL[wIdx].name + ' WINS THE ROUND',
      (players[wIdx].kill || 'BLAST') + '   ·   ROUND ' + roundNo + '   ·   ' + fmtTxt + '  ' + scores[0] + '—' + scores[1],
      color,
      [{label:'NEXT ROUND', cls:'fire', fn:nextRound},
       {label:'RESET SCORES', fn:()=>{ scores=[0,0]; roundNo=1; newRound(true); syncHUD(); }}]);
  }
  syncHUD();
}

/* ---------------- UPDATE ---------------- */
function update(dt){
  if(shake > 0) shake = Math.max(0, shake - dt*58);
  if(flashT > 0) flashT = Math.max(0, flashT - dt*3.2);
  clouds.forEach(c=>{
    c.x += (c.v + wind*0.35) * dt * c.s;
    if(c.x < -180) c.x = W + 150;
    if(c.x > W + 180) c.x = -150;
  });
  players.forEach(p=>{
    if(p.throwT >= 0){ p.throwT += dt; if(p.throwT > 0.95) p.throwT = -1; }
    if(p.dead) p.deadT += dt;
    if(p.winT >= 0) p.winT += dt;
  });
  effects.forEach(e=>{
    e.t += dt;
    if(e.kind === 'p'){ e.vy += CFG.gravity*0.55*dt; e.x += e.vx*dt; e.y += e.vy*dt; }
  });
  effects = effects.filter(e=> e.t < e.life);

  if(phase === 'fly' && proj) flyStep(dt);
  else if(phase === 'boom'){ timer -= dt; if(timer <= 0) resolve(); }
}

/* ---------------- RENDER ---------------- */
function drawSky(){
  const g = ctx.createLinearGradient(0,0,0,GROUND_Y);
  g.addColorStop(0,   '#120c2e');
  g.addColorStop(0.42,'#33235f');
  g.addColorStop(0.72,'#7a3f77');
  g.addColorStop(0.88,'#d3607a');
  g.addColorStop(1,   '#ffa15c');
  ctx.fillStyle = g; ctx.fillRect(0,0,W,GROUND_Y+2);
}
function drawSun(){
  const x = 812, y = 168, r = 46;
  const gg = ctx.createRadialGradient(x,y,4,x,y,r*3.2);
  gg.addColorStop(0,'rgba(255,220,150,.42)');
  gg.addColorStop(0.35,'rgba(255,140,90,.16)');
  gg.addColorStop(1,'rgba(255,120,80,0)');
  ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(x,y,r*3.2,0,7); ctx.fill();
  const g2 = ctx.createLinearGradient(0,y-r,0,y+r);
  g2.addColorStop(0,'#fff2b8'); g2.addColorStop(.55,'#ffca63'); g2.addColorStop(1,'#ff7a4d');
  ctx.fillStyle = g2; ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fill();
  ctx.fillStyle = 'rgba(20,10,40,.20)';
  for(let i=0;i<4;i++){ ctx.fillRect(x-r, y-8+i*15, r*2, 3+i); }
}
function drawClouds(){
  clouds.forEach(c=>{
    ctx.save(); ctx.translate(c.x, c.y); ctx.scale(c.s, c.s);
    ctx.fillStyle = 'rgba(255,190,160,'+c.a+')';
    ctx.beginPath();
    ctx.arc(0,0,26,0,7); ctx.arc(30,-8,20,0,7); ctx.arc(-30,4,18,0,7); ctx.arc(8,10,17,0,7);
    ctx.fill(); ctx.restore();
  });
}
let far = [];
function genFar(){
  far = [];
  let x = -30;
  while(x < W+40){
    const w = rand(60,150), h = rand(90,300);
    far.push({x:x, w:w, h:h, a: rand(.16,.34)});
    x += w + rand(4,26);
  }
}
function drawFar(){
  far.forEach((b,i)=>{
    ctx.fillStyle = 'rgba(38,28,72,'+b.a+')';
    ctx.fillRect(b.x, GROUND_Y-b.h, b.w, b.h);
    ctx.fillStyle = 'rgba(255,200,150,'+(b.a*0.30)+')';
    for(let yy = GROUND_Y-b.h+14; yy < GROUND_Y-10; yy += 20){
      for(let xx = b.x+8; xx < b.x+b.w-8; xx += 16){
        if(hsh(i,xx,yy) > 0.62) ctx.fillRect(xx, yy, 5, 7);
      }
    }
  });
}
function hsh(a,b,c){ const v = Math.sin(a*12.9898 + b*78.233 + c*37.719) * 43758.5453; return v - Math.floor(v); }
function drawBuildings(){
  buildings.forEach((b,i)=>{
    ctx.fillStyle = b.col;
    ctx.fillRect(b.x, b.top, b.w, GROUND_Y - b.top);
    // side shading
    ctx.fillStyle = 'rgba(0,0,0,.22)';
    ctx.fillRect(b.x + b.w - Math.min(9, b.w*0.18), b.top, Math.min(9, b.w*0.18), GROUND_Y-b.top);
    ctx.fillStyle = 'rgba(255,255,255,.10)';
    ctx.fillRect(b.x, b.top, Math.min(6, b.w*0.14), GROUND_Y-b.top);
    // roof cap
    ctx.fillStyle = 'rgba(0,0,0,.35)';
    ctx.fillRect(b.x, b.top, b.w, 4);
    ctx.fillStyle = b.lit ? '#ffd166' : '#9fb0d8';
    ctx.fillRect(b.x, b.top, b.w, 2);
    // windows
    const cols = Math.max(1, Math.floor(b.w/15));
    const rows = Math.max(1, Math.floor((GROUND_Y-b.top-16)/19));
    const padX = (b.w - cols*10) / (cols+1);
    for(let c=0;c<cols;c++){
      for(let r=0;r<rows;r++){
        const wx = b.x + padX + c*(10+padX);
        const wy = b.top + 12 + r*19;
        if(wy > GROUND_Y-10) continue;
        const h = hsh(i, c, r);
        if(h > 0.72){ ctx.fillStyle = 'rgba(255,214,102,.92)'; }
        else if(h > 0.42){ ctx.fillStyle = 'rgba(150,175,225,.30)'; }
        else { ctx.fillStyle = 'rgba(10,12,28,.42)'; }
        ctx.fillRect(wx, wy, 10, 11);
      }
    }
  });
  // ground
  const g = ctx.createLinearGradient(0,GROUND_Y,0,H);
  g.addColorStop(0,'#241b3a'); g.addColorStop(1,'#0d0a17');
  ctx.fillStyle = g; ctx.fillRect(0, GROUND_Y, W, H-GROUND_Y);
  ctx.fillStyle = 'rgba(255,180,120,.30)'; ctx.fillRect(0, GROUND_Y, W, 2);
}
function drawDecals(){
  decals.forEach(d=>{
    const g = ctx.createRadialGradient(d.x,d.y,1,d.x,d.y,d.r);
    g.addColorStop(0,'rgba(12,8,18,.62)');
    g.addColorStop(0.6,'rgba(20,12,26,.34)');
    g.addColorStop(1,'rgba(24,14,30,0)');
    ctx.fillStyle = g; ctx.beginPath(); ctx.arc(d.x,d.y,d.r,0,7); ctx.fill();
  });
}
function drawGorilla(p){
  const s = 2;
  ctx.save();
  ctx.translate(p.x, p.gy);
  // shadow
  ctx.fillStyle = 'rgba(0,0,0,.35)';
  ctx.beginPath(); ctx.ellipse(0, 0, 17*s/2*1.25, 4, 0, 0, 7); ctx.fill();

  if(p.dead){
    const t = Math.min(1, p.deadT/0.45);
    ctx.rotate(p.dir * t * Math.PI/2 * 0.96);
    ctx.translate(0, -1);
  } else if(p.winT >= 0){
    const hop = Math.abs(Math.sin(p.winT*6.5)) * 9;
    ctx.translate(0, -hop);
  }
  ctx.scale(p.dir, 1);

  const C = p.col;
  const R = (x,yT,w,h,c)=>{ ctx.fillStyle = c; ctx.fillRect(x*s, -(yT+h)*s, w*s, h*s); };

  // legs
  R(-8, 0, 6, 11, C.dark);
  R(2,  0, 6, 11, C.dark);
  // torso
  R(-9, 9, 18, 18, C.body);
  R(-9, 9, 18, 4, C.light);
  // head
  R(-8, 27, 16, 15, C.body);
  R(-11, 32, 4, 6, C.dark);
  R(7,  32, 4, 6, C.dark);
  // muzzle
  R(-5, 28, 10, 8, C.light);
  // eyes
  if(p.dead){
    ctx.strokeStyle = '#100c16'; ctx.lineWidth = 2*s;
    ctx.beginPath();
    ctx.moveTo(-7*s, -38*s); ctx.lineTo(-2*s, -33*s);
    ctx.moveTo(-2*s, -38*s); ctx.lineTo(-7*s, -33*s);
    ctx.moveTo(2*s, -38*s);  ctx.lineTo(7*s, -33*s);
    ctx.moveTo(7*s, -38*s);  ctx.lineTo(2*s, -33*s);
    ctx.stroke();
  } else {
    R(-6, 35.5, 3, 3, '#0d0d12');
    R(2,  35.5, 3, 3, '#0d0d12');
  }
  R(-7, 39, 14, 2, C.dark);

  // arms
  let armA = -0.18, armB = -0.18;
  if(p.winT >= 0){ armA = 2.5; armB = 2.5; }
  else if(p.throwT >= 0){
    const t = p.throwT / 0.95;
    if(t < 0.4)      { const u = t/0.4; armA = -0.18 + u*2.6; armB = -0.18 + u*1.2; }
    else if(t < 0.65) { const u = (t-0.4)/0.25; armA = 2.42 - u*3.1; armB = 1.02 - u*1.6; }
    else { armA = -0.68; armB = -0.58; }
  }
  const arm = (shoulderX, ang)=>{
    ctx.save();
    ctx.translate(shoulderX*s, -26*s);
    ctx.rotate(-ang);
    ctx.fillStyle = C.dark;
    ctx.fillRect(-3*s, 0, 6*s, 17*s);
    ctx.fillStyle = C.body;
    ctx.fillRect(-3*s, 12*s, 6*s, 5*s);
    ctx.restore();
  };
  arm(0, armA);
  arm(6, armB);
  ctx.restore();
}
function drawAim(){
  if(phase !== 'aim' || matchOver || !players.length) return;
  const p = players[turn];
  const m = muzzleOf(p);
  const rad = p.angle*Math.PI/180;
  const span = Math.max(1, CFG.powerMax - CFG.powerMin);
  const pw = clamp((p.power - CFG.powerMin)/span, 0, 1);
  const len = 26 + pw*56;
  const ex = m.x + Math.cos(rad)*len, ey = m.y - Math.sin(rad)*len;
  ctx.save();
  ctx.strokeStyle = p.col.hex; ctx.lineWidth = 3; ctx.globalAlpha = 0.85;
  ctx.setLineDash([6,6]); ctx.lineDashOffset = -(performance.now()/60)%12;
  ctx.beginPath(); ctx.moveTo(m.x, m.y); ctx.lineTo(ex, ey); ctx.stroke();
  ctx.setLineDash([]);
  ctx.globalAlpha = 1;
  ctx.fillStyle = p.col.hex;
  ctx.beginPath();
  ctx.arc(ex, ey, 4, 0, 7); ctx.fill();
  // power ticks
  for(let i=1;i<=4;i++){
    const t = i/5;
    const tx = m.x + Math.cos(rad)*(20+t*54), ty = m.y - Math.sin(rad)*(20+t*54);
    ctx.globalAlpha = pw > t ? 0.9 : 0.22;
    ctx.fillRect(tx-2, ty-2, 4, 4);
  }
  ctx.restore();
}
function drawTrail(){
  if(!proj) return;
  const p = proj.owner;
  proj.trail.forEach((pt,i)=>{
    const a = (i/proj.trail.length)*0.55;
    ctx.fillStyle = 'rgba(255,214,102,'+a+')';
    const r = 1 + (i/proj.trail.length)*3.2;
    ctx.beginPath(); ctx.arc(pt.x, pt.y, r, 0, 7); ctx.fill();
  });
}
function drawProjectile(){
  if(!proj) return;
  const ang = Math.atan2(proj.vy, proj.vx);
  ctx.save();
  ctx.translate(proj.x, proj.y);
  ctx.rotate(ang);
  // glow
  const g = ctx.createRadialGradient(0,0,1,0,0,16);
  g.addColorStop(0,'rgba(255,220,120,.55)'); g.addColorStop(1,'rgba(255,200,80,0)');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,0,16,0,7); ctx.fill();
  // banana body
  ctx.fillStyle = '#ffd84d';
  ctx.beginPath();
  ctx.moveTo(-11, 2.5);
  ctx.quadraticCurveTo(0, -7.5, 11, 0.5);
  ctx.quadraticCurveTo(0, 3.6, -11, 2.5);
  ctx.fill();
  ctx.fillStyle = '#ffefa8';
  ctx.beginPath();
  ctx.moveTo(-8, 0.4); ctx.quadraticCurveTo(0,-4.2, 8, -0.6);
  ctx.quadraticCurveTo(0, 1.6, -8, 0.4); ctx.fill();
  ctx.fillStyle = '#7a4a12';
  ctx.fillRect(-12.5, 1.2, 3.5, 3.5);
  ctx.fillRect(9.5, -1.6, 3.5, 3.5);
  ctx.restore();
}
function drawEffects(){
  effects.forEach(e=>{
    const k = e.t / e.life;
    if(e.kind === 'blast'){
      const r = e.R * (0.35 + k*0.85);
      ctx.globalAlpha = (1-k)*0.95;
      const g = ctx.createRadialGradient(e.x,e.y,1,e.x,e.y,r*1.15);
      g.addColorStop(0,   'rgba(255,255,235,1)');
      g.addColorStop(0.32,'rgba(255,214,102,.95)');
      g.addColorStop(0.62,'rgba(255,120,60,.72)');
      g.addColorStop(1,   'rgba(180,40,40,0)');
      ctx.fillStyle = g; ctx.beginPath(); ctx.arc(e.x,e.y,r*1.15,0,7); ctx.fill();
      ctx.globalAlpha = (1-k)*0.9;
      ctx.strokeStyle = 'rgba(255,255,240,.9)';
      ctx.lineWidth = Math.max(1, 4*(1-k));
      ctx.beginPath(); ctx.arc(e.x,e.y,r,0,7); ctx.stroke();
    } else if(e.kind === 'dust'){
      const r = e.R*(0.25 + k*0.9);
      ctx.globalAlpha = (1-k)*0.32;
      ctx.strokeStyle = '#d8c8b0'; ctx.lineWidth = 5*(1-k)+1;
      ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, 7); ctx.stroke();
    } else {
      ctx.globalAlpha = Math.max(0, 1-k);
      ctx.fillStyle = e.col;
      ctx.fillRect(e.x-e.r, e.y-e.r, e.r*2, e.r*2);
    }
  });
  ctx.globalAlpha = 1;
}
function render(){
  ctx.setTransform(1,0,0,1,0,0);
  ctx.clearRect(0,0,W,H);
  ctx.save();
  if(shake > 0.4) ctx.translate(rand(-shake,shake)*0.45, rand(-shake,shake)*0.45);
  drawSky(); drawSun(); drawClouds(); drawFar(); drawBuildings(); drawDecals();
  players.forEach(drawGorilla);
  drawTrail(); drawProjectile();
  ctx.restore();
  drawEffects();
  drawAim();
  if(flashT > 0){
    ctx.fillStyle = 'rgba(255,240,210,' + (flashT*0.38) + ')';
    ctx.fillRect(0,0,W,H);
  }
  // scanline-ish frame
  ctx.strokeStyle = 'rgba(120,140,255,.10)'; ctx.lineWidth = 2;
  ctx.strokeRect(1,1,W-2,H-2);
}

/* ---------------- LOOP ---------------- */
let last = performance.now();
let lastFrame = performance.now();
function frame(t){
  let dt = (t - last)/1000; last = t;
  if(dt > 0.06) dt = 0.06;
  if(dt < 0) dt = 0;
  lastFrame = performance.now();
  update(dt);
  render();
  syncHUD();
}
function rafLoop(t){ frame(t); requestAnimationFrame(rafLoop); }
/* watchdog: keeps the simulation advancing if rAF is throttled (hidden tab / headless) */
setInterval(()=>{ if(performance.now() - lastFrame > 220) frame(performance.now()); }, 60);

/* ---------------- BOOT ---------------- */
buildPanel();
newRound(false);
genFar();
refreshPanelState();
syncHUD();
requestAnimationFrame(frame);

/* expose for debugging / automated checks */
function simShot(angle, power, who){
  const p = players[who === undefined ? turn : who];
  const rad = angle*Math.PI/180, sp = power*CFG.velMult;
  const m = muzzleOf(p);
  const st = {x:m.x, y:m.y, vx:Math.cos(rad)*sp, vy:-Math.sin(rad)*sp, trail:[], t:0, owner:p};
  const saved = proj; proj = st;
  const dt = 1/480;
  let hit = null, i = 0;
  while(i++ < 8000){
    const ax = CFG.windMult*wind;
    st.vx += ax*dt; st.vy += CFG.gravity*dt;
    const nx = st.x + st.vx*dt, ny = st.y + st.vy*dt;
    hit = castHit(st.x, st.y, nx, ny);
    if(hit) break;
    st.x = nx; st.y = ny; st.t += dt;
    if(st.x < -300 || st.x > W+300 || st.y > H+500) break;
  }
  proj = saved;
  return hit ? {x:hit.x, y:hit.y, type:hit.type, who:hit.who ? hit.who.id : null} : null;
}
window.GORILLAZ = {
  CFG: CFG, DEFAULTS: DEFAULTS, simShot: simShot,
  setAim: (a, pw)=>{ const p = players[turn]; if(a!=null)p.angle=clamp(a,0,180); if(pw!=null)p.power=clamp(pw,CFG.powerMin,CFG.powerMax); syncHUD(); },
  get state(){ return { phase, turn, wind, scores, roundNo, matchOver, effects: effects.length,
    players: players.map(p=>({x:p.x, y:p.gy, angle:p.angle, power:p.power, dead:p.dead, dir:p.dir})),
    buildings: buildings.map(b=>({x:b.x, w:b.w, top:b.top})),
    proj: proj ? {x:proj.x, y:proj.y, vx:proj.vx, vy:proj.vy} : null }; },
  fire: fire, adjust: adjust, newRound: newRound, rollWind: rollWind,
  setWind: v => { wind = v; syncHUD(); },
  cfgKey: (k,v) => { CFG[k] = v; onCfgChange(k); }
};
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"39e8a34e-492b-4d4b-b230-108a4fd62422","tokensIn":2620132,"tokensOut":53305,"tokensTotal":2673437,"cost":0.04708343999999999,"turns":39,"toolCalls":38,"failedToolCalls":0,"timestamp":"2026-09-10T21:35:10.469Z"} -->