← Gorillaz results

Gorillaz

Qwen 3.8 · 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 32,931 bytes · SHA-256 05c0fea90eb1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GORILLAZ — Arcade Artillery</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
  background:#070912;
  background-image:radial-gradient(1200px 600px at 50% -10%, #1a1f45 0%, #070912 70%);
  display:flex;align-items:center;justify-content:center;
  font-family:'Press Start 2P','Courier New',monospace;
  color:#e8e6ff;overflow:hidden;user-select:none;
}
#frame{width:min(96vw,1000px)}
#logo{text-align:center;font-size:clamp(18px,3.2vw,30px);letter-spacing:6px;color:#ffd23f;
  text-shadow:0 0 18px rgba(255,180,60,.45),3px 3px 0 #7a2d3e;margin-bottom:10px}
#logo .z{color:#8fe3ff;text-shadow:0 0 18px rgba(120,220,255,.5),3px 3px 0 #23406e}
#logo .sub{font-size:9px;color:#8a90c8;letter-spacing:3px;display:block;margin-top:8px;text-shadow:none}
#stage{position:relative}
canvas{display:block;width:100%;background:#000;border:4px solid #2a2f55;
  box-shadow:0 0 0 4px #0b0e1a,0 0 46px rgba(120,90,255,.22);image-rendering:pixelated}
#cornerBtns{position:absolute;top:10px;right:10px;display:flex;gap:6px;z-index:5}
.cbtn{font-family:inherit;font-size:9px;color:#c6caf2;background:rgba(12,14,30,.85);
  border:2px solid #3a4180;padding:6px 8px;cursor:pointer;letter-spacing:1px}
.cbtn:hover{background:#232a5c;color:#fff}
#panel{position:absolute;top:44px;right:10px;width:296px;max-height:calc(100% - 54px);
  overflow-y:auto;background:rgba(9,11,24,.96);border:3px solid #3a4180;z-index:10;
  display:none;box-shadow:0 8px 30px rgba(0,0,0,.6)}
#panel.open{display:block}
#panel::-webkit-scrollbar{width:8px}
#panel::-webkit-scrollbar-thumb{background:#3a4180}
#panel h2{font-size:10px;letter-spacing:1px;padding:10px 12px;border-bottom:2px solid #3a4180;
  display:flex;justify-content:space-between;align-items:center;gap:8px;color:#ffd23f;position:sticky;top:0;background:rgba(9,11,24,.97)}
#badge{font-size:7px;padding:4px 6px;border:1px solid #3a4180;color:#6f76a8;white-space:nowrap}
#badge.active{color:#ffbf3f;border-color:#ffbf3f;background:rgba(255,191,63,.12);animation:pulse 1.1s infinite}
@keyframes pulse{50%{opacity:.45}}
.psec{font-size:8px;color:#7f88c9;padding:9px 12px 5px;letter-spacing:2px;border-top:2px solid #1c2145}
.psec:first-child{border-top:none}
.prow{display:grid;grid-template-columns:1fr 24px 62px 24px;gap:5px;align-items:center;padding:3px 12px}
.prow.sel{grid-template-columns:1fr 110px}
.plabel{font-size:8px;color:#c6caf2;line-height:1.4}
.pbtn{font-family:inherit;background:#1c2145;color:#e8e6ff;border:2px solid #3a4180;font-size:9px;height:22px;cursor:pointer}
.pbtn:hover{background:#2a3168}
.pval{font-family:inherit;background:#0d1024;border:2px solid #3a4180;color:#fff;font-size:9px;text-align:center;height:22px;width:100%;padding:0 2px}
.pval.edited{color:#ffbf3f;border-color:#ffbf3f;background:rgba(255,191,63,.07)}
select.pval{cursor:pointer}
.plive{font-size:7px;color:#4da6ff;letter-spacing:1px;margin-top:3px}
.plive.next{color:#5a6198}
.pfoot{padding:10px 12px;border-top:2px solid #1c2145}
.pfoot .pbtn{width:100%}
.pnote{font-size:7px;color:#6f76a8;padding:2px 12px 12px;line-height:1.8;letter-spacing:.5px}
#legend{text-align:center;font-size:8px;color:#8a90c8;margin-top:10px;letter-spacing:1px;line-height:1.8}
#legend b{color:#ffd23f}
</style>
</head>
<body>
<div id="frame">
  <h1 id="logo">GORILL<span class="z">AZ</span><span class="sub">★ 2-PLAYER ARCADE ARTILLERY ★</span></h1>
  <div id="stage">
    <canvas id="cv" width="960" height="540"></canvas>
    <div id="cornerBtns">
      <button class="cbtn" id="btnTune">⚙ TUNE [T]</button>
      <button class="cbtn" id="btnMute">♪ ON [M]</button>
    </div>
    <div id="panel">
      <h2>⚙ LIVE TUNING <span id="badge">DEFAULTS</span></h2>
      <div id="rows"></div>
      <div class="pfoot"><button class="pbtn" id="btnReset">RESET DEFAULTS</button></div>
      <div class="pnote">PHYSICS · WIND · POWER · DAMAGE — apply immediately / next launch.<br>
      SKYLINE — applies when the next round generates.<br>
      AMBER values are edited from classic defaults.</div>
    </div>
  </div>
  <div id="legend"><b>← →</b> ANGLE · <b>↑ ↓</b> POWER · <b>ENTER / SPACE</b> FIRE BANANA BOMB · <b>T</b> LIVE TUNING · <b>M</b> MUTE</div>
</div>
<script>
'use strict';
/* ================= helpers ================= */
const W=960,H=540,GROUND_H=36,groundY=H-GROUND_H,STEP=1/120;
const SPEED_SCALE=4.4, WIND_ACCEL=2.4;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const easeOutCubic=t=>1-Math.pow(1-t,3);
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;};}
function shade(hex,amt){const n=parseInt(hex.slice(1),16);const r=clamp((n>>16)+amt,0,255),g=clamp(((n>>8)&255)+amt,0,255),b=clamp((n&255)+amt,0,255);return`rgb(${r|0},${g|0},${b|0})`;}
function F(s){return s+'px "Press Start 2P","Courier New",monospace';}
function fmt(v){return Number.isInteger(v)?String(v):String(+v.toFixed(2));}

/* ================= live-tunable config ================= */
const DEFAULTS={
  blastRadius:40, gravity:300,
  windMin:0, windMax:15, windMode:'turn',
  powerMin:10, powerMax:100,
  damageMode:'radius', falloffThreshold:0.35,
  buildingCount:11, heightMin:60, heightMax:260,
  winTarget:1
};
const SCHEMA=[
 {k:'gravity',label:'Gravity',min:50,max:900,step:10,section:'PHYSICS',live:true},
 {k:'windMin',label:'Wind Speed Min',min:0,max:30,step:1,section:'WIND',live:true},
 {k:'windMax',label:'Wind Speed Max',min:1,max:40,step:1,section:'WIND',live:true},
 {k:'windMode',label:'Wind Direction Shifts',type:'select',options:[['turn','Shift Each Turn'],['match','Fixed Per Match'],['none','None — Constant']],section:'WIND',live:true},
 {k:'powerMin',label:'Throw Power Min',min:1,max:90,step:1,section:'POWER',live:true},
 {k:'powerMax',label:'Throw Power Max',min:20,max:160,step:5,section:'POWER',live:true},
 {k:'blastRadius',label:'Blast Radius',min:10,max:120,step:2,section:'COMBAT',live:true},
 {k:'damageMode',label:'Splash Damage',type:'select',options:[['radius','Full Radius Kill'],['falloff','Falloff Threshold']],section:'COMBAT',live:true},
 {k:'falloffThreshold',label:'Falloff Threshold',min:0.05,max:0.95,step:0.05,section:'COMBAT',live:true},
 {k:'buildingCount',label:'Building Count',min:4,max:20,step:1,section:'SKYLINE',live:false},
 {k:'heightMin',label:'Building Height Min',min:30,max:300,step:10,section:'SKYLINE',live:false},
 {k:'heightMax',label:'Building Height Max',min:60,max:380,step:10,section:'SKYLINE',live:false},
 {k:'winTarget',label:'Match Length',type:'select',num:true,options:[[1,'Single Round'],[3,'Best of 3'],[5,'Best of 5']],section:'SESSION',live:true}
];

/* ================= state ================= */
const S={
  cfg:{...DEFAULTS}, phase:'aim', turn:0, winner:null,
  scores:[0,0], round:1, first:0,
  seed:0, rng:null, city:[], specks:[], players:[null,null],
  proj:null, boom:null, parts:[], scorches:[],
  wind:0, t:0, acc:0, pendingKill:false
};

/* ================= audio (WebAudio, generated) ================= */
const AU={
  ctx:null,master:null,windGain:null,muted:false,
  ensure(){
    if(!this.ctx){
      try{
        const AC=window.AudioContext||window.webkitAudioContext;
        this.ctx=new AC();
        this.master=this.ctx.createGain();
        this.master.gain.value=0.5;
        this.master.connect(this.ctx.destination);
        const len=2*this.ctx.sampleRate;
        const buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate);
        const d=buf.getChannelData(0);
        for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
        const src=this.ctx.createBufferSource();src.buffer=buf;src.loop=true;
        const bp=this.ctx.createBiquadFilter();bp.type='bandpass';bp.frequency.value=320;bp.Q.value=0.4;
        this.windGain=this.ctx.createGain();this.windGain.gain.value=0;
        src.connect(bp).connect(this.windGain).connect(this.master);
        src.start();
      }catch(e){this.ctx=null;}
    }
    if(this.ctx&&this.ctx.state==='suspended')this.ctx.resume();
  },
  setWind(n){if(this.ctx&&this.windGain)this.windGain.gain.setTargetAtTime(this.muted?0:n*0.05,this.ctx.currentTime,0.4);},
  tone(type,f0,f1,t0,dur,vol){
    if(!this.ctx||this.muted)return;
    const t=this.ctx.currentTime+t0;
    const o=this.ctx.createOscillator();o.type=type;
    o.frequency.setValueAtTime(Math.max(1,f0),t);
    if(f1)o.frequency.exponentialRampToValueAtTime(Math.max(1,f1),t+dur);
    const g=this.ctx.createGain();
    g.gain.setValueAtTime(vol,t);
    g.gain.exponentialRampToValueAtTime(0.001,t+dur);
    o.connect(g).connect(this.master);
    o.start(t);o.stop(t+dur+0.02);
  },
  launch(){this.tone('triangle',170,760,0,0.28,0.22);this.tone('square',90,240,0,0.18,0.08);},
  explosion(){
    if(!this.ctx||this.muted)return;
    const t=this.ctx.currentTime;
    const len=(0.55*this.ctx.sampleRate)|0;
    const buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate);
    const d=buf.getChannelData(0);
    for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*(1-i/len);
    const src=this.ctx.createBufferSource();src.buffer=buf;
    const lp=this.ctx.createBiquadFilter();lp.type='lowpass';
    lp.frequency.setValueAtTime(900,t);lp.frequency.exponentialRampToValueAtTime(60,t+0.5);
    const g=this.ctx.createGain();g.gain.setValueAtTime(0.7,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.55);
    src.connect(lp).connect(g).connect(this.master);src.start();
    this.tone('sine',110,28,0,0.4,0.5);
  },
  fanfare(){[523.25,659.25,783.99,1046.5].forEach((f,i)=>this.tone('square',f,f,i*0.13,0.14,0.16));},
  toggle(){
    this.muted=!this.muted;
    if(this.master)this.master.gain.value=this.muted?0:0.5;
    if(this.windGain&&!this.muted)this.setWind(Math.abs(S.wind)/Math.max(1,S.cfg.windMax));
    return this.muted;
  }
};
/* ================= city generation ================= */
function genCity(seed){
  S.seed=seed; S.rng=mulberry32(seed);
  const r=S.rng, cfg=S.cfg;
  S.city=[]; S.specks=[];
  for(let i=0;i<70;i++){
    S.specks.push({x:Math.floor(r()*W),y:Math.floor(r()*(groundY-140)),a:0.2+r()*0.5,s:r()<0.3?2:1});
  }
  let n=clamp(cfg.buildingCount|0,4,18);
  const span=W-24;
  const hMin=clamp(cfg.heightMin|0,20,380), hMax=clamp(cfg.heightMax|0,30,420);
  const hLo=Math.min(hMin,hMax), hHi=Math.max(hMin,hMax);
  let totalW=0;
  const widths=[],heights=[];
  for(let i=0;i<n;i++){
    widths.push(48+(r()*54|0));
    heights.push(hLo+((r()*(hHi-hLo))|0));
    totalW+=widths[i];
  }
  const totalGap=Math.max(3*(n-1),span-totalW);
  const gapBase=Math.max(3,totalGap/(n-1)*0.6), gapRand=Math.max(0,totalGap/(n-1)*0.4);
  const gaps=[]; let gsum=0;
  for(let i=0;i<n-1;i++){const g=gapBase+r()*gapRand;gaps.push(g);gsum+=g;}
  const gs=gsum>0?totalGap/gsum:1;
  const palette=['#d94f4f','#e8a13c','#4fc3d9','#7a5cc9','#58b368','#d96aa5','#c9c94f','#4f86d9','#d97a4f','#8a5a3c'];
  let x=12;
  for(let i=0;i<n;i++){
    const w=widths[i],h=heights[i];
    const b={x,y:groundY-h,w,h,color:palette[i%palette.length],winSeed:(r()*1e9)|0,dark:r()<0.35,wins:[]};
    const cols=Math.max(1,Math.floor((w-12)/14));
    const rows=Math.max(1,Math.floor((h-14)/18));
    const ox=(w-(cols*14-4))/2, oy=10;
    for(let c=0;c<cols;c++)for(let rw=0;rw<rows;rw++){
      const lit=b.dark
        ? (((b.winSeed>>>((c*7+rw*13)))&1)===1 && ((b.winSeed>>>((c*3+rw*5)))&1)===1)
        : (((b.winSeed>>>((c*7+rw*13)))&1)===1);
      b.wins.push({x:ox+c*14,y:oy+rw*18,lit});
    }
    S.city.push(b);
    x+=w;
    if(i<n-1)x+=gaps[i]*gs;
  }
}

function pickRoof(list){
  const b=list[(Math.random()*list.length)|0];
  return {x:b.x+b.w/2,y:b.y,b};
}
function placePlayers(){
  const half=Math.floor(S.city.length/2);
  const left=S.city.slice(0,half), right=S.city.slice(half);
  let p1=pickRoof(left), p2=pickRoof(right);
  let tries=0;
  while(Math.abs(p1.x-p2.x)<260&&tries++<40){
    if(Math.random()<0.5)p1=pickRoof(left);else p2=pickRoof(right);
  }
  S.players=[
    Object.assign(p1,{dir:1,aim:30,power:50,dead:false,deadT:0,win:false,id:0}),
    Object.assign(p2,{dir:-1,aim:150,power:50,dead:false,deadT:0,win:false,id:1})
  ];
}

function newWind(){
  const c=S.cfg;
  const min=Math.min(c.windMin,c.windMax), max=Math.max(c.windMin,c.windMax);
  const mag=min+Math.random()*(max-min);
  const dir=Math.random()<0.5?-1:1;
  S.wind=dir*mag;
  AU.setWind(mag/Math.max(1,max));
}

/* ================= turn / combat flow ================= */
function fire(){
  const p=S.players[S.turn];
  const a=p.aim*Math.PI/180;
  const v=p.power*SPEED_SCALE;
  S.proj={x:p.x+p.dir*22,y:p.y-26,vx:Math.cos(a)*v,vy:-Math.sin(a)*v,trail:[],rot:0};
  S.phase='flight';
  AU.launch();
}

function stepProj(dt){
  const pr=S.proj, g=S.cfg.gravity;
  pr.vy+=g*dt;
  pr.vx+=S.wind*WIND_ACCEL*dt;
  const px0=pr.x,py0=pr.y;
  pr.x+=pr.vx*dt; pr.y+=pr.vy*dt;
  pr.rot+=dt*14;
  pr.trail.push({x:pr.x,y:pr.y});
  if(pr.trail.length>26)pr.trail.shift();
  // direct hit on opposing gorilla
  for(let i=0;i<2;i++){
    if(i===S.turn)continue;
    const q=S.players[i];
    if(q.dead)continue;
    const dx=pr.x-q.x, dy=pr.y-(q.y-22);
    if(dx*dx+dy*dy<24*24){impact();return;}
  }
  // ground
  if(pr.y>=groundY-2){pr.y=groundY-2;impact();return;}
  // buildings — stop at first collision, no penetration
  for(const b of S.city){
    if(pr.x+3>=b.x&&pr.x-3<=b.x+b.w&&pr.y+3>=b.y){
      if(py0<b.y-1||px0<b.x-1||px0>b.x+b.w+1){impact();return;}
    }
  }
  // safety bounds
  if(pr.x<-80||pr.x>W+80||pr.y>H+80)impact();
}

function impact(){
  const pr=S.proj;
  S.proj=null;
  explode(pr.x,pr.y);
}

function explode(x,y){
  const c=S.cfg;
  const R=clamp(c.blastRadius,10,120);
  S.boom={x,y,t:0,rad:R};
  S.scorches.push({x,y,r:R});
  if(S.scorches.length>40)S.scorches.shift();
  for(let i=0;i<34;i++){
    const a=Math.random()*Math.PI*2, sp=40+Math.random()*180;
    S.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-60,life:0.5+Math.random()*0.5,t:0,size:2+Math.random()*3,color:Math.random()<0.5?'#ffb84d':'#ff6b4d',dust:false});
  }
  for(let i=0;i<14;i++){
    const a=Math.random()*Math.PI*2, sp=10+Math.random()*40;
    S.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-30,life:0.9+Math.random()*0.6,t:0,size:3+Math.random()*4,color:'#9a93b5',dust:true});
  }
  AU.explosion();
  for(let i=0;i<2;i++){
    const q=S.players[i];
    if(q.dead)continue;
    const d=Math.hypot(x-q.x,y-(q.y-22));
    let hit=d<=R+8;
    if(!hit&&c.damageMode==='falloff'&&d<=R*2){
      const dmg=1-d/(R*2);
      if(dmg>=clamp(c.falloffThreshold,0.01,0.99))hit=true;
    }
    if(hit){q.dead=true;q.deadT=0;}
  }
  S.phase='boom';
}

function resolveBoom(){
  const me=S.turn, op=1-me;
  let winner=null;
  if(S.players[op].dead)winner=me;
  else if(S.players[me].dead)winner=op;
  if(winner===null){
    S.turn=op;
    S.phase='aim';
    if(S.cfg.windMode!=='none')newWind();
    return;
  }
  S.winner=winner;
  S.scores[winner]++;
  S.players[winner].win=true;
  S.phase='roundover';
  AU.fanfare();
}

function nextRound(){
  S.round++;
  genCity((Math.random()*0x7fffffff)|0);
  placePlayers();
  S.scorches=[];S.parts=[];S.boom=null;S.proj=null;
  S.winner=null;
  S.first=(Math.random()*2)|0;
  S.turn=S.first;
  newWind();
  S.phase='aim';
}

function newMatch(){
  S.scores=[0,0];
  S.round=1;
  genCity((Math.random()*0x7fffffff)|0);
  placePlayers();
  S.scorches=[];S.parts=[];S.boom=null;S.proj=null;
  S.winner=null;
  S.first=(Math.random()*2)|0;
  S.turn=S.first;
  newWind();
  S.phase='aim';
}
/* ================= rendering ================= */
const cv=document.getElementById('cv');
const ctx=cv.getContext('2d');
ctx.imageSmoothingEnabled=false;

function drawSky(){
  const g=ctx.createLinearGradient(0,0,0,groundY);
  g.addColorStop(0,'#12142e');
  g.addColorStop(0.55,'#2a2a5e');
  g.addColorStop(0.85,'#6e4a7e');
  g.addColorStop(1,'#c96a4f');
  ctx.fillStyle=g;
  ctx.fillRect(0,0,W,groundY);
  // stars
  for(const s of S.specks){
    ctx.globalAlpha=s.a;
    ctx.fillStyle='#cfd6ff';
    ctx.fillRect(s.x,s.y,s.s,s.s);
  }
  ctx.globalAlpha=1;
  // sun
  const sx=790,sy=118;
  const rg=ctx.createRadialGradient(sx,sy,10,sx,sy,90);
  rg.addColorStop(0,'rgba(255,190,90,0.55)');
  rg.addColorStop(1,'rgba(255,190,90,0)');
  ctx.fillStyle=rg;
  ctx.fillRect(sx-90,sy-90,180,180);
  ctx.fillStyle='#ffcf4d';
  ctx.beginPath();ctx.arc(sx,sy,34,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='#ffe9a8';
  ctx.beginPath();ctx.arc(sx,sy,26,0,Math.PI*2);ctx.fill();
  // clouds
  cloud(120,90,1);cloud(430,150,0.7);cloud(640,70,0.85);cloud(880,180,0.6);
  function cloud(cx,cy,s){
    ctx.globalAlpha=0.55;
    ctx.fillStyle='#8f7fa8';
    ctx.fillRect(cx-34*s,cy,68*s,12*s);
    ctx.fillRect(cx-20*s,cy-9*s,40*s,10*s);
    ctx.fillRect(cx-10*s,cy+12*s,50*s,8*s);
    ctx.globalAlpha=1;
  }
}

function drawCity(){
  for(const b of S.city){
    ctx.fillStyle=b.color;
    ctx.fillRect(b.x,b.y,b.w,b.h);
    ctx.fillStyle='rgba(0,0,0,0.3)';
    ctx.fillRect(b.x+b.w-6,b.y,6,b.h);
    ctx.fillStyle='rgba(255,255,255,0.18)';
    ctx.fillRect(b.x,b.y,b.w,4);
    // antenna on some
    if(b.h>200){
      ctx.fillStyle='#20223a';
      ctx.fillRect(b.x+b.w/2-2,b.y-16,4,16);
      ctx.fillRect(b.x+b.w/2-5,b.y-16,10,3);
    }
    // windows
    for(const wn of b.wins){
      ctx.fillStyle=wn.lit?'#ffd94d':'#1c2136';
      ctx.fillRect(b.x+wn.x,b.y+wn.y,8,10);
    }
  }
}

function drawGround(){
  ctx.fillStyle='#20242f';
  ctx.fillRect(0,groundY,W,GROUND_H);
  ctx.fillStyle='#3a4152';
  ctx.fillRect(0,groundY,W,5);
  ctx.fillStyle='#14161f';
  for(let x=8;x<W;x+=24)ctx.fillRect(x,groundY+14,12,4);
  // scorch marks
  for(const s of S.scorches){
    ctx.globalAlpha=0.4;
    ctx.fillStyle='#000';
    ctx.beginPath();
    ctx.ellipse(s.x,Math.min(s.y,groundY-2),s.r*0.5,Math.max(3,s.r*0.18),0,0,Math.PI*2);
    ctx.fill();
    ctx.globalAlpha=1;
  }
}

function drawGorilla(g){
  const x=g.x, y=g.y;
  const body=g.id===0?'#e84a3f':'#3f7fe8';
  const bodyD=shade(body,-50);
  const skin='#c98d5a';
  const t=S.t;
  const bob=g.dead?0:Math.sin(t*2.4+g.id*2)*2;
  ctx.save();
  ctx.translate(x,y+bob);
  if(g.dead){
    // X eyes, slumped
    ctx.globalAlpha=0.85;
    ctx.fillStyle=body;
    ctx.fillRect(-14,-26,28,26);
    ctx.fillStyle=skin;
    ctx.fillRect(-9,-22,18,12);
    ctx.strokeStyle='#1a1a2e';ctx.lineWidth=2;
    for(const ex of[-4,4]){
      ctx.beginPath();ctx.moveTo(ex-3,-19);ctx.lineTo(ex+3,-13);
      ctx.moveTo(ex+3,-19);ctx.lineTo(ex-3,-13);ctx.stroke();
    }
    ctx.restore();
    return;
  }
  // idle sway
  const sway=Math.sin(t*2.4+g.id*2)*1.5;
  // body
  ctx.fillStyle=body;
  ctx.fillRect(-14,-30+sway*0.3,28,30);
  ctx.fillStyle=bodyD;
  ctx.fillRect(-14,-30+sway*0.3,4,30);
  // head
  ctx.fillStyle=body;
  ctx.fillRect(-12,-46+sway,24,18);
  ctx.fillStyle=skin;
  ctx.fillRect(-8,-40+sway,16,11);
  ctx.fillStyle=body;
  ctx.fillRect(-12,-46+sway,24,5);
  // ears
  ctx.fillRect(-16,-42+sway,5,6);
  ctx.fillRect(11,-42+sway,5,6);
  // eyes (blink)
  const blink=(t%3.4)<0.12;
  ctx.fillStyle='#fff';
  ctx.fillRect(-6,-38+sway,5,4);
  ctx.fillRect(1,-38+sway,5,4);
  if(!blink){
    ctx.fillStyle='#1a1a2e';
    ctx.fillRect(g.dir>0?-3: -4,-37+sway,2,2);
    ctx.fillRect(g.dir>0? 3: 2,-37+sway,2,2);
  }else{
    ctx.fillStyle='#1a1a2e';
    ctx.fillRect(-6,-37+sway,5,1);
    ctx.fillRect(1,-37+sway,5,1);
  }
  // mouth
  ctx.fillStyle='#7a4a2e';
  ctx.fillRect(-3,-31+sway,6,2);
  // arms
  ctx.fillStyle=bodyD;
  ctx.fillRect(-18,-26,5,16);
  ctx.fillRect(13,-26,5,16);
  // legs
  ctx.fillStyle=bodyD;
  ctx.fillRect(-12,-4,9,4);
  ctx.fillRect(3,-4,9,4);
  ctx.restore();
  // win pose
  if(g.win){
    ctx.save();
    ctx.translate(x,y);
    const wv=Math.sin(t*8)*6;
    ctx.fillStyle=body;
    ctx.fillRect(-20,-44+wv,6,14);
    ctx.fillRect(14,-44-wv,6,14);
    ctx.restore();
  }
  // name tag
  ctx.font=F(7);
  ctx.textAlign='center';
  ctx.fillStyle=g.id===0?'#ff8d7a':'#7ab4ff';
  ctx.fillText('P'+(g.id+1),x,y-52+bob);
}

function drawProjectile(){
  const pr=S.proj;
  if(!pr)return;
  // trail
  for(let i=0;i<pr.trail.length;i++){
    const p=pr.trail[i];
    ctx.globalAlpha=(i/pr.trail.length)*0.5;
    ctx.fillStyle='#ffe14d';
    const s=1+(i/pr.trail.length)*2;
    ctx.fillRect(p.x-s/2,p.y-s/2,s,s);
  }
  ctx.globalAlpha=1;
  ctx.save();
  ctx.translate(pr.x,pr.y);
  ctx.rotate(pr.rot);
  // banana body
  ctx.fillStyle='#ffd23f';
  ctx.fillRect(-6,-3,12,6);
  ctx.fillRect(-8,-2,4,4);
  ctx.fillRect(4,-2,4,4);
  ctx.fillStyle='#b8860b';
  ctx.fillRect(-9,-2,2,4);
  ctx.fillRect(7,-2,2,4);
  ctx.fillStyle='#8a6508';
  ctx.fillRect(-6,-1,3,2);
  ctx.restore();
}

function drawBoom(){
  const b=S.boom;
  if(!b)return;
  const p=b.t/0.55;
  if(p>=1)return;
  const e=easeOutCubic(Math.min(1,p*1.4));
  const r=b.rad*e;
  const g=ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,Math.max(1,r));
  g.addColorStop(0,'rgba(255,255,220,'+(0.9*(1-p))+')');
  g.addColorStop(0.4,'rgba(255,170,60,'+(0.7*(1-p))+')');
  g.addColorStop(0.8,'rgba(255,80,40,'+(0.4*(1-p))+')');
  g.addColorStop(1,'rgba(255,80,40,0)');
  ctx.fillStyle=g;
  ctx.beginPath();ctx.arc(b.x,b.y,r,0,Math.PI*2);ctx.fill();
  ctx.strokeStyle='rgba(255,230,160,'+(0.8*(1-p))+')';
  ctx.lineWidth=3;
  ctx.beginPath();ctx.arc(b.x,b.y,r*0.85,0,Math.PI*2);ctx.stroke();
}

function drawParts(){
  for(const p of S.parts){
    ctx.globalAlpha=Math.max(0,1-p.t/p.life);
    ctx.fillStyle=p.color;
    ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
  }
  ctx.globalAlpha=1;
}

function drawAim(){
  if(S.phase!=='aim')return;
  const p=S.players[S.turn];
  const a=p.aim*Math.PI/180;
  const v=p.power*SPEED_SCALE;
  // dotted trajectory preview (no wind)
  let x=p.x+p.dir*22,y=p.y-26;
  let vx=Math.cos(a)*v,vy=-Math.sin(a)*v;
  const dt=0.05;
  ctx.fillStyle='rgba(255,255,255,0.5)';
  for(let i=0;i<70;i++){
    vy+=S.cfg.gravity*dt;
    x+=vx*dt;y+=vy*dt;
    if(y>=groundY-2)break;
    let hitB=false;
    for(const b of S.city)if(x>=b.x&&x<=b.x+b.w&&y>=b.y){hitB=true;break;}
    if(hitB)break;
    if(i%2===0)ctx.fillRect(x-1.5,y-1.5,3,3);
  }
  // arrow
  const len=34+p.power*0.5;
  const ax=p.x+p.dir*22+Math.cos(a)*len;
  const ay=p.y-26-Math.sin(a)*len;
  ctx.strokeStyle=p.id===0?'#ff8d7a':'#7ab4ff';
  ctx.lineWidth=3;
  ctx.beginPath();
  ctx.moveTo(p.x+p.dir*22,p.y-26);
  ctx.lineTo(ax,ay);
  ctx.stroke();
  ctx.fillStyle=ctx.strokeStyle;
  ctx.beginPath();
  ctx.moveTo(ax,ay);
  ctx.lineTo(ax-Math.cos(a-0.4)*10,ay+Math.sin(a-0.4)*10);
  ctx.lineTo(ax-Math.cos(a+0.4)*10,ay+Math.sin(a+0.4)*10);
  ctx.closePath();ctx.fill();
}

function drawHUD(){
  // top bar
  ctx.fillStyle='rgba(8,10,22,0.85)';
  ctx.fillRect(0,0,W,34);
  ctx.fillStyle='#2a2f55';
  ctx.fillRect(0,34,W,2);
  ctx.font=F(9);
  // wind
  const c=S.cfg;
  const wmag=Math.abs(S.wind);
  const wdir=S.wind>=0?'→':'←';
  ctx.textAlign='left';
  ctx.fillStyle='#8fe3ff';
  ctx.fillText('WIND',14,16);
  ctx.fillStyle='#fff';
  ctx.fillText(wmag.toFixed(1)+' '+wdir,14,29);
  // wind bar
  const bx=96,bw=110;
  ctx.fillStyle='#1c2145';
  ctx.fillRect(bx,10,bw,14);
  ctx.strokeStyle='#3a4180';ctx.lineWidth=1;
  ctx.strokeRect(bx+0.5,10.5,bw-1,13);
  const frac=wmag/Math.max(1,c.windMax);
  const fh=bw*clamp(frac,0,1);
  ctx.fillStyle=S.wind>=0?'#4fc3d9':'#d94f4f';
  if(S.wind>=0)ctx.fillRect(bx+1,11,fh-2,12);
  else ctx.fillRect(bx+bw-fh+1,11,fh-2,12);
  // turn indicator (center)
  const tn=S.turn+1;
  ctx.textAlign='center';
  ctx.fillStyle=S.turn===0?'#ff8d7a':'#7ab4ff';
  ctx.fillText('PLAYER '+tn+' TURN',W/2,16);
  ctx.fillStyle='#8a90c8';
  ctx.font=F(7);
  ctx.fillText('ROUND '+S.round,W/2,29);
  // scores (between wind bar and turn text)
  ctx.font=F(9);
  ctx.textAlign='left';
  ctx.fillStyle='#ff8d7a';
  ctx.fillText('P1 '+S.scores[0],222,16);
  ctx.fillStyle='#7ab4ff';
  ctx.fillText('P2 '+S.scores[1],302,16);
  ctx.font=F(7);
  ctx.fillStyle='#8a90c8';
  const tgt=c.winTarget|0;
  ctx.fillText(tgt>1?('FIRST TO '+tgt+' WINS'):'SINGLE ROUND',222,29);
  // seed
  ctx.textAlign='left';
  ctx.fillStyle='#4a5080';
  ctx.font=F(6);
  ctx.fillText('SEED '+S.seed,14,44);
  // aim readout (bottom)
  if(S.phase==='aim'){
    const p=S.players[S.turn];
    ctx.fillStyle='rgba(8,10,22,0.85)';
    ctx.fillRect(0,H-30,W,30);
    ctx.fillStyle='#2a2f55';
    ctx.fillRect(0,H-30,W,2);
    ctx.font=F(9);
    ctx.textAlign='left';
    ctx.fillStyle='#ffd23f';
    ctx.fillText('ANGLE',14,H-12);
    ctx.fillStyle='#fff';
    ctx.fillText(String(Math.round(p.aim))+'°',90,H-12);
    ctx.fillStyle='#ffd23f';
    ctx.fillText('POWER',190,H-12);
    ctx.fillStyle='#fff';
    ctx.fillText(String(Math.round(p.power)),270,H-12);
    // power bar
    const px=340,pw=160;
    ctx.fillStyle='#1c2145';
    ctx.fillRect(px,H-20,pw,12);
    ctx.strokeStyle='#3a4180';
    ctx.strokeRect(px+0.5,H-19.5,pw-1,11);
    const pmn=c.powerMin,pmx=c.powerMax;
    const pf=(p.power-pmn)/Math.max(1,pmx-pmn);
    ctx.fillStyle=p.id===0?'#e84a3f':'#3f7fe8';
    ctx.fillRect(px+1,H-19,Math.max(2,(pw-2)*clamp(pf,0,1)),10);
    ctx.fillStyle='#8a90c8';
    ctx.font=F(7);
    ctx.fillText(pmn+'-'+pmx,px+pw+8,H-12);
    ctx.fillStyle='#ffd23f';
    ctx.font=F(8);
    ctx.fillText('ENTER / SPACE TO FIRE',W-230,H-12);
  }
}

function drawBanner(){
  if(S.phase!=='roundover')return;
  ctx.fillStyle='rgba(5,6,14,0.72)';
  ctx.fillRect(0,0,W,H);
  const wnr=S.winner;
  const col=wnr===0?'#ff8d7a':'#7ab4ff';
  ctx.textAlign='center';
  ctx.fillStyle=col;
  ctx.font=F(26);
  ctx.fillText('PLAYER '+(wnr+1)+' WINS THE ROUND!',W/2,H/2-56);
  ctx.fillStyle='#fff';
  ctx.font=F(12);
  ctx.fillText('SCORE   P1 '+S.scores[0]+'  :  '+S.scores[1]+' P2',W/2,H/2-16);
  const tgt=S.cfg.winTarget|0;
  if(tgt>1&&S.scores[wnr]>=tgt){
    ctx.fillStyle='#ffd23f';
    ctx.font=F(14);
    ctx.fillText('★ MATCH CHAMPION ★',W/2,H/2+20);
    ctx.fillStyle='#c6caf2';
    ctx.font=F(9);
    ctx.fillText('PRESS ENTER FOR A FRESH MATCH',W/2,H/2+56);
  }else{
    ctx.fillStyle='#c6caf2';
    ctx.font=F(9);
    ctx.fillText('PRESS ENTER FOR REMATCH — NEW SKYLINE',W/2,H/2+56);
  }
  // blinking
  if((S.t*2|0)%2===0){
    ctx.fillStyle='#ffd23f';
    ctx.font=F(8);
    ctx.fillText('» GORILLAZ «',W/2,H/2+84);
  }
}

function draw(){
  drawSky();
  drawCity();
  drawGround();
  drawAim();
  for(const g of S.players)drawGorilla(g);
  drawProjectile();
  drawBoom();
  drawParts();
  drawHUD();
  drawBanner();
}
/* ================= main loop ================= */
let last=performance.now();
function frame(now){
  let dt=(now-last)/1000;
  last=now;
  if(dt>0.25)dt=0.25;
  S.t+=dt;
  S.acc+=dt;
  let guard=0;
  while(S.acc>=STEP&&guard++<10){
    S.acc-=STEP;
    tick(STEP);
  }
  draw();
  requestAnimationFrame(frame);
}

function tick(dt){
  if(S.phase==='flight'&&S.proj){
    stepProj(dt);
  }
  if(S.phase==='boom'){
    S.boom.t+=dt;
    if(S.boom.t>=0.55){
      S.boom=null;
      resolveBoom();
    }
  }
  for(let i=S.parts.length-1;i>=0;i--){
    const p=S.parts[i];
    p.t+=dt;
    if(p.t>=p.life){S.parts.splice(i,1);continue;}
    p.vy+=160*dt;
    p.x+=p.vx*dt;p.y+=p.vy*dt;
    if(p.y>groundY){p.y=groundY;p.vy*=-0.3;p.vx*=0.7;}
  }
  for(const g of S.players)if(g.dead)g.deadT+=dt;
}

/* ================= input ================= */
window.addEventListener('keydown',e=>{
  AU.ensure();
  const k=e.key;
  if(k==='t'||k==='T'){togglePanel();return;}
  if(k==='m'||k==='M'){toggleMute();return;}
  if(document.activeElement&&document.activeElement.tagName==='INPUT')return;
  if(S.phase==='aim'){
    const p=S.players[S.turn];
    const c=S.cfg;
    if(k==='ArrowLeft'){p.aim=clamp(p.aim+1,1,179);e.preventDefault();}
    else if(k==='ArrowRight'){p.aim=clamp(p.aim-1,1,179);e.preventDefault();}
    else if(k==='ArrowUp'){p.power=clamp(p.power+1,c.powerMin,c.powerMax);e.preventDefault();}
    else if(k==='ArrowDown'){p.power=clamp(p.power-1,c.powerMin,c.powerMax);e.preventDefault();}
    else if(k==='Enter'||k===' '){fire();e.preventDefault();}
  }else if(S.phase==='roundover'){
    if(k==='Enter'||k===' '){
      const tgt=c.winTarget|0;
      if(tgt>1&&S.scores[S.winner]>=tgt)newMatch();
      else nextRound();
      e.preventDefault();
    }
  }
});
function c(){return S.cfg;}
document.addEventListener('keydown',e=>{
  if((e.key==='Enter'||e.key===' ')&&S.phase==='aim'&&document.activeElement&&document.activeElement.tagName==='SELECT')return;
});

/* ================= tuning panel ================= */
const panel=document.getElementById('panel');
const badge=document.getElementById('badge');
const rowsEl=document.getElementById('rows');
const rowRefs={};

function buildPanel(){
  rowsEl.innerHTML='';
  let curSec='';
  for(const f of SCHEMA){
    if(f.section!==curSec){
      curSec=f.section;
      const h=document.createElement('div');
      h.className='psec';
      h.textContent=f.section;
      rowsEl.appendChild(h);
    }
    const row=document.createElement('div');
    row.className='prow'+(f.type==='select'?' sel':'');
    const lab=document.createElement('div');
    lab.className='plabel';
    lab.textContent=f.label;
    row.appendChild(lab);
    const refs={row,f};
    rowRefs[f.k]=refs;
    if(f.type==='select'){
      const sel=document.createElement('select');
      sel.className='pval';
      for(const o of f.options){
        const op=document.createElement('option');
        op.value=String(o[0]);
        op.textContent=o[1];
        sel.appendChild(op);
      }
      sel.value=String(S.cfg[f.k]);
      sel.addEventListener('change',()=>{
        S.cfg[f.k]=f.num?Number(sel.value):sel.value;
        if(f.k==='windMode'&&S.cfg.windMode!=='none')newWind();
        syncPanel();
      });
      row.appendChild(sel);
      refs.val=sel;
    }else{
      const minus=document.createElement('button');
      minus.className='pbtn';minus.textContent='−';
      minus.addEventListener('click',()=>{
        S.cfg[f.k]=clamp(S.cfg[f.k]-f.step,f.min,f.max);
        syncPanel();
      });
      const inp=document.createElement('input');
      inp.className='pval';
      inp.type='number';
      inp.min=f.min;inp.max=f.max;inp.step=f.step;
      inp.addEventListener('change',()=>{
        const v=parseFloat(inp.value);
        if(Number.isFinite(v))S.cfg[f.k]=clamp(v,f.min,f.max);
        syncPanel();
      });
      const plus=document.createElement('button');
      plus.className='pbtn';plus.textContent='+';
      plus.addEventListener('click',()=>{
        S.cfg[f.k]=clamp(S.cfg[f.k]+f.step,f.min,f.max);
        syncPanel();
      });
      row.appendChild(minus);row.appendChild(inp);row.appendChild(plus);
      refs.val=inp;
      const live=document.createElement('div');
      live.className='plive';
      row.appendChild(live);
      refs.live=live;
    }
    rowsEl.appendChild(row);
  }
  syncPanel();
}

function syncPanel(){
  let anyEdited=false;
  for(const f of SCHEMA){
    const refs=rowRefs[f.k];
    if(!refs)continue;
    const def=DEFAULTS[f.k], cur=S.cfg[f.k];
    const edited=String(def)!==String(cur);
    if(edited)anyEdited=true;
    if(f.type==='select'){
      refs.val.value=String(cur);
      refs.val.classList.toggle('edited',edited);
    }else{
      if(document.activeElement!==refs.val)refs.val.value=fmt(cur);
      refs.val.classList.toggle('edited',edited);
      if(refs.live){
        refs.live.textContent=f.live?'APPLIES NOW':'NEXT ROUND';
        refs.live.className='plive'+(f.live?'':' next');
      }
    }
  }
  badge.textContent=anyEdited?'LIVE TUNING ACTIVE':'DEFAULTS';
  badge.classList.toggle('active',anyEdited);
}

function togglePanel(){
  panel.classList.toggle('open');
}
function toggleMute(){
  AU.ensure();
  const m=AU.toggle();
  document.getElementById('btnMute').textContent=m?'♪ OFF [M]':'♪ ON [M]';
}
document.getElementById('btnTune').addEventListener('click',()=>{AU.ensure();togglePanel();});
document.getElementById('btnMute').addEventListener('click',toggleMute);
document.getElementById('btnReset').addEventListener('click',()=>{
  S.cfg={...DEFAULTS};
  syncPanel();
  newWind();
});

/* ================= boot ================= */
buildPanel();
genCity((Math.random()*0x7fffffff)|0);
placePlayers();
S.first=(Math.random()*2)|0;
S.turn=S.first;
newWind();
S.phase='aim';
requestAnimationFrame(frame);
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"full","sessionId":"c973d702-4646-42b2-bc53-d12663a46d82","tokensIn":423187,"tokensOut":66383,"tokensTotal":489570,"cost":0,"turns":15,"toolCalls":14,"failedToolCalls":0,"timestamp":"2026-09-08T21:48:49.519Z"} -->