← Gorillaz results

Gorillaz

Claude haiku 5.5 · full

Initial view of Gorillaz

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Task text captured before this run. Effective runtime system instructions were not captured.

Generation prompt · 631bf49400
Game Spec — "Gorillaz" (Artillery Clone)

Concept & Overview A two-player, turn-based physics artillery game. Each match takes place on a randomly generated urban skyline. Two gorilla characters stand atop separate buildings and take turns hurling an explosive projectile—styled as a banana—at each other. The first gorilla eliminated by blast or direct impact loses the round.

Players & Setup

Exactly two human players; local turn-based only.
At match start, generate a fresh city skyline composed of rectangular buildings of random widths and heights arranged along a flat ground line.
Place one gorilla on the roof of a randomly selected left-side building and the other on a randomly selected right-side building, ensuring neither starts inside blast range of the other.
Core Loop (Per Turn)

Active player receives control.
Player sets two independent values: launch angle (direction/arc) and throw velocity/power.
The projectile launches along a ballistic trajectory influenced by constant downward gravity and a visible, variable wind vector (speed and horizontal direction).
The projectile travels until it intersects a building, the ground, or the opposing gorilla.
On intersection, trigger an explosion with a fixed circular blast radius.
If the opposing gorilla is within the blast radius or struck directly, that player is eliminated and the active player wins the match.
If no elimination occurs, the turn passes to the opponent. Wind may shift randomly between turns or remain constant per match rules—specify visible wind indicator updates after each turn.
Physics & Combat Rules

Ballistic arc: projectile path is a parabola modified by gravity.
Wind: horizontal force applied continuously during flight; display current wind speed and left/right direction in a persistent HUD element.
Collision: projectile collides with building edges/roofs and ground plane. Buildings do not take damage or alter shape from explosions.
Explosion: circular damage zone centered on impact point. No chain reactions.
Direct hit and splash damage both count as a kill.
Environment

Procedural skyline: 8–14 buildings, random heights within a defined min/max range, random widths with small gaps between them.
Flat ground plane across the bottom.
Static background: sky gradient, a simple sun/moon graphic, and decorative clouds (optional, non-interactive).
No scrolling; entire playfield visible in a single screen frame.
Visual Style

Retro arcade aesthetic: blocky buildings in flat, high-contrast colors; simple pixel-art or vector-style gorilla sprites with minimal animation (idle stance, win/loss reaction).
Projectile rendered as a small curved sprite with motion trail.
Explosion rendered as an expanding radial circle with brief particle/dust effect, then a lingering scorch marker (optional).
Buildings colored distinctly per structure; gorillas color-coded to match player identity (e.g., red vs. blue).
User Interface / HUD

Top bar: current wind speed and directional arrow/indicator.
Turn indicator: clearly labeled “Player 1 Turn” / “Player 2 Turn.”
During aiming/input phase: display current angle value and velocity/power value being set, updating in real time as the player adjusts.
Score tracking: number of rounds won per player across multiple consecutive matches (optional persistent session score).
Controls (Input Specification)

Each player uses the same input device in sequence, or separate input mappings if hardware allows.
Angle control: increment/decrease launch angle in discrete steps or continuous adjustment.
Velocity control: increment/decrement throw power on a defined scale (e.g., 1–100).
Confirm/fire button to execute the throw with current values.
No pause/menu system required other than a “New Match / Rematch” prompt after a win.
Win Condition & Session Flow

Single round ends immediately upon one gorilla being eliminated.
After round resolution, display winner and offer rematch with a new randomly generated skyline.
Optional: best-of-three or best-of-five session mode.
Audio Design (Optional but Specified)

Launch sound on throw execution.
Impact/explosion sound on collision.
Brief victory fanfare or gorilla vocalization on round end.
Subtle wind ambience or tone reflecting current wind strength.
Live Configuration / Runtime Tuning The game must expose a live-editable configuration layer—accessible via an in-game settings panel, overlay menu, or dedicated tuning screen—that allows direct modification of internal constants equivalent to those originally hardcoded in the source file. Changes apply immediately or take effect on the very next turn/projectile launch without requiring a restart or recompilation.

Required configurable values (minimum set):

Blast / Explosion Radius: the circular damage radius triggered on impact.
Gravity Strength: the downward acceleration constant applied to the projectile arc.
Wind Variance / Range: the minimum and maximum possible wind speed values and whether direction shifts are permitted.
Building Count: the number of buildings generated in the skyline.
Building Height Range: minimum and maximum allowable building heights.
Projectile Velocity Scale: the upper and lower bounds of the throw power input range.
Explosion Damage Threshold: whether splash damage requires being inside the radius or allows a configurable falloff.
The interface must display current numeric values for each parameter, allow increment/decrement or direct value entry, and provide a visible indicator (e.g., a “Live Tuning Active” badge or color-coded value) confirming that edited values are currently in effect. Default values must match the classic reference experience, with edits persisting for the session or optionally resetting per match.

Constraints & Rules

No power-ups, item drops, or terrain destruction.
No AI opponent; strictly two-player local.
No network/multiplayer mode.
Projectiles must not penetrate through buildings; they stop at first collision.
Gorilla positions are fixed horizontally; only vertical placement changes based on building height.
All live-edited physics and gameplay constants must remain deterministic within a given turn; no hidden random modifiers outside the configured ranges.
Non-Functional Requirements (Design Only)

Must run at a consistent frame rate with deterministic physics per turn.
Screen resolution should preserve the retro aspect ratio or allow clean scaling without distortion.
All random elements (city layout, wind shifts, gorilla placement) must use visible, reproducible seeds if a replay/debug feature is included (optional).
The live configuration interface must be non-blocking: players can open it between turns without aborting the current session state.
Name & Branding

