Gorillaz
Qwen 3.8 27B GGUF XL · 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,847 bytes · SHA-256 deaae4817532
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GORILLAZ — Banana Bomb Artillery</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
:root{
--p1:#ff5252; --p2:#52a5ff; --amber:#ffd23b; --bg:#0e0b16;
--line:#4a3f6e; --dim:#9a8fc0; --green:#7dff6a;
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:100%;overflow:hidden;background:var(--bg);font-family:'Press Start 2P','Courier New',monospace;user-select:none;-webkit-user-select:none;}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
background:radial-gradient(1200px 700px at 50% 40%, #1c1533 0%, #0e0b16 70%);}
#frame{position:relative;width:min(100vw, calc(100vh * 16 / 9));aspect-ratio:16/9;
box-shadow:0 0 0 4px #241b3f, 0 0 70px rgba(0,0,0,.7);}
canvas{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated;image-rendering:crisp-edges;background:#2f6fd0;}
#scanlines{position:absolute;inset:0;z-index:4;pointer-events:none;opacity:.45;
background:repeating-linear-gradient(0deg, rgba(0,0,0,.10) 0 1px, transparent 1px 3px);}
.hud{position:absolute;left:0;right:0;display:flex;align-items:center;justify-content:space-between;
padding:8px 12px;background:rgba(10,8,20,.74);pointer-events:none;z-index:5;gap:10px;}
#hud-top{top:0;border-bottom:3px solid #241b3f;}
#hud-bottom{bottom:0;border-top:3px solid #241b3f;flex-wrap:wrap;row-gap:4px;}
.chip{font-size:9px;color:#efeaff;letter-spacing:1px;display:flex;gap:8px;align-items:center;}
.p1c{color:var(--p1);} .p2c{color:var(--p2);}
.dim{color:var(--dim);font-size:7px;}
#wind-box{font-size:10px;color:var(--amber);display:flex;gap:8px;align-items:center;}
#wind-arrow{font-size:14px;min-width:14px;text-align:center;}
#turn-label{font-size:11px;padding:6px 10px;border:2px solid var(--p1);color:var(--p1);letter-spacing:1px;}
#aim-readout{font-size:9px;color:#efeaff;letter-spacing:1px;}
.btnrow{display:flex;gap:5px;align-items:center;}
.hint{font-size:6px;color:var(--dim);letter-spacing:1px;margin-left:auto;text-align:right;}
.mini{pointer-events:auto;cursor:pointer;width:28px;height:28px;font-family:inherit;font-size:10px;
background:#241b3f;color:#efeaff;border:1px solid var(--line);}
.mini:hover{background:#332856;}
.btn{pointer-events:auto;cursor:pointer;font-family:inherit;font-size:9px;color:#16101f;background:var(--amber);
border:none;padding:9px 12px;box-shadow:0 3px 0 #8a6d1a;letter-spacing:1px;}
.btn:active{transform:translateY(2px);box-shadow:0 1px 0 #8a6d1a;}
.btn.ghost{background:#241b3f;color:#efeaff;box-shadow:0 3px 0 #120d22;}
#sysbtns{position:absolute;top:46px;right:10px;display:flex;gap:6px;z-index:8;}
#tuning-badge{position:absolute;top:46px;left:12px;z-index:6;font-size:7px;color:#0e0b16;background:var(--green);
padding:6px 8px;display:none;pointer-events:none;box-shadow:2px 2px 0 rgba(0,0,0,.6);letter-spacing:1px;}
#tuning-badge.show{display:block;}
#banner{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;
gap:20px;background:rgba(8,6,16,.55);z-index:10;}
#banner.show{display:flex;}
#banner-title{font-size:32px;color:var(--amber);text-shadow:4px 4px 0 #000;text-align:center;line-height:1.4;}
#banner-sub{font-size:11px;color:#efeaff;letter-spacing:2px;text-shadow:2px 2px 0 #000;}
#banner .row{display:flex;gap:14px;}
#panel{position:absolute;top:0;right:0;bottom:0;width:min(320px,88%);background:rgba(19,14,32,.97);
border-left:3px solid var(--amber);transform:translateX(103%);transition:transform .25s ease;z-index:30;
display:flex;flex-direction:column;}
#panel.open{transform:none;}
#panel-head{padding:12px;border-bottom:2px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:8px;}
#panel-title{font-size:10px;color:var(--amber);letter-spacing:1px;}
#panel-body{overflow-y:auto;padding:4px 12px;flex:1;}
#panel-body::-webkit-scrollbar{width:8px;}
#panel-body::-webkit-scrollbar-thumb{background:var(--line);}
.trow{padding:10px 0;border-bottom:1px dashed #2c2347;}
.trow-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:6px;}
.tlabel{font-size:8px;color:#cfc7e8;letter-spacing:1px;}
.tnote{font-size:6px;padding:3px 5px;border:1px solid var(--line);color:var(--dim);letter-spacing:1px;white-space:nowrap;}
.tnote.live{color:var(--green);border-color:#2e5a35;}
.tctrl{display:flex;gap:6px;align-items:center;}
.tval{flex:1;min-width:0;background:#0d0a18;border:1px solid var(--line);color:var(--amber);
font-family:inherit;font-size:11px;padding:7px 8px;text-align:right;}
.tval.dirty{color:var(--green);border-color:#2e5a35;}
select.tval{text-align:left;cursor:pointer;}
input.tval::-webkit-outer-spin-button,input.tval::-webkit-inner-spin-button{opacity:1;}
.tbtn{font-family:inherit;font-size:8px;padding:9px 12px;background:#241b3f;color:#efeaff;border:1px solid var(--line);cursor:pointer;letter-spacing:1px;}
.tbtn:hover{background:#332856;}
.tbtn.on{background:#2e5a35;color:var(--green);border-color:#2e5a35;}
#panel-foot{padding:10px 12px;border-top:2px solid var(--line);font-size:7px;color:var(--dim);line-height:1.9;}
#panel-seed{color:var(--amber);}
</style>
</head>
<body>
<div id="stage">
<div id="frame">
<canvas id="game" width="960" height="540"></canvas>
<div id="scanlines"></div>
<div id="hud-top" class="hud">
<div class="chip"><span class="p1c">P1 <span id="score-1">0</span></span></div>
<div class="chip" id="wind-box">WIND <span id="wind-val">0</span> <span id="wind-arrow">·</span><span class="dim">MATCH <span id="match-num">1</span></span></div>
<div class="chip"><span class="p2c"><span id="score-2">0</span> P2</span></div>
</div>
<div id="hud-bottom" class="hud">
<div id="turn-label">PLAYER 1 TURN</div>
<div id="aim-readout">ANGLE 45° · POWER 60</div>
<div class="btnrow">
<button class="mini" id="btn-a-" title="Angle down">∠−</button>
<button class="mini" id="btn-a+" title="Angle up">∠+</button>
<button class="mini" id="btn-p-" title="Power down">P−</button>
<button class="mini" id="btn-p+" title="Power up">P+</button>
<button class="btn" id="btn-fire">FIRE</button>
</div>
<div class="hint">◀▶ ANGLE · ▲▼ POWER · SPACE FIRE · T TUNE · M MUTE</div>
</div>
<div id="tuning-badge">LIVE TUNING ACTIVE</div>
<div id="sysbtns">
<button class="mini" id="btn-mute" title="Mute (M)">♪ ON</button>
<button class="mini" id="btn-gear" title="Tuning (T)">⚙</button>
</div>
<div id="banner">
<div id="banner-title">PLAYER 1 WINS!</div>
<div id="banner-sub">KA-BOOM!</div>
<div class="row">
<button class="btn" id="btn-rematch">REMATCH (R)</button>
<button class="btn ghost" id="btn-tune2">TUNING (T)</button>
</div>
</div>
<aside id="panel">
<div id="panel-head">
<div id="panel-title">⚙ LIVE TUNING</div>
<button class="mini" id="btn-close-panel">✕</button>
</div>
<div id="panel-body"></div>
<div id="panel-foot">
SEED <span id="panel-seed">0</span> — city layout & wind are deterministic per seed.<br>
Edits apply as tagged (LIVE / NEXT TURN / NEXT MATCH) and persist for the session.
<div style="margin-top:8px"><button class="tbtn" id="btn-reset-cfg">RESET DEFAULTS</button></div>
</div>
</aside>
</div>
</div>
<script>
'use strict';
/* ============ helpers ============ */
const $=id=>document.getElementById(id);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const TAU=Math.PI*2;
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);let r=n>>16&255,g=n>>8&255,b=n&255;
const f=c=>clamp(Math.round(amt<0?c*(1+amt):c+(255-c)*amt),0,255);
return `rgb(${f(r)},${f(g)},${f(b)})`;
}
/* ============ live configuration ============ */
const DEFAULTS={
blastRadius:70, gravity:900,
windMin:0, windMax:100, windShifts:true, windDirShifts:true,
buildingCount:11, bldgHMin:80, bldgHMax:260,
powerMin:10, powerMax:100,
splashModel:'hard', falloffMin:0
};
let cfg={...DEFAULTS};
const dirty=new Set();
const FIELDS=[
{k:'blastRadius',label:'BLAST RADIUS',min:20,max:150,step:5,unit:'px',note:'live'},
{k:'gravity',label:'GRAVITY',min:100,max:2000,step:50,unit:'px/s²',note:'live'},
{k:'windMin',label:'WIND MIN',min:0,max:100,step:5,note:'next turn'},
{k:'windMax',label:'WIND MAX',min:0,max:200,step:5,note:'next turn'},
{k:'windShifts',label:'WIND SHIFTS / TURN',type:'toggle',note:'next turn'},
{k:'windDirShifts',label:'WIND DIR SHIFTS',type:'toggle',note:'next turn'},
{k:'buildingCount',label:'BUILDING COUNT',min:4,max:20,step:1,note:'next match'},
{k:'bldgHMin',label:'BUILDING H MIN',min:40,max:300,step:10,note:'next match'},
{k:'bldgHMax',label:'BUILDING H MAX',min:100,max:380,step:10,note:'next match'},
{k:'powerMin',label:'POWER MIN',min:1,max:100,step:1,note:'live'},
{k:'powerMax',label:'POWER MAX',min:10,max:150,step:5,note:'live'},
{k:'splashModel',label:'SPLASH MODEL',type:'select',options:[['hard','HARD RADIUS'],['falloff','FALLOFF']],note:'live'},
{k:'falloffMin',label:'FALLOFF MIN DMG',min:0,max:100,step:5,unit:'%',note:'live'}
];
/* ============ canvas / constants ============ */
const W=960,H=540,GROUND_H=46,GROUND_Y=H-GROUND_H;
const POWER_K=8.5, WIND_K=1.2, STEP=1/120;
const cv=$('game'),ctx=cv.getContext('2d');
ctx.imageSmoothingEnabled=false;
/* ============ state ============ */
const game={state:'aim',turn:0,scores:[0,0],match:1,seed:0,first:0,windRng:Math.random};
const wind={value:0,dir:1};
let blds=[],gorillas=[],banana=null,blast=null,particles=[],scorch=[],clouds=[],streaks=[];
const aims=[{angle:45,power:60},{angle:45,power:60}];
let acc=0,frameCount=0;
/* ============ sprites (12 x 14 grids) ============ */
const SPR_IDLE=[
"..11111111..",".1111111111.","111111111111","1EE111111EE1",
"111111111111",".11MMMMMM11.","..11111111..",".3333333333.",
"433333333334","433555555334",".3355555533.",".3333333333.",
".33......33.",".33......33."
];
const SPR_WIN=[
"..11111111..",".1111111111.","111111111111","1EE111111EE1",
"111111111111",".11MMMMMM11.","..11111111..","44.333333.44",
"44.333333.44","44.355553.44","..33555533..","..33333333..",
".33......33.",".33......33."
];
const SPR_DEAD=[
"..11111111..",".1111111111.","111111111111","1XX111111XX1",
"111111111111",".11MMMMMM11.","..11111111..",".3333333333.",
"433333333334","433555555334",".3355555533.",".3333333333.",
".33......33.",".33......33."
];
function pal(idx){
const base=idx===0?'#ff5252':'#52a5ff';
return {'1':base,'3':shade(base,-0.32),'4':shade(base,0.05),'5':shade(base,0.45),'2':'#241a20','E':'#141420'};
}
const DEAD_PAL={'1':'#8a8a99','3':shade('#8a8a99',-0.3),'4':shade('#8a8a99',0.05),'5':shade('#8a8a99',0.4),'2':'#241a20','X':'#241a20','E':'#141420'};
/* ============ audio (synth) ============ */
const Audio={
ctx:null,master:null,muted:false,windGain:null,
unlock(){
if(!this.ctx){
const AC=window.AudioContext||window.webkitAudioContext;if(!AC)return;
this.ctx=new AC();
this.master=this.ctx.createGain();this.master.gain.value=0.55;this.master.connect(this.ctx.destination);
const len=this.ctx.sampleRate*2,buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate),d=buf.getChannelData(0);
let last=0;for(let i=0;i<len;i++){last=last*0.97+(Math.random()*2-1)*0.05;d[i]=last*3;}
const src=this.ctx.createBufferSource();src.buffer=buf;src.loop=true;
const f=this.ctx.createBiquadFilter();f.type='bandpass';f.frequency.value=320;f.Q.value=0.6;
this.windGain=this.ctx.createGain();this.windGain.gain.value=0;
src.connect(f);f.connect(this.windGain);this.windGain.connect(this.master);src.start();
}
if(this.ctx.state==='suspended')this.ctx.resume();
},
setWind(v){ if(this.ctx&&this.windGain) this.windGain.gain.setTargetAtTime(this.muted?0:(v/100)*0.14,this.ctx.currentTime,0.6); },
tone(freq,dur,type,vol,slide){
if(!this.ctx||this.muted)return;
const t=this.ctx.currentTime,o=this.ctx.createOscillator(),g=this.ctx.createGain();
o.type=type||'square';o.frequency.setValueAtTime(freq,t);
if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,freq+slide),t+dur);
g.gain.setValueAtTime(vol||0.2,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);
o.connect(g);g.connect(this.master);o.start(t);o.stop(t+dur+0.02);
},
noise(dur,vol,f0,f1){
if(!this.ctx||this.muted)return;
const t=this.ctx.currentTime,len=Math.max(1,Math.floor(this.ctx.sampleRate*dur));
const buf=this.ctx.createBuffer(1,len,this.ctx.sampleRate),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 f=this.ctx.createBiquadFilter();f.type='lowpass';
f.frequency.setValueAtTime(f0,t);f.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t+dur);
const g=this.ctx.createGain();g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);
src.connect(f);f.connect(g);g.connect(this.master);src.start(t);
},
launch(){this.noise(0.12,0.25,3000,600);this.tone(180,0.18,'square',0.15,420);},
explode(direct){this.noise(direct?0.7:0.55,0.8,900,60);this.tone(60,direct?0.5:0.35,'sine',0.5,-20);},
hurt(){this.tone(140,0.2,'sawtooth',0.25,-60);},
fanfare(){[392,523,659,784,1046].forEach((f,i)=>setTimeout(()=>this.tone(f,i===4?0.4:0.13,'square',0.22),i*130));},
toggle(){
this.muted=!this.muted;
if(this.ctx){
if(this.muted)this.windGain.gain.setTargetAtTime(0,this.ctx.currentTime,0.1);
else this.setWind(wind.value);
}
return this.muted;
}
};
/* ============ city generation ============ */
const BLDG_COLORS=['#e94f37','#f6ae2d','#38a3a5','#8e5ea2','#4c86a8','#d96c47','#6b9e4e','#b56576','#e8c547','#5f798b','#c25b8e','#43a047'];
function makeGorilla(b,idx){
return {x:b.x+b.w/2,y:b.y,dir:idx===0?1:-1,idx,hp:100,alive:true,win:false,hurtT:0,phase:idx*1.7};
}
function genCity(){
const rng=mulberry32(game.seed);
game.windRng=mulberry32(game.seed^0x9E3779B9);
const n=clamp(Math.round(cfg.buildingCount),4,24);
const hMin=Math.min(cfg.bldgHMin,cfg.bldgHMax),hMax=Math.max(cfg.bldgHMin,cfg.bldgHMax);
const widths=[],gaps=[];
for(let i=0;i<n;i++){widths.push(58+rng()*72);if(i<n-1)gaps.push(8+rng()*16);}
const sumW=widths.reduce((a,b)=>a+b,0),sumG=gaps.reduce((a,b)=>a+b,0);
const scale=(W-16-sumG)/sumW;
const palette=[...BLDG_COLORS];
for(let i=palette.length-1;i>0;i--){const j=Math.floor(rng()*(i+1));[palette[i],palette[j]]=[palette[j],palette[i]];}
blds=[];let x=8;
for(let i=0;i<n;i++){
const w=Math.max(40,widths[i]*scale);
const h=Math.round(hMin+rng()*(hMax-hMin));
const b={x,w,y:GROUND_Y-h,h,color:palette[i%palette.length],windows:[]};
const cols=Math.floor((w-14)/15),rows=Math.floor((h-16)/17);
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
const wx=b.x+9+c*15,wy=b.y+10+r*17;
if(wx+9<b.x+w-4)b.windows.push({x:wx,y:wy,lit:rng()<0.28});
}
blds.push(b);x+=w+(i<n-1?gaps[i]:0);
}
// place gorillas: left-side / right-side buildings, out of blast range
const leftIdx=[],rightIdx=[];
for(let i=0;i<n;i++){if(i<n/2)leftIdx.push(i);else rightIdx.push(i);}
const pairs=[];
for(const i of leftIdx)for(const j of rightIdx)pairs.push([i,j]);
for(let i=pairs.length-1;i>0;i--){const j=Math.floor(rng()*(i+1));[pairs[i],pairs[j]]=[pairs[j],pairs[i]];}
const sep=(i,j)=>Math.abs((blds[j].x+blds[j].w/2)-(blds[i].x+blds[i].w/2));
const need=cfg.blastRadius*2.2;
let pick=null,bestD=-1;
for(const [i,j] of pairs){
const d=sep(i,j);
if(d>bestD){bestD=d;pick=[i,j];}
if(d>=need){break;}
}
gorillas=[makeGorilla(blds[pick[0]],0),makeGorilla(blds[pick[1]],1)];
scorch=[];
clouds=[];for(let i=0;i<5;i++)clouds.push({x:rng()*W,y:40+rng()*140,s:0.7+rng()*0.9,v:4+rng()*8});
streaks=[];for(let i=0;i<9;i++)streaks.push({y:30+rng()*(GROUND_Y-60),len:40+rng()*70,off:rng()*W});
}
function rollWind(){
const lo=Math.min(cfg.windMin,cfg.windMax),hi=Math.max(cfg.windMin,cfg.windMax);
wind.value=Math.round((lo+game.windRng()*(hi-lo))*10)/10;
if(cfg.windDirShifts)wind.dir=game.windRng()<0.5?-1:1;
Audio.setWind(wind.value);
}
/* ============ match / turn / combat ============ */
function startMatch(){
game.seed=10000+Math.floor(Math.random()*89999);
genCity();
banana=null;blast=null;particles=[];acc=0;
game.turn=game.first;
game.state='aim';
rollWind();
$('banner').classList.remove('show');
refreshPanel();
}
function startTurn(){
if(cfg.windShifts)rollWind();else Audio.setWind(wind.value);
game.state='aim';
}
function fire(){
if(game.state!=='aim')return;
const g=gorillas[game.turn],a=aims[game.turn];
const sp=a.power*POWER_K,rad=a.angle*Math.PI/180;
banana={x:g.x,y:g.y-26,vx:Math.cos(rad)*g.dir*sp,vy:-Math.sin(rad)*sp,trail:[],grace:0.15,shooter:game.turn};
game.state='flight';
Audio.launch();
}
function stepBanana(dt){
const b=banana;
b.grace-=dt;
b.vx+=wind.dir*wind.value*WIND_K*dt;
b.vy+=cfg.gravity*dt;
b.x+=b.vx*dt;b.y+=b.vy*dt;
b.trail.push({x:b.x,y:b.y});if(b.trail.length>22)b.trail.shift();
if(b.grace<=0){
for(const g of gorillas){
if(!g.alive)continue;
if(Math.hypot(b.x-g.x,b.y-(g.y-21))<26){directHit(g);return;}
}
}
for(const bl of blds){
if(b.x>bl.x-5&&b.x<bl.x+bl.w+5&&b.y>bl.y-5&&b.y<GROUND_Y){impact(b.x,Math.max(b.y,bl.y));return;}
}
if(b.y>=GROUND_Y){impact(b.x,GROUND_Y);return;}
if(b.x<-80||b.x>W+80||b.y>H+140){banana=null;nextTurn();}
}
function directHit(g){
const b=banana;banana=null;
g.hp=0;g.alive=false;g.hurtT=1;
doExplosion(b.x,b.y,true);
}
function impact(x,y){
const b=banana;banana=null;
doExplosion(x,y,false);
}
function doExplosion(x,y,direct){
blast={x,y,t:0,direct};
spawnParticles(x,y);
scorch.push({x,y,r:10+Math.random()*12});if(scorch.length>24)scorch.shift();
Audio.explode(direct);
game.state='blast';
}
function resolveBlast(){
const R=cfg.blastRadius;
for(const g of gorillas){
if(!g.alive)continue;
const d=Math.hypot(g.x-blast.x,(g.y-21)-blast.y);
if(d<=R){
const dmg=cfg.splashModel==='hard'?100:100*(cfg.falloffMin/100+(1-cfg.falloffMin/100)*(1-d/R));
g.hp-=dmg;g.hurtT=0.5;
if(g.hp<=0){g.hp=0;g.alive=false;}
else Audio.hurt();
}
}
blast=null;
checkEnd();
}
function checkEnd(){
const dead=gorillas.filter(g=>!g.alive);
if(dead.length===0){nextTurn();return;}
if(dead.length===2){endRound(-1);return;}
const winner=gorillas.find(g=>g.alive);
winner.win=true;
endRound(winner.idx);
}
function nextTurn(){game.turn=1-game.turn;startTurn();}
const SUBS=['KA-BOOM!','BANANA SPLAT!','MONKEY BUSINESS!','DIRECT HIT, BABY!','WHAT A LOB!'];
function endRound(winnerIdx){
game.state='end';
if(winnerIdx>=0){game.scores[winnerIdx]++;Audio.fanfare();}
else Audio.hurt();
const t=$('banner-title');
t.textContent=winnerIdx<0?'DRAW!':`PLAYER ${winnerIdx+1} WINS!`;
t.style.color=winnerIdx===0?'var(--p1)':winnerIdx===1?'var(--p2)':'var(--amber)';
$('banner-sub').textContent=winnerIdx<0?'BOTH GORILLAS SPLATTED':SUBS[Math.floor(Math.random()*SUBS.length)];
$('banner').classList.add('show');
}
function rematch(){
game.match++;
game.first=1-game.first;
startMatch();
}
/* ============ particles ============ */
function spawnParticles(x,y){
const cols=['#ffd23b','#ff8c42','#ff5252','#ffe8a3','#8d8d99'];
for(let i=0;i<26;i++){
const a=Math.random()*TAU,sp=60+Math.random()*260;
particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-80,life:0.4+Math.random()*0.5,max:0.9,size:2+Math.random()*4,color:cols[i%cols.length]});
}
}
/* ============ update ============ */
const keys={};
function update(dt){
frameCount++;
const dir=wind.dir||1;
for(const c of clouds){
c.x+=(c.v+wind.value*0.3)*dir*dt;
if(c.x>W+140)c.x=-140;if(c.x<-160)c.x=W+140;
}
if(game.state==='aim'){
const g=gorillas[game.turn],a=aims[game.turn];
const leftK=g.dir===1?'ArrowLeft':'ArrowRight',rightK=g.dir===1?'ArrowRight':'ArrowLeft';
let dA=0;if(keys[leftK])dA++;if(keys[rightK])dA--;
a.angle=clamp(a.angle+dA*50*dt,0,90);
let dP=0;if(keys.ArrowUp)dP++;if(keys.ArrowDown)dP--;
a.power=clamp(a.power+dP*55*dt,cfg.powerMin,cfg.powerMax);
}
if(game.state==='flight'){
acc+=dt;
while(acc>=STEP&&game.state==='flight'){stepBanana(STEP);acc-=STEP;}
}
if(game.state==='blast'&&blast){
blast.t+=dt;
if(blast.t>0.55)resolveBlast();
}
for(const p of particles){p.vy+=600*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt;}
particles=particles.filter(p=>p.life>0);
for(const g of gorillas)g.hurtT=Math.max(0,g.hurtT-dt);
syncHud();
}
/* ============ rendering ============ */
function drawSky(){
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#2f6fd0');g.addColorStop(0.55,'#5f9fe0');g.addColorStop(1,'#ffd9a0');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
}
function drawSun(){
ctx.save();ctx.translate(830,92);
ctx.fillStyle='#ffe066';
for(let i=0;i<8;i++){ctx.rotate(TAU/8);ctx.fillRect(-5,-44,10,10);}
ctx.beginPath();ctx.arc(0,0,30,0,TAU);ctx.fill();
ctx.fillStyle='#fff3b0';ctx.beginPath();ctx.arc(0,0,22,0,TAU);ctx.fill();
ctx.restore();
}
function drawClouds(){
ctx.fillStyle='rgba(255,255,255,0.85)';
for(const c of clouds){
const s=c.s;
ctx.fillRect(c.x,c.y,60*s,16*s);
ctx.fillRect(c.x+12*s,c.y-10*s,30*s,12*s);
ctx.fillRect(c.x-10*s,c.y+4*s,30*s,10*s);
}
}
function drawWindStreaks(){
if(wind.value<5)return;
const n=Math.min(streaks.length,Math.ceil(wind.value/10));
const sp=60+wind.value*2.2,dir=wind.dir||1,t=performance.now()/1000;
ctx.strokeStyle='rgba(255,255,255,0.22)';ctx.lineWidth=2;
for(let i=0;i<n;i++){
const s=streaks[i];
const span=W+s.len;
let x=(s.off+sp*dir*t)%span;if(x<0)x+=span;x-=s.len;
ctx.beginPath();ctx.moveTo(x,s.y);ctx.lineTo(x+s.len*dir,s.y);ctx.stroke();
}
}
function drawBuildings(){
for(const b of blds){
ctx.fillStyle=b.color;ctx.fillRect(b.x,b.y,b.w,b.h);
ctx.fillStyle=shade(b.color,0.35);ctx.fillRect(b.x,b.y,b.w,4);
ctx.fillStyle=shade(b.color,-0.45);ctx.fillRect(b.x+b.w-5,b.y,5,b.h);
for(const wd of b.windows){
ctx.fillStyle=wd.lit?'#ffe066':'#1c2340';
ctx.fillRect(wd.x,wd.y,9,11);
}
ctx.fillStyle='rgba(0,0,0,0.2)';ctx.fillRect(b.x,b.y+b.h-6,b.w,6);
}
}
function drawGround(){
ctx.fillStyle='#23253a';ctx.fillRect(0,GROUND_Y,W,GROUND_H);
ctx.fillStyle='#3a3d5c';ctx.fillRect(0,GROUND_Y,W,4);
ctx.fillStyle='#15162a';
for(let x=0;x<W;x+=24)ctx.fillRect(x,GROUND_Y+14,12,3);
}
function drawScorch(){
for(const s of scorch){
ctx.fillStyle='rgba(20,18,26,0.5)';
ctx.beginPath();ctx.ellipse(s.x,Math.max(s.y,GROUND_Y-2),s.r,s.r*0.3,0,0,TAU);ctx.fill();
}
}
function drawGorilla(g){
const t=performance.now()/1000;
let grid=SPR_IDLE,pp=pal(g.idx);
if(!g.alive){grid=SPR_DEAD;pp=DEAD_PAL;}
else if(g.win){grid=SPR_WIN;}
let yOff=Math.sin(t*3+g.phase)*2;
if(g.win)yOff=-Math.abs(Math.sin(t*6))*8;
if(g.hurtT>0&&g.alive)yOff=Math.sin(t*60)*3;
const px=Math.round(g.x-24),py=Math.round(g.y-28+yOff);
for(let r=0;r<14;r++)for(let c=0;c<12;c++){
const ch=grid[r][c];if(ch==='.'||ch===' ')continue;
ctx.fillStyle=pp[ch]||'#000';
ctx.fillRect(px+c*4,py+r*4,4,4);
}
const col=g.idx===0?'#ff5252':'#52a5ff';
ctx.fillStyle=col;
ctx.fillRect(g.x-4,py-16,8,6);
ctx.beginPath();ctx.moveTo(g.x-4,py-10);ctx.lineTo(g.x+4,py-10);ctx.lineTo(g.x,py-4);ctx.fill();
}
function drawAim(){
if(game.state!=='aim')return;
const g=gorillas[game.turn],a=aims[game.turn];
const col=g.idx===0?'#ff5252':'#52a5ff';
const rad=a.angle*Math.PI/180,sp=a.power*POWER_K;
let x=g.x,y=g.y-26,vx=Math.cos(rad)*g.dir*sp,vy=-Math.sin(rad)*sp;
const dt=1/60;
const PREVIEW=18; // show only the start of the arc, not the full trajectory
for(let i=0;i<PREVIEW;i++){
vx+=wind.dir*wind.value*WIND_K*dt;vy+=cfg.gravity*dt;
x+=vx*dt;y+=vy*dt;
if(i%3===0){ctx.globalAlpha=Math.max(0.25,1-i/PREVIEW);ctx.fillStyle=col;ctx.fillRect(x-2,y-2,4,4);}
let hit=y>GROUND_Y||x<-10||x>W+10;
if(!hit)for(const bl of blds){if(x>bl.x&&x<bl.x+bl.w&&y>bl.y){hit=true;break;}}
if(hit)break;
}
ctx.globalAlpha=1;
ctx.save();ctx.translate(g.x,g.y-26);
ctx.rotate(g.dir===1?-rad:rad-Math.PI);
ctx.strokeStyle=col;ctx.lineWidth=3;
ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(34,0);ctx.stroke();
ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(42,0);ctx.lineTo(30,-6);ctx.lineTo(30,6);ctx.fill();
ctx.restore();
const bx=g.x-31,by=g.y-70;
ctx.fillStyle='rgba(0,0,0,0.5)';ctx.fillRect(bx-2,by-2,66,10);
const frac=clamp((a.power-cfg.powerMin)/(cfg.powerMax-cfg.powerMin||1),0,1);
ctx.fillStyle=col;ctx.fillRect(bx,by,62*frac,6);
ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.strokeRect(bx-2,by-2,66,10);
}
function drawBanana(){
if(!banana)return;
const b=banana;
for(let i=0;i<b.trail.length;i++){
const p=b.trail[i],a=i/b.trail.length;
ctx.fillStyle=`rgba(255,210,59,${a*0.5})`;
const s=2+a*3;
ctx.fillRect(p.x-s/2,p.y-s/2,s,s);
}
ctx.save();
ctx.translate(b.x,b.y);
ctx.rotate(Math.atan2(b.vy,b.vx)+Math.PI/2);
ctx.fillStyle='#ffd23b';ctx.fillRect(-3,-7,6,14);
ctx.fillRect(-5,-4,2,8);ctx.fillRect(3,-4,2,8);
ctx.fillStyle='#c99a1e';ctx.fillRect(-3,-9,6,3);ctx.fillRect(-3,6,6,3);
ctx.fillStyle='#fff3b0';ctx.fillRect(-1,-5,2,8);
ctx.restore();
}
function drawBlast(){
if(!blast)return;
const p=blast.t/0.55;
const r=cfg.blastRadius*Math.min(1,p*1.15);
ctx.save();
ctx.globalAlpha=Math.max(0,1-p);
ctx.fillStyle='rgba(255,210,59,0.35)';
ctx.beginPath();ctx.arc(blast.x,blast.y,r,0,TAU);ctx.fill();
ctx.strokeStyle='#fff';ctx.lineWidth=3;
ctx.beginPath();ctx.arc(blast.x,blast.y,r,0,TAU);ctx.stroke();
ctx.strokeStyle='#ff8c42';ctx.lineWidth=6;
ctx.beginPath();ctx.arc(blast.x,blast.y,r*0.6,0,TAU);ctx.stroke();
ctx.restore();
}
function draw(){
drawSky();drawSun();drawClouds();drawWindStreaks();
drawBuildings();drawGround();drawScorch();
for(const g of gorillas)drawGorilla(g);
drawAim();
drawBanana();
drawBlast();
for(const p of particles){
ctx.globalAlpha=Math.max(0,p.life/p.max);
ctx.fillStyle=p.color;
ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
}
ctx.globalAlpha=1;
}
/* ============ HUD sync ============ */
let hudCache='';
function syncHud(){
const g=gorillas[game.turn];
const a=aims[game.turn];
const turnTxt=game.state==='end'?'GAME OVER':`PLAYER ${game.turn+1} TURN`;
const readTxt=game.state==='aim'?`ANGLE ${Math.round(a.angle)}° · POWER ${Math.round(a.power)}`
:game.state==='flight'?'BANANA IN FLIGHT…'
:game.state==='blast'?'KA-BOOM!':'…';
const key=[game.turn,game.state,Math.round(a.angle),Math.round(a.power),Math.round(wind.value*10),wind.dir,game.scores.join(),game.match].join('|');
if(key===hudCache)return;hudCache=key;
$('score-1').textContent=game.scores[0];
$('score-2').textContent=game.scores[1];
$('match-num').textContent=game.match;
$('wind-val').textContent=Math.round(wind.value);
$('wind-arrow').textContent=wind.value<1?'·':(wind.dir>0?'→':'←');
const tl=$('turn-label');
tl.textContent=turnTxt;
const c=game.state==='end'?'#efeaff':(game.turn===0?'var(--p1)':'var(--p2)');
tl.style.color=c;tl.style.borderColor=c;
$('aim-readout').textContent=readTxt;
}
/* ============ tuning panel ============ */
function buildPanel(){
const body=$('panel-body');
body.innerHTML='';
for(const f of FIELDS){
const row=document.createElement('div');row.className='trow';
const top=document.createElement('div');top.className='trow-top';
const lab=document.createElement('span');lab.className='tlabel';lab.textContent=f.label;
const note=document.createElement('span');note.className='tnote';note.id='note-'+f.k;
top.append(lab,note);
const ctrl=document.createElement('div');ctrl.className='tctrl';
if(f.type==='toggle'){
const input=document.createElement('button');input.className='tbtn';input.id='in-'+f.k;
input.onclick=()=>setCfg(f.k,!cfg[f.k]);
ctrl.append(input);
}else if(f.type==='select'){
const input=document.createElement('select');input.className='tval';input.id='in-'+f.k;
for(const [v,l] of f.options){const o=document.createElement('option');o.value=v;o.textContent=l;input.append(o);}
input.onchange=()=>setCfg(f.k,input.value);
ctrl.append(input);
}else{
const minus=document.createElement('button');minus.className='tbtn';minus.textContent='−';
const inp=document.createElement('input');inp.type='number';inp.className='tval';inp.id='in-'+f.k;
inp.min=f.min;inp.max=f.max;inp.step=f.step;
inp.addEventListener('change',()=>setCfg(f.k,parseFloat(inp.value)));
const plus=document.createElement('button');plus.className='tbtn';plus.textContent='+';
minus.onclick=()=>setCfg(f.k,cfg[f.k]-f.step);
plus.onclick=()=>setCfg(f.k,cfg[f.k]+f.step);
ctrl.append(minus,inp,plus);
}
row.append(top,ctrl);
body.append(row);
}
}
function refreshPanel(){
for(const f of FIELDS){
const el=$('in-'+f.k),note=$('note-'+f.k);
if(!el||!note)continue;
if(f.type==='toggle'){el.textContent=cfg[f.k]?'ON':'OFF';el.classList.toggle('on',!!cfg[f.k]);}
else el.value=cfg[f.k];
const isDirty=dirty.has(f.k);
el.classList.toggle('dirty',isDirty);
note.textContent=isDirty?`LIVE (${f.note.toUpperCase()})`:f.note.toUpperCase();
note.classList.toggle('live',isDirty);
}
$('tuning-badge').classList.toggle('show',dirty.size>0);
$('panel-seed').textContent=game.seed;
}
function setCfg(k,v){
const f=FIELDS.find(x=>x.k===k);
if(f&&f.min!=null)v=clamp(v,f.min,f.max);
cfg[k]=v;
dirty.add(k);
if((k==='powerMin'||k==='powerMax')&&cfg.powerMin>cfg.powerMax){
const t=cfg.powerMin;cfg.powerMin=cfg.powerMax;cfg.powerMax=t;
}
for(const a of aims)a.power=clamp(a.power,cfg.powerMin,cfg.powerMax);
refreshPanel();
}
function resetCfg(){
cfg={...DEFAULTS};dirty.clear();
for(const a of aims)a.power=clamp(a.power,cfg.powerMin,cfg.powerMax);
refreshPanel();
}
/* ============ input ============ */
window.addEventListener('keydown',e=>{
if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT')return;
Audio.unlock();
const k=e.key;
if(k===' '){e.preventDefault();fire();return;}
if(k==='ArrowLeft'||k==='ArrowRight'||k==='ArrowUp'||k==='ArrowDown'){e.preventDefault();keys[k]=true;return;}
if(k==='Enter'&&game.state==='end'){rematch();return;}
if(k==='r'||k==='R'){if(game.state==='end')rematch();return;}
if(k==='t'||k==='T'){togglePanel();return;}
if(k==='m'||k==='M'){toggleMute();return;}
});
window.addEventListener('keyup',e=>{delete keys[e.key];});
function nudge(kind,d){
if(game.state!=='aim')return;
Audio.unlock();
const a=aims[game.turn];
if(kind==='angle')a.angle=clamp(a.angle+d,0,90);
else a.power=clamp(a.power+d,cfg.powerMin,cfg.powerMax);
}
$('btn-a-').onclick=()=>nudge('angle',-5);
$('btn-a+').onclick=()=>nudge('angle',5);
$('btn-p-').onclick=()=>nudge('power',-5);
$('btn-p+').onclick=()=>nudge('power',5);
$('btn-fire').onclick=()=>{Audio.unlock();fire();};
$('btn-rematch').onclick=rematch;
$('btn-gear').onclick=()=>togglePanel();
$('btn-tune2').onclick=()=>togglePanel();
$('btn-close-panel').onclick=()=>togglePanel(false);
$('btn-mute').onclick=toggleMute;
$('btn-reset-cfg').onclick=resetCfg;
function togglePanel(force){
const open=force!==undefined?force:!$('panel').classList.contains('open');
$('panel').classList.toggle('open',open);
}
function toggleMute(){
Audio.unlock();
const m=Audio.toggle();
$('btn-mute').textContent=m?'♪ OFF':'♪ ON';
}
/* ============ main loop / init ============ */
let lastT=0;
function loop(ts){
const dt=Math.min(0.05,(ts-lastT)/1000||0.016);lastT=ts;
update(dt);
draw();
requestAnimationFrame(loop);
}
buildPanel();
startMatch();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF-XL","provider":"brigid","persona":"full","sessionId":"a1a3292c-abc8-4c4f-b7ac-b6b003d05f64","tokensIn":52987,"tokensOut":1437,"tokensTotal":54424,"cost":0,"turns":3,"toolCalls":2,"failedToolCalls":0,"timestamp":"2026-08-19T03:24:53.966Z"} -->