Gorillaz
GLM 5.3 Flash · 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 24,630 bytes · SHA-256 996d28b67881
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Gorillaz — Banana Bomb Artillery</title>
<style>
:root{ --p1:#ff4b4b; --p2:#4b9dff; }
*{box-sizing:border-box; margin:0; padding:0;}
html,body{height:100%; background:#0b0b12; overflow:hidden;
font-family:"Courier New", monospace; color:#fff; user-select:none;}
#wrap{position:fixed; inset:0; display:flex; align-items:center; justify-content:center;}
#stage{position:relative; width:1280px; height:720px; transform-origin:center center;}
canvas{display:block; image-rendering:pixelated; background:#000;}
/* ---------- HUD ---------- */
#hud{position:absolute; inset:0; pointer-events:none;}
.panel{background:rgba(10,10,20,.82); border:3px solid #444; box-shadow:0 0 0 3px #000, 4px 4px 0 rgba(0,0,0,.5);}
#topbar{position:absolute; top:10px; left:50%; transform:translateX(-50%); padding:6px 18px;
display:flex; gap:22px; align-items:center; font-size:18px; letter-spacing:1px;}
#windArrow{display:inline-block; width:0; height:0; vertical-align:middle; margin:0 6px;
border-top:8px solid transparent; border-bottom:8px solid transparent;}
#turnbar{position:absolute; top:10px; left:14px; padding:6px 14px; font-size:20px; font-weight:bold;}
#scorebar{position:absolute; top:10px; right:14px; padding:6px 14px; font-size:16px;}
#aimbar{position:absolute; bottom:12px; left:50%; transform:translateX(-50%); padding:8px 20px;
display:flex; gap:26px; align-items:center; font-size:18px;}
#aimbar b{font-size:22px;}
.val{color:#ffe14b; min-width:52px; display:inline-block; text-align:center;}
#msg{position:absolute; top:44%; left:50%; transform:translate(-50%,-50%); text-align:center;
padding:22px 40px; font-size:30px; display:none; pointer-events:auto;}
#msg button{margin-top:16px; font-family:inherit; font-size:20px; padding:8px 22px; cursor:pointer;
background:#ffe14b; border:3px solid #000; box-shadow:3px 3px 0 #000;}
#msg button:hover{background:#fff;}
/* ---------- Settings ---------- */
#gearBtn{position:absolute; bottom:12px; right:12px; pointer-events:auto; cursor:pointer;
padding:8px 14px; font-size:16px; background:rgba(10,10,20,.82); border:3px solid #444; color:#fff;}
#settings{position:absolute; top:60px; right:14px; width:360px; max-height:600px; overflow-y:auto;
padding:14px; display:none; pointer-events:auto; font-size:13px;}
#settings h3{font-size:17px; margin-bottom:4px; color:#ffe14b;}
#liveBadge{display:inline-block; margin-left:8px; padding:2px 8px; font-size:11px; background:#2ecc40;
color:#000; border:2px solid #000; display:none;}
.cfg{margin:9px 0; border-bottom:1px solid #333; padding-bottom:8px;}
.cfg label{display:block; font-weight:bold; margin-bottom:3px;}
.cfg .row{display:flex; align-items:center; gap:6px;}
.cfg input[type=number]{width:74px; background:#111; color:#ffe14b; border:2px solid #555;
font-family:inherit; padding:3px 4px; font-size:13px;}
.cfg input[type=checkbox]{transform:scale(1.2);}
.cfg button{font-family:inherit; cursor:pointer; background:#333; color:#fff; border:2px solid #666;
width:26px; height:24px; font-weight:bold;}
.cfg .num{color:#ffe14b; min-width:70px; text-align:center; display:inline-block;}
.cfg .hint{color:#8af; font-size:11px; margin-top:2px;}
.changed{outline:2px solid #2ecc40;}
#resetCfg{margin-top:10px; width:100%; font-family:inherit; font-size:14px; padding:6px; cursor:pointer;
background:#ffe14b; border:3px solid #000; font-weight:bold;}
#help{position:absolute; bottom:12px; left:12px; font-size:12px; color:#9aa; line-height:1.5;}
</style>
</head>
<body>
<div id="wrap"><div id="stage">
<canvas id="cv" width="1280" height="720"></canvas>
<div id="hud">
<div id="turnbar" class="panel">PLAYER 1 TURN</div>
<div id="topbar" class="panel">
<span>WIND <span id="windArrow"></span><span id="windVal">0.0</span></span>
</div>
<div id="scorebar" class="panel">P1 <span id="s1" style="color:var(--p1)">0</span> — <span id="s2" style="color:var(--p2)">0</span> P2</div>
<div id="aimbar" class="panel">
<span>ANGLE <b class="val" id="angVal">45°</b></span>
<span>POWER <b class="val" id="powVal">50</b></span>
<span style="color:#7f7">◀ ▶ angle ▲ ▼ power SPACE fire</span>
</div>
<div id="help">Q/E also adjust angle · A/Z adjust power · S opens settings</div>
<div id="msg" class="panel"><div id="msgText"></div><button id="rematchBtn">REMATCH</button></div>
<div id="gearBtn">⚙ SETTINGS [S]</div>
<div id="settings" class="panel">
<h3>LIVE TUNING <span id="liveBadge">LIVE TUNING ACTIVE</span></h3>
<div class="cfg"><label>Blast Radius (px)</label>
<div class="row"><button data-k="blastRadius" data-d="-5">−</button><span class="num" id="v_blastRadius"></span><button data-k="blastRadius" data-d="5">+</button>
<input type="number" data-k="blastRadius" step="1"></div></div>
<div class="cfg"><label>Gravity (px/s²)</label>
<div class="row"><button data-k="gravity" data-d="-20">−</button><span class="num" id="v_gravity"></span><button data-k="gravity" data-d="20">+</button>
<input type="number" data-k="gravity" step="10"></div></div>
<div class="cfg"><label>Wind Min Speed</label>
<div class="row"><button data-k="windMin" data-d="-5">−</button><span class="num" id="v_windMin"></span><button data-k="windMin" data-d="5">+</button>
<input type="number" data-k="windMin" step="1"></div></div>
<div class="cfg"><label>Wind Max Speed</label>
<div class="row"><button data-k="windMax" data-d="-5">−</button><span class="num" id="v_windMax"></span><button data-k="windMax" data-d="5">+</button>
<input type="number" data-k="windMax" step="1"></div></div>
<div class="cfg"><label>Wind Shifts Between Turns</label>
<input type="checkbox" data-k="windShift"></div>
<div class="cfg"><label>Building Count</label>
<div class="row"><button data-k="buildingCount" data-d="-1">−</button><span class="num" id="v_buildingCount"></span><button data-k="buildingCount" data-d="1">+</button>
<input type="number" data-k="buildingCount" step="1"></div>
<div class="hint">Applies on next skyline generation</div></div>
<div class="cfg"><label>Building Min Height (px)</label>
<div class="row"><button data-k="bldMinH" data-d="-10">−</button><span class="num" id="v_bldMinH"></span><button data-k="bldMinH" data-d="10">+</button>
<input type="number" data-k="bldMinH" step="5"></div></div>
<div class="cfg"><label>Building Max Height (px)</label>
<div class="row"><button data-k="bldMaxH" data-d="-10">−</button><span class="num" id="v_bldMaxH"></span><button data-k="bldMaxH" data-d="10">+</button>
<input type="number" data-k="bldMaxH" step="5"></div></div>
<div class="cfg"><label>Power Min (throw scale)</label>
<div class="row"><button data-k="powMin" data-d="-5">−</button><span class="num" id="v_powMin"></span><button data-k="powMin" data-d="5">+</button>
<input type="number" data-k="powMin" step="1"></div></div>
<div class="cfg"><label>Power Max (throw scale)</label>
<div class="row"><button data-k="powMax" data-d="-5">−</button><span class="num" id="v_powMax"></span><button data-k="powMax" data-d="5">+</button>
<input type="number" data-k="powMax" step="1"></div></div>
<div class="cfg"><label>Splash Damage Mode</label>
<select id="splashMode" data-k="splashMode" style="font-family:inherit;background:#111;color:#ffe14b;border:2px solid #555;padding:3px;">
<option value="inside">Inside radius only</option>
<option value="falloff">Falloff (threshold %)</option>
</select></div>
<div class="cfg"><label>Damage Threshold % (falloff mode)</label>
<div class="row"><button data-k="dmgThreshold" data-d="-5">−</button><span class="num" id="v_dmgThreshold"></span><button data-k="dmgThreshold" data-d="5">+</button>
<input type="number" data-k="dmgThreshold" step="5"></div>
<div class="hint">% of blast radius within which splash kills</div></div>
<button id="resetCfg">RESET TO CLASSIC DEFAULTS</button>
</div>
</div>
</div></div>
<script>
"use strict";
/* ================= GORILLAZ — banana bomb artillery ================= */
const W=1280,H=720,GROUND=H-40;
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
/* ---- Live configuration (defaults = classic reference) ---- */
const DEFAULTS={
blastRadius:60, gravity:320, windMin:0, windMax:40, windShift:true,
buildingCount:10, bldMinH:120, bldMaxH:340,
powMin:1, powMax:100, splashMode:'inside', dmgThreshold:100
};
let cfg=Object.assign({},DEFAULTS);
let cfgDirty=false; // any edit this session -> show LIVE badge
/* ---- Game state ---- */
let buildings=[],gorillas=[],banana=null,particles=[],scorches=[],clouds=[];
let turn=0, phase='aim', score=[0,0], angle=45, power=50, wind=0;
let winner=null, msgTimer=0, shake=0, sunT=0;
const $=id=>document.getElementById(id);
function syncUI(){
$('angVal').textContent=Math.round(angle)+'°';
$('powVal').textContent=Math.round(power);
$('windVal').textContent=Math.abs(wind).toFixed(1);
const a=$('windArrow');
if(wind>0.01){a.style.borderLeft='14px solid #ffe14b';a.style.borderRight='none';}
else if(wind<-0.01){a.style.borderRight='14px solid #ffe14b';a.style.borderLeft='none';}
else {a.style.borderLeft='7px solid #888';a.style.borderRight='7px solid #888';}
$('turnbar').textContent='PLAYER '+(turn+1)+' TURN';
$('turnbar').style.color=turn===0?'var(--p1)':'var(--p2)';
$('s1').textContent=score[0]; $('s2').textContent=score[1];
}
function syncCfgUI(){
for(const k in DEFAULTS){
const el=document.querySelector('[data-k="'+k+'"]');
if(!el)continue;
if(el.type==='checkbox')el.checked=cfg[k];
else if(el.tagName==='SELECT')el.value=cfg[k];
else el.value=cfg[k];
const num=$('v_'+k); if(num)num.textContent=cfg[k];
}
}
function markLive(){ if(cfgDirty){$('liveBadge').style.display='inline-block';} }
/* ---- Settings panel wiring ---- */
document.querySelectorAll('.cfg button').forEach(b=>{
b.addEventListener('click',()=>{
const k=b.dataset.k,d=+b.dataset.d;
cfg[k]=clamp((+cfg[k])+d, k==='buildingCount'?4:0, k==='buildingCount'?20:2000);
if(k==='windMin'&&cfg.windMin>cfg.windMax)cfg.windMin=cfg.windMax;
if(k==='windMax'&&cfg.windMax<cfg.windMin)cfg.windMax=cfg.windMin;
if(k==='bldMinH'&&cfg.bldMinH>cfg.bldMaxH)cfg.bldMinH=cfg.bldMaxH;
if(k==='bldMaxH'&&cfg.bldMaxH<cfg.bldMinH)cfg.bldMaxH=cfg.bldMinH;
if(k==='powMin'&&cfg.powMin>cfg.powMax)cfg.powMin=cfg.powMax;
if(k==='powMax'&&cfg.powMax<cfg.powMin)cfg.powMax=cfg.powMin;
cfgDirty=true; syncCfgUI(); markLive();
});
});
document.querySelectorAll('.cfg input[type=number]').forEach(inp=>{
inp.addEventListener('change',()=>{
const k=inp.dataset.k; let v=+inp.value; if(isNaN(v))return;
cfg[k]=clamp(v,0,5000); cfgDirty=true; syncCfgUI(); markLive(); clampAim();
});
});
document.querySelectorAll('.cfg input[type=checkbox], .cfg select').forEach(el=>{
el.addEventListener('change',()=>{ cfg[el.dataset.k]= el.type==='checkbox'?el.checked:el.value;
cfgDirty=true; syncCfgUI(); markLive(); });
});
$('resetCfg').addEventListener('click',()=>{ cfg=Object.assign({},DEFAULTS); syncCfgUI(); });
function toggleSettings(){ const s=$('settings'); s.style.display=s.style.display==='block'?'none':'block'; }
$('gearBtn').addEventListener('click',toggleSettings);
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
function clampAim(){ angle=clamp(angle,0,180); power=clamp(power,cfg.powMin,cfg.powMax); }
/* ---- Audio (WebAudio, retro blips) ---- */
let AC=null;
function ac(){ if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)(); return AC; }
function beep(freq,dur,type,vol,slide){
try{ const a=ac(),o=a.createOscillator(),g=a.createGain();
o.type=type||'square'; o.frequency.value=freq;
if(slide)o.frequency.exponentialRampToValueAtTime(slide,a.currentTime+dur);
g.gain.value=vol||0.08; g.gain.exponentialRampToValueAtTime(0.001,a.currentTime+dur);
o.connect(g).connect(a.destination); o.start(); o.stop(a.currentTime+dur);
}catch(e){}
}
function sndLaunch(){ beep(220,0.25,'square',0.09,660); }
function sndBoom(){ beep(90,0.5,'sawtooth',0.16,30); beep(50,0.6,'triangle',0.14,20); }
function sndWin(){ [523,659,784,1047].forEach((f,i)=>setTimeout(()=>beep(f,0.18,'square',0.1),i*140)); }
function sndHoot(){ beep(160,0.15,'sawtooth',0.1,110); setTimeout(()=>beep(140,0.2,'sawtooth',0.1,90),160); }
/* ---- World generation ---- */
function mulberry(seed){return function(){seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.imul(seed^seed>>>15,1|seed);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
let seed=(Math.random()*1e9)|0;
function genCity(){
const rnd=mulberry(seed);
buildings=[];
const n=clamp(Math.round(cfg.buildingCount),4,20);
const gap=8+rnd()*14;
const totalW=W-40;
// choose widths summing to available space
let widths=[],sum=0;
for(let i=0;i<n;i++){const w=70+rnd()*90; widths.push(w); sum+=w;}
sum+=(n-1)*gap;
const scale=totalW/sum;
let x=20+(totalW-sum*scale)/2;
const palette=['#7a4a2b','#8a5a3a','#5a6b8a','#6b4a6b','#4a7a5a','#8a6a4a','#5a5a7a','#7a6a3a'];
for(let i=0;i<n;i++){
const w=widths[i]*scale, h=cfg.bldMinH+rnd()*(cfg.bldMaxH-cfg.bldMinH);
buildings.push({x,w,h:Math.max(60,h),color:palette[i%palette.length],winSeed:rnd()*1000});
x+=w+gap;
}
// gorilla placement: left half building, right half building, min separation
const left=buildings.filter(b=>b.x+b.w/2<W*0.42);
const right=buildings.filter(b=>b.x+b.w/2>W*0.58);
const bl=left[(rnd()*left.length)|0]||buildings[1]||buildings[0];
const br=right[(rnd()*right.length)|0]||buildings[buildings.length-2]||buildings[buildings.length-1];
gorillas=[
{x:bl.x+bl.w/2, y:GROUND-bl.h, color:'#ff4b4b', alive:true, face:1, react:0},
{x:br.x+br.w/2, y:GROUND-br.h, color:'#4b9dff', alive:true, face:-1, react:0}
];
clouds=[]; for(let i=0;i<6;i++)clouds.push({x:rnd()*W,y:40+rnd()*180,w:80+rnd()*120,s:0.15+rnd()*0.3});
scorches=[];
wind=(cfg.windMin+rnd()*(Math.max(0,cfg.windMax-cfg.windMin)))*(rnd()<0.5?-1:1);
turn=(rnd()<0.5)?0:1;
banana=null; particles=[]; phase='aim'; winner=null;
angle=45; power=50;
syncUI();
}
/* ---- Collision helpers ---- */
function pointInBuilding(px,py){
for(const b of buildings){
if(px>=b.x&&px<=b.x+b.w&&py>=GROUND-b.h&&py<=GROUND)return b;
} return null;
}
function hitGorilla(px,py){
for(let i=0;i<2;i++){ const g=gorillas[i];
if(g.alive&&Math.abs(px-g.x)<26&&py>g.y-58&&py<g.y+6)return i;
} return -1;
}
/* ---- Turn / firing ---- */
function fire(){
if(phase!=='aim')return;
phase='fly';
const g=gorillas[turn];
const a=angle*Math.PI/180;
const dir=turn===0?1:-1;
const v=power*7.2; // velocity scale: power 1..100 -> px/s
banana={x:g.x+dir*30,y:g.y-58,vx:Math.cos(a)*v*dir,vy:-Math.sin(a)*v,trail:[]};
sndLaunch();
}
function explode(x,y){
phase='boom'; sndBoom(); shake=12;
const r=cfg.blastRadius;
for(let i=0;i<40;i++){
const a=Math.random()*Math.PI*2,s=40+Math.random()*180;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,life:0.5+Math.random()*0.6,
c:['#ffe14b','#ff8c1a','#ff4b1a','#888'][ (Math.random()*4)|0 ]});
}
scorches.push({x,y,r:r*0.8});
// kill check
let killed=-1;
for(let i=0;i<2;i++){ const g=gorillas[i]; if(!g.alive)continue;
const d=Math.hypot(g.x-x,(g.y-28)-y);
let kill=false;
if(cfg.splashMode==='falloff'){
const thr=r*(cfg.dmgThreshold/100);
kill = d<=thr;
} else kill = d<=r;
if(kill)killed=i;
}
setTimeout(()=>{
if(killed>=0){ gorillas[killed].alive=false; gorillas[killed].react=1;
winner=1-turn; score[1-turn]++; endRound(); }
else { turn=1-turn; shiftWind(); phase='aim'; syncUI(); }
},900);
}
function shiftWind(){
if(!cfg.windShift)return;
const span=Math.max(0,cfg.windMax-cfg.windMin);
wind=(cfg.windMin+Math.random()*span)*(Math.random()<0.5?-1:1);
syncUI();
}
function endRound(){
phase='over'; sndWin(); setTimeout(sndHoot,700);
const g=gorillas[winner]; g.react=2;
$('msgText').innerHTML='<span style="color:'+(winner===0?'var(--p1)':'var(--p2)')+'">PLAYER '+(winner+1)+' WINS!</span><br><span style="font-size:16px;color:#aaa">Gorilla hoots in victory 🦍🍌</span>';
$('msg').style.display='block';
}
$('rematchBtn').addEventListener('click',()=>{
$('msg').style.display='none'; seed=(Math.random()*1e9)|0; genCity();
});
/* ---- Input ---- */
const held={};
addEventListener('keydown',e=>{
if(e.repeat)return held[e.code]=true;
held[e.code]=true;
if(phase==='aim'){
const st=5;
if(e.code==='ArrowLeft'||e.code==='KeyQ'){angle=clamp(angle-st,0,180);syncUI();}
if(e.code==='ArrowRight'||e.code==='KeyE'){angle=clamp(angle+st,0,180);syncUI();}
if(e.code==='ArrowUp'){power=clamp(power+1,cfg.powMin,cfg.powMax);syncUI();}
if(e.code==='ArrowDown'){power=clamp(power-1,cfg.powMin,cfg.powMax);syncUI();}
if(e.code==='KeyA'){power=clamp(power+5,cfg.powMin,cfg.powMax);syncUI();}
if(e.code==='KeyZ'){power=clamp(power-5,cfg.powMin,cfg.powMax);syncUI();}
if(e.code==='Space'){e.preventDefault();fire();}
}
if(e.code==='KeyS')toggleSettings();
});
addEventListener('keyup',e=>held[e.code]=false);
// mouse aim: drag on canvas sets angle/power
let dragging=false;
cv.addEventListener('mousedown',()=>{ if(phase==='aim')dragging=true; });
addEventListener('mouseup',()=>dragging=false);
cv.addEventListener('mousemove',e=>{
if(!dragging||phase!=='aim')return;
const r=cv.getBoundingClientRect();
const mx=(e.clientX-r.left)*(W/r.width), my=(e.clientY-r.top)*(H/r.height);
const g=gorillas[turn];
const dx=mx-(g.x+(turn===0?30:-30)), dy=(g.y-58)-my;
angle=clamp(Math.round(Math.atan2(dy,dx*(turn===0?1:-1))*180/Math.PI),0,180);
power=clamp(Math.round(Math.hypot(dx,dy)/7.2),cfg.powMin,cfg.powMax);
syncUI();
});
cv.addEventListener('click',()=>{ if(phase==='aim'&&!draggingJustFired())fire(); });
let downT=0; cv.addEventListener('mousedown',()=>downT=performance.now());
function draggingJustFired(){ return performance.now()-downT>200; }
/* ---- Update ---- */
let last=performance.now();
function update(dt){
sunT+=dt;
clouds.forEach(c=>{ c.x+=c.s*dt*(1+Math.abs(wind)/60); if(c.x>W+140)c.x=-140; if(c.x<-160)c.x=W+140; });
if(shake>0)shake=Math.max(0,shake-dt*30);
gorillas.forEach(g=>{ if(g.react>0&&g.react<1)g.react+=dt; });
// held-key repeat for aim
if(phase==='aim'){
if(held.ArrowLeft||held.KeyQ)angle=clamp(angle-90*dt,0,180),syncUI();
if(held.ArrowRight||held.KeyE)angle=clamp(angle+90*dt,0,180),syncUI();
}
// particles
particles.forEach(p=>{p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=300*dt;p.life-=dt;});
particles=particles.filter(p=>p.life>0);
// banana flight
if(phase==='fly'&&banana){
const steps=4, sdt=dt/steps;
for(let s=0;s<steps;s++){
const b=banana;
b.vy+=cfg.gravity*sdt;
b.vx+=wind*sdt; // wind: horizontal acceleration
b.x+=b.vx*sdt; b.y+=b.vy*sdt;
b.trail.push({x:b.x,y:b.y}); if(b.trail.length>40)b.trail.shift();
if(b.x<-50||b.x>W+50||b.y>H+50){ banana=null; turn=1-turn; shiftWind(); phase='aim'; syncUI(); break; }
const gi=hitGorilla(b.x,b.y);
if(gi>=0&&gi!==turn){ explode(b.x,b.y); break; }
if(b.y>=GROUND||pointInBuilding(b.x,b.y)){ explode(b.x,b.y); break; }
}
}
}
/* ---- Drawing ---- */
function drawGorilla(g){
const x=g.x,y=g.y,c=g.color;
ctx.save(); ctx.translate(x,y);
if(g.face<0)ctx.scale(-1,1);
if(g.react>0&&g.react<1){ ctx.translate(0,-Math.sin(g.react*Math.PI)*10); }
if(!g.alive){ ctx.rotate(1.4); ctx.translate(10,10); }
// body
ctx.fillStyle=c;
ctx.fillRect(-20,-38,40,38); // torso
ctx.fillRect(-26,-58,16,22); // left arm
ctx.fillRect(10,-58,16,22); // right arm
ctx.fillRect(-16,0,12,8); ctx.fillRect(4,0,12,8); // legs
// head
ctx.fillStyle=c; ctx.fillRect(-16,-58,32,24);
// face plate
ctx.fillStyle='#e8c39e'; ctx.fillRect(-10,-52,20,14);
ctx.fillStyle='#000';
ctx.fillRect(-7,-49,4,4); ctx.fillRect(3,-49,4,4); // eyes
ctx.fillRect(-4,-44,8,3); // nose/mouth
// chest
ctx.fillStyle='#e8c39e'; ctx.fillRect(-8,-30,16,14);
ctx.restore();
// player marker
ctx.fillStyle=c; ctx.font='bold 12px Courier New'; ctx.textAlign='center';
ctx.fillText('P'+(gorillas.indexOf(g)+1),x,y-70);
}
function drawBananaSprite(x,y,rot){
ctx.save(); ctx.translate(x,y); ctx.rotate(rot);
ctx.fillStyle='#ffe14b';
ctx.beginPath(); ctx.arc(0,0,6,0.3,Math.PI-0.3); ctx.lineTo(0,2); ctx.fill();
ctx.strokeStyle='#b89a20'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.arc(0,0,6,0.3,Math.PI-0.3); ctx.stroke();
ctx.restore();
}
function draw(){
ctx.save();
if(shake>0)ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
// sky
const sky=ctx.createLinearGradient(0,0,0,GROUND);
sky.addColorStop(0,'#1a1a3e'); sky.addColorStop(0.55,'#4a3a6e'); sky.addColorStop(1,'#c96a4a');
ctx.fillStyle=sky; ctx.fillRect(-20,-20,W+40,H+40);
// sun
ctx.fillStyle='#ffd27a'; ctx.beginPath(); ctx.arc(W-160,110,42,0,7); ctx.fill();
ctx.fillStyle='#ffefb0'; ctx.beginPath(); ctx.arc(W-160,110,30,0,7); ctx.fill();
// stars
ctx.fillStyle='rgba(255,255,255,.6)';
for(let i=0;i<40;i++){ const sx=(i*137)%W, sy=(i*89)%220; ctx.fillRect(sx,sy,2,2); }
// clouds
ctx.fillStyle='rgba(255,255,255,.25)';
clouds.forEach(c=>{ ctx.beginPath(); ctx.ellipse(c.x,c.y,c.w,18,0,0,7); ctx.fill();
ctx.beginPath(); ctx.ellipse(c.x+c.w*0.4,c.y-10,c.w*0.5,14,0,0,7); ctx.fill(); });
// ground
ctx.fillStyle='#2a5a2a'; ctx.fillRect(-20,GROUND,W+40,H-GROUND+20);
ctx.fillStyle='#1d401d'; ctx.fillRect(-20,GROUND,W+40,6);
// scorch marks
scorches.forEach(s=>{ ctx.fillStyle='rgba(20,15,10,.55)';
ctx.beginPath(); ctx.arc(s.x,Math.min(s.y,GROUND),s.r*0.5,0,7); ctx.fill(); });
// buildings
buildings.forEach(b=>{
const top=GROUND-b.h;
ctx.fillStyle=b.color; ctx.fillRect(b.x,top,b.w,b.h);
ctx.fillStyle='rgba(0,0,0,.25)'; ctx.fillRect(b.x,top,b.w,6);
// windows
ctx.fillStyle='rgba(255,235,150,.85)';
for(let wy=top+16;wy<GROUND-14;wy+=26)
for(let wx=b.x+8;wx<b.x+b.w-12;wx+=22){
const lit=((wx*7+wy*13+b.winSeed)|0)%5!==0;
if(lit)ctx.fillRect(wx,wy,10,12);
}
});
// gorillas
gorillas.forEach(drawGorilla);
// aim guide
if(phase==='aim'){
const g=gorillas[turn], a=angle*Math.PI/180, dir=turn===0?1:-1;
const v=power*7.2;
ctx.strokeStyle='rgba(255,255,255,.35)'; ctx.setLineDash([4,6]); ctx.beginPath();
let px=g.x+dir*30, py=g.y-58, vx=Math.cos(a)*v*dir, vy=-Math.sin(a)*v;
for(let i=0;i<40;i++){ vx+=wind*0.03; vy+=cfg.gravity*0.03; px+=vx*0.03; py+=vy*0.03;
if(py>GROUND)break; i===0?ctx.moveTo(px,py):ctx.lineTo(px,py); }
ctx.stroke(); ctx.setLineDash([]);
}
// banana trail + sprite
if(banana){
banana.trail.forEach((t,i)=>{ const al=i/banana.trail.length;
ctx.fillStyle='rgba(255,225,75,'+(al*0.5)+')';
ctx.fillRect(t.x-1.5,t.y-1.5,3,3); });
const rot=Math.atan2(banana.vy,banana.vx);
drawBananaSprite(banana.x,banana.y,rot);
}
// explosion flash
if(phase==='boom'&&particles.length>30){
ctx.fillStyle='rgba(255,220,120,.5)';
ctx.beginPath(); ctx.arc(scorches[scorches.length-1].x,scorches[scorches.length-1].y,cfg.blastRadius*0.9,0,7); ctx.fill();
}
// particles
particles.forEach(p=>{ ctx.globalAlpha=Math.min(1,p.life*2); ctx.fillStyle=p.c;
ctx.fillRect(p.x-2,p.y-2,4,4); ctx.globalAlpha=1; });
ctx.restore();
}
function loop(t){
const dt=Math.min(0.033,(t-last)/1000); last=t;
update(dt); draw(); requestAnimationFrame(loop);
}
/* ---- Scale stage to window ---- */
function fit(){
const s=Math.min(innerWidth/W,innerHeight/H);
$('stage').style.transform='scale('+s+')';
}
addEventListener('resize',fit);
genCity(); fit(); syncCfgUI(); requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"63b42dba-e2d8-475c-a8ef-d16bf026ca71","tokensIn":103093,"tokensOut":9963,"tokensTotal":113056,"cost":0.008475465,"turns":8,"toolCalls":7,"failedToolCalls":0,"timestamp":"2026-08-31T14:12:35.692Z"} -->