Gorillaz
Qwen 3.8 · full

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"} -->