Title: “Gorillaz” (stylized with a “z”).
Characters referred to as gorillas; projectile referred to as a “banana bomb” or “exploding banana.”
Tone: lighthearted arcade competition with a retro 1990s shareware aesthetic.
Write the implementation using any suitable architecture, framework, or technology stack, provided the above mechanics, physics rules, visual descriptions, session flow, and live configuration editing requirements are preserved exactly. Do not alter the turn-based structure, the two-player requirement, the ballistic physics model, or the presence of the live-tunable constant values.
Inspect original source 28,345 bytes · SHA-256 df54450c1704
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Gorillaz</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:#06071a;overflow:hidden;font-family:"Courier New",Courier,monospace;color:#fff;user-select:none}
body{display:flex;align-items:center;justify-content:center}
#wrap{position:relative;width:min(96vw,calc((100vh - 26px) * 4 / 3));aspect-ratio:4/3;border:4px solid #ffd23f;border-radius:4px;box-shadow:0 0 0 4px #000,0 0 40px rgba(255,210,63,.25);background:#000}
canvas{position:absolute;inset:0;width:100%;height:100%;display:block;image-rendering:pixelated}
#overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:rgba(4,4,18,.55)}
#overlay.show{display:flex}
.card{background:#101235;border:4px solid #ff4fa3;box-shadow:6px 6px 0 #000;padding:20px 26px;text-align:center;max-width:84%}
.card h1{margin:0 0 4px;font-size:clamp(28px,5vw,54px);color:#ffd23f;text-shadow:3px 3px 0 #e53935,6px 6px 0 #000;letter-spacing:3px}
.card h2{margin:6px 0 4px;font-size:clamp(15px,2.4vw,22px);color:#7ef0ff}
.card p{margin:6px 0 12px;font-size:clamp(11px,1.5vw,14px);color:#dfe6ff;line-height:1.5}
button,select{font-family:inherit;font-weight:bold;background:#ffd23f;color:#111;border:3px solid #000;box-shadow:3px 3px 0 #000;padding:6px 12px;cursor:pointer;font-size:13px}
button:hover{background:#fff27a}
button:active{transform:translate(2px,2px);box-shadow:1px 1px 0 #000}
select{background:#fff}
.hidden{display:none!important}
#tuneBtn{position:fixed;top:8px;right:8px;z-index:5;font-size:12px;padding:5px 9px}
#panel{position:fixed;top:44px;right:8px;width:300px;max-height:calc(100vh - 60px);overflow:auto;z-index:4;background:rgba(10,12,34,.96);border:3px solid #ffd23f;box-shadow:5px 5px 0 #000;padding:10px 12px;font-size:12px}
#panel h3{margin:0 0 6px;color:#ffd23f;font-size:15px;letter-spacing:1px}
#badge{display:inline-block;padding:3px 7px;font-weight:bold;font-size:11px;border:2px solid #000;margin-bottom:6px}
#badge.live{background:#3cff7a;color:#042;animation:blink 1.2s steps(2) infinite}
#badge.def{background:#666;color:#fff}
@keyframes blink{50%{opacity:.55}}
.grp{margin:10px 0 4px;color:#7ef0ff;border-bottom:1px dashed #334;font-weight:bold}
.row{display:flex;align-items:center;justify-content:space-between;gap:6px;margin:5px 0}
.lab{flex:1;line-height:1.2}
.tag{display:block;font-size:9px;color:#8890c0}
.ctl{display:flex;gap:3px;align-items:center}
.ctl button{padding:1px 6px;font-size:12px;box-shadow:2px 2px 0 #000}
.ctl input[type=number]{width:66px;background:#000;color:#fff;border:2px solid #555;font-family:inherit;font-size:12px;padding:2px;text-align:right}
.row.changed .ctl input[type=number]{color:#3cff7a;border-color:#3cff7a;font-weight:bold}
.row.changed .lab{color:#ffe066}
.row input[type=checkbox]{transform:scale(1.3);margin-right:4px}
#help{position:fixed;left:8px;bottom:6px;font-size:11px;color:#8890c0}
</style>
</head>
<body>
<div id="wrap">
  <canvas id="c" width="800" height="600"></canvas>
  <div id="overlay" class="show">
    <div class="card">
      <h1 id="ovTitle">GORILLAZ</h1>
      <h2 id="ovSub">Banana Bomb Artillery · Two Gorillas Enter</h2>
      <p id="ovText">Player 1 (RED) and Player 2 (BLUE) take turns hurling exploding bananas. Last gorilla standing wins the round.</p>
      <p id="ovBo">Session: <select id="boSel"><option value="1">Single round</option><option value="3" selected>Best of 3</option><option value="5">Best of 5</option></select></p>
      <button id="ovBtn">START</button>
    </div>
  </div>
</div>
<button id="tuneBtn">⚙ TUNING [T]</button>
<div id="panel" class="hidden">
  <h3>LIVE TUNING</h3>
  <div id="badge" class="def">DEFAULT VALUES</div>
  <div id="pbody"></div>
  <p style="font-size:10px;color:#8890c0;line-height:1.4">Edits apply live without pausing. Tags show when each value takes effect. Edited values turn green.</p>
  <button id="resetBtn">RESET DEFAULTS</button>
</div>
<div id="help">← → angle · ↑ ↓ power · SPACE fire · T tuning · M mute</div>

<script>
// ---------- constants & config ----------
const W=800,H=600,HUDH=60,GROUND=500,STEP=1/240;
const cv=document.getElementById('c'),g=cv.getContext('2d');

const DEF={blastRadius:38,damageThreshold:0,gravity:420,velMin:100,velMax:560,windMin:10,windMax:70,windEachTurn:true,windFlip:true,bldMin:8,bldMax:14,hMin:70,hMax:230,resetEachMatch:false};
const cfg=Object.assign({},DEF);

const SCHEMA=[
  {grp:'EXPLOSION'},
  {k:'blastRadius',l:'Blast radius (px)',min:10,max:120,step:2,apply:'next impact'},
  {k:'damageThreshold',l:'Damage threshold (%)',min:0,max:95,step:5,apply:'0 = strict radius · >0 = falloff'},
  {grp:'PHYSICS'},
  {k:'gravity',l:'Gravity (px/s²)',min:50,max:1200,step:10,apply:'next launch'},
  {k:'velMin',l:'Power MIN speed (px/s)',min:20,max:400,step:10,apply:'next launch · power 1 maps here'},
  {k:'velMax',l:'Power MAX speed (px/s)',min:100,max:1200,step:10,apply:'next launch · power 100 maps here'},
  {grp:'WIND'},
  {k:'windMin',l:'Wind MIN (px/s²)',min:0,max:200,step:5,apply:'next turn'},
  {k:'windMax',l:'Wind MAX (px/s²)',min:0,max:200,step:5,apply:'next turn'},
  {k:'windEachTurn',l:'Wind changes each turn',t:'bool',apply:'off = fixed per match'},
  {k:'windFlip',l:'Direction may flip',t:'bool',apply:'next turn'},
  {grp:'SKYLINE'},
  {k:'bldMin',l:'Buildings MIN',min:4,max:14,step:1,apply:'next match'},
  {k:'bldMax',l:'Buildings MAX',min:4,max:16,step:1,apply:'next match'},
  {k:'hMin',l:'Height MIN (px)',min:30,max:300,step:10,apply:'next match'},
  {k:'hMax',l:'Height MAX (px)',min:40,max:380,step:10,apply:'next match'},
  {grp:'SESSION'},
  {k:'resetEachMatch',l:'Reset tuning each round',t:'bool',apply:'restores defaults on new round'},
];
const FIELD={};SCHEMA.forEach(r=>{if(r.k)FIELD[r.k]=r;});
const PAIRS=[['velMin','velMax'],['windMin','windMax'],['bldMin','bldMax'],['hMin','hMax']];

function setCfg(k,v){
  const r=FIELD[k];
  if(r.t==='bool'){cfg[k]=!!v;}
  else{
    v=Math.max(r.min,Math.min(r.max,v));cfg[k]=v;
    for(const [a,b] of PAIRS){ if(cfg[a]>cfg[b]){ if(k===a) cfg[b]=cfg[a]; else cfg[a]=cfg[b]; } }
  }
  syncPanel();
  setWindAudio();
}

// ---------- RNG (seeded, reproducible) ----------
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;};}
let rng=mulberry32(1);
function rand(a,b){return a+(b-a)*rng();}
function rInt(a,b){return Math.floor(rand(a,b+1));}
function shuffle(arr){for(let i=arr.length-1;i>0;i--){const j=rInt(0,i);[arr[i],arr[j]]=[arr[j],arr[i]];}return arr;}

// ---------- game data ----------
const PAL=['#2a9d8f','#e9c46a','#8e7dbe','#e76f51','#4cc9f0','#90be6d','#f4a261','#577590','#bc4749','#06d6a0','#c77dff','#ffb703'];
const PL=[
  {name:'PLAYER 1',color:'RED',col:'#e8373b',dark:'#7a1216',light:'#ffc9b8',ang:45,pow:60,mode:'idle',alive:true,gx:0,roofY:0,wins:0},
  {name:'PLAYER 2',color:'BLUE',col:'#2f7bff',dark:'#123c86',light:'#bfe0ff',ang:45,pow:60,mode:'idle',alive:true,gx:0,roofY:0,wins:0}
];
const S={state:'title',time:0,bld:[],cur:0,round:0,bo:3,wind:0,seed:0,scorch:[],expl:null,proj:null,trail:[],parts:[],
  snap:null,acc:0,pending:null,wait:0,waitFn:null,shake:0,clouds:[],ovAction:null,msg:'',winner:-1};

// ---------- skyline ----------
function buildSkyline(){
  let bl=null,L=null,R=null;
  for(let tries=0;tries<40;tries++){
    const n=rInt(cfg.bldMin,cfg.bldMax);
    const ws=[],gs=[];
    for(let i=0;i<n;i++) ws.push(rand(46,104));
    for(let i=0;i<n-1;i++) gs.push(rInt(3,9));
    const sumW=ws.reduce((a,b)=>a+b,0), sumG=gs.reduce((a,b)=>a+b,0);
    const sc=(W-sumG)/sumW;
    const pal=shuffle(PAL.slice());
    bl=[];let x=0;
    for(let i=0;i<n;i++){
      const w=Math.max(30,Math.floor(ws[i]*sc)), h=rInt(cfg.hMin,cfg.hMax);
      const cols=Math.max(1,Math.floor((w-10)/12)), rows=Math.max(1,Math.floor((h-16)/14));
      const lit=[];for(let k=0;k<cols*rows;k++) lit.push(rng()<0.45);
      bl.push({x,w,h,top:GROUND-h,col:pal[i%pal.length],cols,rows,lit});
      x+=w+(i<n-1?gs[i]:0);
    }
    const half=Math.floor(n/2);
    L=bl[rInt(0,half-1)]; R=bl[rInt(half,n-1)];
    if(Math.hypot((R.x+R.w/2)-(L.x+L.w/2),R.top-L.top)>cfg.blastRadius*2.5) break;
  }
  S.bld=bl;
  PL[0].gx=L.x+L.w/2; PL[0].roofY=L.top;
  PL[1].gx=R.x+R.w/2; PL[1].roofY=R.top;
}

// ---------- wind ----------
function rollWind(first){
  if(!first && !cfg.windEachTurn) return;
  const mag=rand(cfg.windMin,cfg.windMax);
  let sign;
  if(first || cfg.windFlip) sign=rng()<0.5?-1:1;
  else sign=Math.sign(S.wind)||1;
  S.wind=sign*mag;
  setWindAudio();
}

// ---------- session / round flow ----------
function need(){return Math.floor(S.bo/2)+1;}
function resetTuningIfNeeded(){
  if(cfg.resetEachMatch){Object.assign(cfg,DEF,{resetEachMatch:true});syncPanel();}
}
function startSession(){
  S.bo=parseInt(document.getElementById('boSel').value,10)||3;
  PL.forEach(p=>p.wins=0);
  S.round=0;
  newRound();
}
function newRound(){
  if(S.round>0) resetTuningIfNeeded();
  S.round++;
  S.cur=(S.round-1)%2;
  PL.forEach(p=>{p.alive=true;p.mode='idle';});
  S.scorch=[];S.parts=[];S.expl=null;S.proj=null;S.trail=[];S.pending=null;
  S.waitFn=null;
  S.seed=(Math.random()*1e9)|0;
  rng=mulberry32(S.seed);
  buildSkyline();
  S.wind=0;rollWind(true);
  S.state='aim';S.msg='';
  hideOverlay();
}
function nextTurn(){
  S.cur^=1;
  rollWind(false);
  S.state='aim';
}
function showOverlay(title,sub,text,btn,action,showBo){
  document.getElementById('ovTitle').textContent=title;
  document.getElementById('ovSub').textContent=sub;
  document.getElementById('ovText').textContent=text;
  document.getElementById('ovBtn').textContent=btn;
  document.getElementById('ovBo').classList.toggle('hidden',!showBo);
  document.getElementById('overlay').classList.add('show');
  S.ovAction=action;
}
function hideOverlay(){document.getElementById('overlay').classList.remove('show');S.ovAction=null;}
function overlayVisible(){return document.getElementById('overlay').classList.contains('show');}

function showRoundEnd(){
  const w=PL[S.winner], o=PL[S.winner^1];
  S.state='over';
  if(w.wins>=need()){
    showOverlay(`${w.name} WINS!`,`${w.color} GORILLA TAKES THE SESSION`,
      `Final score ${PL[0].wins} - ${PL[1].wins} (best of ${S.bo}). Ready for another stampede?`,
      'NEW SESSION',startSession,true);
  } else {
    showOverlay(`${w.name} WINS THE ROUND`,`${w.color} GORILLA STANDS TALL`,
      `${o.color} gorilla has been bananaed. Score ${PL[0].wins} - ${PL[1].wins}. A fresh skyline awaits.`,
      'NEXT ROUND  [ENTER]',newRound,false);
  }
}

// ---------- firing & physics ----------
function fire(){
  const p=PL[S.cur], dir=S.cur===0?1:-1;
  const spd=cfg.velMin+(cfg.velMax-cfg.velMin)*(p.pow-1)/99;
  const a=p.ang*Math.PI/180;
  S.snap={g:cfg.gravity,w:S.wind};
  S.proj={x:p.gx+dir*14,y:p.roofY-40,vx:Math.cos(a)*spd*dir,vy:-Math.sin(a)*spd,px:0,py:0};
  S.proj.px=S.proj.x;S.proj.py=S.proj.y;
  S.trail=[];S.acc=0;S.state='flight';S.msg='BANANA AWAY!';
  sfx.launch();
}
function physStep(dt){
  const P=S.proj;
  P.px=P.x;P.py=P.y;
  P.vx+=S.snap.w*dt;
  P.vy+=S.snap.g*dt;
  P.x+=P.vx*dt;P.y+=P.vy*dt;
  S.trail.push({x:P.x,y:P.y});if(S.trail.length>26)S.trail.shift();
  for(let i=0;i<2;i++){
    const q=PL[i];if(!q.alive)continue;
    if(P.x>=q.gx-11&&P.x<=q.gx+11&&P.y>=q.roofY-40&&P.y<=q.roofY){impact(P.x,P.y,i);return;}
  }
  for(const b of S.bld){
    if(P.x>=b.x&&P.x<=b.x+b.w&&P.y>=b.top){impact(P.px,P.py,-1);return;}
  }
  if(P.y>=GROUND){impact(P.x,GROUND,-1);return;}
  if(P.x<-40||P.x>W+40||P.y>H){miss();}
}
function miss(){
  S.proj=null;S.expl=null;S.state='blast';S.msg='OUT OF RANGE';
  S.waitFn=nextTurn;S.wait=0.8;
}
function impact(x,y,direct){
  S.proj=null;S.state='blast';S.msg='KABOOM!';
  const R=cfg.blastRadius,thr=cfg.damageThreshold;
  const kill=[false,false];
  for(let i=0;i<2;i++){
    const q=PL[i];if(!q.alive)continue;
    if(direct===i){kill[i]=true;continue;}
    const d=Math.hypot(x-q.gx,y-(q.roofY-20));
    if(d<=R&&(1-d/R)*100>=thr) kill[i]=true;
  }
  S.expl={x,y,t:0,dur:1.1,R};
  S.scorch.push({x,y,r:R*0.85});
  spawnParts(x,y);
  S.shake=0.4;
  sfx.boom();
  S.pending=kill;
}
function resolveBlast(){
  const k=S.pending;S.pending=null;S.expl=null;
  if(!k[0]&&!k[1]){S.waitFn=nextTurn;S.wait=0.6;return;}
  const cur=S.cur,opp=cur^1;
  for(let i=0;i<2;i++) if(k[i]){PL[i].alive=false;PL[i].mode='dead';}
  const winner=k[cur]?opp:cur;
  const w=PL[winner];
  w.wins++;
  w.mode=k[winner]?'dead':'win';
  S.winner=winner;
  S.msg=`${w.name} WINS!`;
  sfx.victory();
  setTimeout(()=>sfx.vocal(),250);
  S.waitFn=showRoundEnd;S.wait=1.8;
}

// ---------- particles ----------
function spawnParts(x,y){
  S.parts=[];
  for(let i=0;i<26;i++){
    const a=rand(0,Math.PI*2),sp=rand(120,380);
    S.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-120,life:rand(0.5,1),max:1,c:rng()<0.5?'#ffd23f':'#ff7b2e',dust:false});
  }
  for(let i=0;i<10;i++){
    const a=rand(Math.PI,Math.PI*2),sp=rand(30,120);
    S.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:rand(0.8,1.4),max:1.4,r:rand(4,8),dust:true});
  }
}
function updateParts(dt){
  for(const p of S.parts){
    p.vy+=(p.dust?30:420)*dt;
    if(p.dust)p.vx*=0.97;
    p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt;
  }
  S.parts=S.parts.filter(p=>p.life>0);
}

// ---------- update ----------
function update(dt){
  S.time+=dt;
  S.shake=Math.max(0,S.shake-dt);
  if(S.state==='flight'){
    S.acc+=dt;
    while(S.acc>=STEP&&S.state==='flight'){S.acc-=STEP;physStep(STEP);}
  }
  if(S.state==='blast'&&S.expl){
    S.expl.t+=dt;
    if(S.expl.t>=S.expl.dur) resolveBlast();
  }
  if(S.waitFn){
    S.wait-=dt;
    if(S.wait<=0){const f=S.waitFn;S.waitFn=null;f();}
  }
  updateParts(dt);
}

// ---------- rendering ----------
function drawSky(){
  const gr=g.createLinearGradient(0,0,0,H);
  gr.addColorStop(0,'#0b1446');gr.addColorStop(0.5,'#2e4fb0');gr.addColorStop(0.85,'#f08c63');gr.addColorStop(1,'#ffc078');
  g.fillStyle=gr;g.fillRect(0,0,W,H);
}
function drawWorldBackdrop(){
  // sun
  g.fillStyle='#ffd166';g.beginPath();g.arc(660,88,34,0,Math.PI*2);g.fill();
  g.fillStyle='#f29e4c';for(let i=0;i<3;i++)g.fillRect(622,84+i*12,80,3);
  // clouds (decorative, drift)
  g.fillStyle='rgba(255,255,255,0.85)';
  for(const c of S.clouds){
    const cx=((c.x+S.time*c.sp)%(W+160))-80;
    g.fillRect(cx,c.y,60*c.s,14*c.s);
    g.fillRect(cx+10*c.s,c.y-8*c.s,30*c.s,10*c.s);
    g.fillRect(cx+38*c.s,c.y+4*c.s,22*c.s,10*c.s);
  }
}
function drawBuildings(){
  for(const b of S.bld){
    g.fillStyle=b.col;g.fillRect(b.x,b.top,b.w,b.h);
    g.fillStyle='rgba(0,0,0,0.28)';g.fillRect(b.x+b.w-6,b.top,6,b.h);
    g.fillStyle='rgba(255,255,255,0.3)';g.fillRect(b.x,b.top,b.w,4);
    for(let r=0;r<b.rows;r++)for(let c=0;c<b.cols;c++){
      const on=b.lit[r*b.cols+c];
      g.fillStyle=on?'#ffe98a':'#1d2140';
      g.fillRect(b.x+6+c*12,b.top+10+r*14,6,8);
    }
    g.strokeStyle='#0a0a1a';g.lineWidth=2;g.strokeRect(b.x,b.top,b.w,b.h);
  }
  g.fillStyle='#2b1b3d';g.fillRect(0,GROUND,W,H-GROUND);
  g.fillStyle='#8a5cc0';g.fillRect(0,GROUND,W,4);
  g.fillStyle='#3a2652';for(let x=0;x<W;x+=24)g.fillRect(x,GROUND+12,12,3);
}
function drawScorch(){
  for(const s of S.scorch){
    g.fillStyle='rgba(15,8,2,0.55)';
    g.beginPath();g.ellipse(s.x,s.y,s.r,s.r*0.55,0,0,Math.PI*2);g.fill();
  }
}
function drawGorilla(q,idx){
  const t=S.time;
  g.save();g.translate(Math.round(q.gx),Math.round(q.roofY));
  if(q.mode==='dead'){
    g.fillStyle=q.dark;g.fillRect(-14,-8,26,8);
    g.fillStyle=q.col;g.fillRect(9,-14,12,12);
    g.fillStyle=q.light;g.fillRect(-14,-14,8,6);
    g.strokeStyle='#111';g.lineWidth=2;g.beginPath();
    g.moveTo(12,-12);g.lineTo(18,-6);g.moveTo(18,-12);g.lineTo(12,-6);g.stroke();
    g.restore();return;
  }
  const up=q.mode==='win';
  const bob=up?-Math.abs(Math.sin(t*9))*10:Math.sin(t*2+idx)*0.6;
  g.fillStyle=q.dark;g.fillRect(-7,-10,5,10);g.fillRect(2,-10,5,10);
  g.fillStyle=q.col;g.fillRect(-9,-27+bob,18,18);
  g.fillStyle=q.dark;
  if(up){g.fillRect(-14,-44+bob,5,18);g.fillRect(9,-44+bob,5,18);}
  else{g.fillRect(-13,-26+bob,5,16);g.fillRect(8,-26+bob,5,16);}
  g.fillStyle=q.col;g.fillRect(-7,-40+bob,14,13);
  g.fillStyle=q.light;g.fillRect(-4,-33+bob,8,6);
  g.fillStyle='#fff';g.fillRect(-4,-37+bob,3,3);g.fillRect(1,-37+bob,3,3);
  g.fillStyle='#000';g.fillRect(-3,-36+bob,1,1);g.fillRect(2,-36+bob,1,1);
  if(up){g.fillStyle='#300';g.fillRect(-3,-30+bob,6,3);}
  g.restore();
  // name tag
  g.font='bold 10px "Courier New",monospace';g.textAlign='center';g.textBaseline='alphabetic';
  g.fillStyle=q.col;g.fillText(idx===0?'P1':'P2',q.gx,q.roofY-56);
}
function drawTurnMarker(){
  if(S.state!=='aim')return;
  const p=PL[S.cur];
  const y=p.roofY-70+Math.sin(S.time*6)*3;
  g.fillStyle=p.col;g.strokeStyle='#000';g.lineWidth=2;
  g.beginPath();g.moveTo(p.gx,y+8);g.lineTo(p.gx-7,y);g.lineTo(p.gx+7,y);g.closePath();g.fill();g.stroke();
}
function drawPreview(){
  if(S.state!=='aim')return;
  const p=PL[S.cur],dir=S.cur===0?1:-1;
  const spd=cfg.velMin+(cfg.velMax-cfg.velMin)*(p.pow-1)/99;
  const a=p.ang*Math.PI/180;
  let x=p.gx+dir*14,y=p.roofY-40,vx=Math.cos(a)*spd*dir,vy=-Math.sin(a)*spd;
  const dt=0.02;
  g.fillStyle='rgba(255,255,255,0.55)';
  for(let s=0;s<260;s++){
    vx+=S.wind*dt;vy+=cfg.gravity*dt;x+=vx*dt;y+=vy*dt;
    if(y>=GROUND||x<-20||x>W+20)break;
    let hit=false;for(const b of S.bld){if(x>=b.x&&x<=b.x+b.w&&y>=b.top){hit=true;break;}}
    if(hit)break;
    if(s%3===0){g.fillRect(x-1.5,y-1.5,3,3);}
  }
}
function drawBanana(x,y,ang){
  g.save();g.translate(x,y);g.rotate(ang);
  g.strokeStyle='#ffe135';g.lineWidth=5;g.lineCap='round';
  g.beginPath();g.arc(0,-6,9,0.15*Math.PI,0.85*Math.PI);g.stroke();
  g.strokeStyle='#8a6d00';g.lineWidth=1.5;g.beginPath();g.arc(0,-6,9,0.15*Math.PI,0.85*Math.PI);g.stroke();
  g.fillStyle='#6b4a00';g.fillRect(-9,-2,3,3);g.fillRect(7,-2,3,3);
  g.restore();
}
function drawProjectile(){
  const T=S.trail;
  for(let i=0;i<T.length;i++){
    const f=(i+1)/T.length;
    g.fillStyle=`rgba(255,243,160,${f*0.6})`;
    g.beginPath();g.arc(T[i].x,T[i].y,1+f*3,0,Math.PI*2);g.fill();
  }
  if(S.proj){
    const P=S.proj;
    drawBanana(P.x,P.y,Math.atan2(P.vy,P.vx));
  }
}
function drawExplosion(){
  const e=S.expl;if(!e)return;
  const p=Math.min(1,e.t/e.dur);
  const r=e.R*Math.min(1,0.35+p*1.4);
  g.fillStyle=`rgba(255,120,40,${0.55*(1-p)})`;
  g.beginPath();g.arc(e.x,e.y,r,0,Math.PI*2);g.fill();
  g.fillStyle=`rgba(255,235,120,${0.8*(1-p)})`;
  g.beginPath();g.arc(e.x,e.y,r*0.55,0,Math.PI*2);g.fill();
  g.strokeStyle=`rgba(255,255,255,${1-p})`;g.lineWidth=3;
  g.beginPath();g.arc(e.x,e.y,r,0,Math.PI*2);g.stroke();
}
function drawParts(){
  for(const p of S.parts){
    const a=Math.max(0,p.life/p.max);
    if(p.dust){g.fillStyle=`rgba(200,190,170,${a*0.7})`;g.beginPath();g.arc(p.x,p.y,p.r*(1+(1-a)),0,Math.PI*2);g.fill();}
    else{g.fillStyle=p.c;g.globalAlpha=a;g.fillRect(p.x,p.y,3,3);g.globalAlpha=1;}
  }
}
function drawHUD(){
  const p=PL[S.cur];
  g.fillStyle='#0a0b20';g.fillRect(0,0,W,HUDH);
  g.fillStyle='#ffd23f';g.fillRect(0,HUDH-3,W,3);
  g.textBaseline='middle';
  // Row 1: wind (left)
  g.font='bold 14px "Courier New",monospace';g.textAlign='left';
  g.fillStyle='#7ef0ff';g.fillText('WIND',10,17);
  const m=Math.abs(S.wind),dir=S.wind<0?-1:1;
  if(m<1){g.fillStyle='#aab';g.fillText('CALM',66,17);}
  else{
    g.fillStyle='#fff';g.fillText(String(Math.round(m/2)),66,17);
    const L=14+(m/Math.max(1,cfg.windMax))*44;
    const x0=108,x2=x0+dir*L;
    g.strokeStyle='#ffe066';g.fillStyle='#ffe066';g.lineWidth=3;
    g.beginPath();g.moveTo(x0,17);g.lineTo(x2,17);g.stroke();
    g.beginPath();g.moveTo(x2,17);g.lineTo(x2-dir*8,11);g.lineTo(x2-dir*8,23);g.closePath();g.fill();
  }
  // turn label (center)
  let label,lc=p.col;
  if(S.state==='aim'){label=`${p.name} TURN`;}
  else if(S.state==='flight'){label='BANANA IN FLIGHT';lc='#fff';}
  else if(S.state==='blast'){label=S.msg;lc='#ffd23f';}
  else {label='ROUND OVER';lc='#fff';}
  g.textAlign='center';g.font='bold 18px "Courier New",monospace';g.fillStyle=lc;
  g.fillText(label,W/2,17);
  // angle (right)
  g.font='bold 14px "Courier New",monospace';g.textAlign='right';g.fillStyle='#fff';
  g.fillText(`ANGLE ${p.ang}°`,W-10,17);
  // Row 2: scores (left)
  g.font='bold 12px "Courier New",monospace';g.textAlign='left';
  for(let i=0;i<2;i++){
    const q=PL[i],bx=i===0?10:170;
    g.fillStyle=q.col;g.fillText(`P${i+1}`,bx,44);
    for(let k=0;k<need();k++){
      g.fillStyle=k<q.wins?q.col:'#222';g.strokeStyle=q.col;g.lineWidth=1;
      g.fillRect(bx+26+k*13,38,10,10);
      if(k>=q.wins)g.strokeRect(bx+26+k*13+0.5,38.5,9,9);
    }
  }
  // round info (center)
  g.textAlign='center';g.font='11px "Courier New",monospace';g.fillStyle='#9aa3d6';
  g.fillText(`ROUND ${S.round} · BEST OF ${S.bo} · SEED ${S.seed}`,W/2,44);
  // power bar (right)
  g.textAlign='right';g.font='bold 12px "Courier New",monospace';g.fillStyle='#fff';
  g.fillText(`POWER ${p.pow}`,650,44);
  g.fillStyle='#222';g.fillRect(660,38,128,12);
  const f=(p.pow-1)/99;
  g.fillStyle=f>0.8?'#ff5a5a':f>0.5?'#ffd23f':'#3cff7a';
  g.fillRect(660,38,128*f,12);
  g.strokeStyle='#000';g.lineWidth=2;g.strokeRect(660,38,128,12);
}
function render(){
  g.setTransform(1,0,0,1,0,0);
  drawSky();
  const sx=S.shake>0?(Math.random()-0.5)*S.shake*24:0;
  const sy=S.shake>0?(Math.random()-0.5)*S.shake*24:0;
  g.save();g.translate(sx,HUDH+sy);
  drawWorldBackdrop();
  drawBuildings();
  drawScorch();
  PL.forEach((q,i)=>drawGorilla(q,i));
  drawTurnMarker();
  drawPreview();
  drawProjectile();
  drawExplosion();
  drawParts();
  g.restore();
  drawHUD();
}

// ---------- audio ----------
let AC=null,master=null,wGain=null,muted=false;
function audioInit(){
  if(AC){if(AC.state==='suspended')AC.resume();return;}
  try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){return;}
  master=AC.createGain();master.gain.value=0.35;master.connect(AC.destination);
  const len=AC.sampleRate*2,buf=AC.createBuffer(1,len,AC.sampleRate),d=buf.getChannelData(0);
  let last=0;for(let i=0;i<len;i++){last=(last+0.02*(Math.random()*2-1))/1.02;d[i]=last*2.5;}
  const src=AC.createBufferSource();src.buffer=buf;src.loop=true;
  const lp=AC.createBiquadFilter();lp.type='lowpass';lp.frequency.value=700;
  wGain=AC.createGain();wGain.gain.value=0.02;
  src.connect(lp);lp.connect(wGain);wGain.connect(master);src.start();
  setWindAudio();
}
function setWindAudio(){
  if(!AC||!wGain)return;
  const f=Math.min(1,Math.abs(S.wind)/Math.max(1,cfg.windMax));
  wGain.gain.setTargetAtTime(0.02+f*0.10,AC.currentTime,0.3);
}
function tone(f,dur,type,vol,to,delay){
  if(!AC||muted)return;
  delay=delay||0;
  const t0=AC.currentTime+delay;
  const o=AC.createOscillator(),gn=AC.createGain();
  o.type=type;o.frequency.setValueAtTime(f,t0);
  if(to)o.frequency.exponentialRampToValueAtTime(to,t0+dur);
  gn.gain.setValueAtTime(vol,t0);gn.gain.exponentialRampToValueAtTime(0.0008,t0+dur);
  o.connect(gn);gn.connect(master);o.start(t0);o.stop(t0+dur+0.02);
}
function boomSfx(){
  if(!AC||muted)return;
  const len=Math.floor(AC.sampleRate*0.8),buf=AC.createBuffer(1,len,AC.sampleRate),d=buf.getChannelData(0);
  for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/len,2);
  const s=AC.createBufferSource();s.buffer=buf;
  const f=AC.createBiquadFilter();f.type='lowpass';
  f.frequency.setValueAtTime(1400,AC.currentTime);f.frequency.exponentialRampToValueAtTime(120,AC.currentTime+0.8);
  const gn=AC.createGain();gn.gain.value=0.9;
  s.connect(f);f.connect(gn);gn.connect(master);s.start();
}
const sfx={
  launch:()=>tone(900,0.22,'square',0.12,180),
  boom:boomSfx,
  victory:()=>{[523,659,784,1047,784,1047].forEach((f,i)=>tone(f,0.22,'square',0.12,0,i*0.14));},
  vocal:()=>{tone(260,0.5,'sawtooth',0.18,640);tone(640,0.3,'triangle',0.1,300,0.5);}
};
function toggleMute(){
  muted=!muted;
  if(master)master.gain.value=muted?0:0.35;
}

// ---------- panel ----------
function buildPanel(){
  const pb=document.getElementById('pbody');pb.innerHTML='';
  for(const r of SCHEMA){
    if(r.grp){const d=document.createElement('div');d.className='grp';d.textContent=r.grp;pb.appendChild(d);continue;}
    const d=document.createElement('div');d.className='row';d.id='row-'+r.k;
    if(r.t==='bool'){
      d.innerHTML=`<div class="lab">${r.l}<span class="tag">${r.apply}</span></div><div class="ctl"><input type="checkbox" id="in-${r.k}"></div>`;
      d.querySelector('input').addEventListener('change',e=>{setCfg(r.k,e.target.checked);});
    } else {
      d.innerHTML=`<div class="lab">${r.l}<span class="tag">${r.apply}</span></div><div class="ctl"><button data-s="-1">−</button><input type="number" id="in-${r.k}" min="${r.min}" max="${r.max}" step="${r.step}"><button data-s="1">+</button></div>`;
      const inp=d.querySelector('input');
      inp.addEventListener('change',()=>{const v=parseFloat(inp.value);if(!isNaN(v))setCfg(r.k,v);else syncPanel();});
      d.querySelectorAll('button').forEach(b=>b.addEventListener('click',()=>{
        setCfg(r.k,cfg[r.k]+r.step*Number(b.dataset.s));b.blur();
      }));
    }
    pb.appendChild(d);
  }
}
function syncPanel(){
  let n=0;
  for(const r of SCHEMA){
    if(!r.k)continue;
    const row=document.getElementById('row-'+r.k),inp=document.getElementById('in-'+r.k);
    const changed=cfg[r.k]!==DEF[r.k];
    if(changed)n++;
    row.classList.toggle('changed',changed);
    if(r.t==='bool')inp.checked=cfg[r.k];
    else if(document.activeElement!==inp)inp.value=cfg[r.k];
  }
  const b=document.getElementById('badge');
  if(n>0){b.className='live';b.textContent=`● LIVE TUNING ACTIVE · ${n} edited`;}
  else{b.className='def';b.textContent='DEFAULT VALUES · CLASSIC';}
}
function togglePanel(){
  document.getElementById('panel').classList.toggle('hidden');
  syncPanel();
}

// ---------- input ----------
document.addEventListener('keydown',e=>{
  const tg=e.target.tagName;
  if(tg==='INPUT'||tg==='SELECT'||tg==='TEXTAREA'){if(e.key==='Escape')e.target.blur();return;}
  if(tg==='BUTTON'&&(e.key===' '||e.key==='Enter'))return;
  audioInit();
  if(e.key==='t'||e.key==='T'){togglePanel();return;}
  if(e.key==='m'||e.key==='M'){toggleMute();return;}
  if(overlayVisible()){
    if(['Enter',' ','r','R','n','N'].includes(e.key)){e.preventDefault();if(S.ovAction)S.ovAction();}
    return;
  }
  if(S.state!=='aim')return;
  const p=PL[S.cur];
  switch(e.key){
    case 'ArrowLeft':p.ang=Math.max(0,p.ang-1);e.preventDefault();break;
    case 'ArrowRight':p.ang=Math.min(90,p.ang+1);e.preventDefault();break;
    case 'ArrowUp':p.pow=Math.min(100,p.pow+1);e.preventDefault();break;
    case 'ArrowDown':p.pow=Math.max(1,p.pow-1);e.preventDefault();break;
    case ' ':case 'Enter':e.preventDefault();if(!e.repeat)fire();break;
  }
});
document.getElementById('ovBtn').addEventListener('click',()=>{audioInit();if(S.ovAction)S.ovAction();});
document.getElementById('tuneBtn').addEventListener('click',()=>{togglePanel();});
document.getElementById('resetBtn').addEventListener('click',()=>{Object.assign(cfg,DEF);syncPanel();setWindAudio();});

// ---------- init ----------
S.clouds=[];
for(let i=0;i<5;i++)S.clouds.push({x:Math.random()*W,y:30+Math.random()*120,s:0.6+Math.random()*0.8,sp:6+Math.random()*10});
buildSkyline();
buildPanel();syncPanel();
showOverlay('GORILLAZ','Banana Bomb Artillery · Two Gorillas Enter','Player 1 (RED) and Player 2 (BLUE) take turns hurling exploding bananas. Last gorilla standing wins the round. ←→ angle, ↑↓ power, SPACE to throw.','START',startSession,true);
S.state='title';
let last=performance.now();
function loop(now){
  const dt=Math.min(0.05,(now-last)/1000);last=now;
  update(dt);render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-haiku-5.5","provider":"openrouter","persona":"full","sessionId":"753eb47f-1547-4dca-af93-601dca03ccf6","tokensIn":60916,"tokensOut":66182,"tokensTotal":127098,"cost":0.036846475000000004,"turns":3,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-10-08T03:34:10.233Z"} -->