Gorillaz
Tiel Coder 35B A3B GGUF MTP · 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 25,390 bytes · SHA-256 2bf0c9e9c8a9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gorillaz — Artillery Duel</title>
<style>
:root { --p1:#ff4455; --p2:#44aaff; --ink:#e9f0f0; }
* { box-sizing: border-box; }
html,body { margin:0; height:100%; background:#0b0e13; overflow:hidden; font-family: "Courier New", monospace; }
#wrap { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; }
canvas { image-rendering: pixelated; image-rendering: crisp-edges; background:#0b0e13; box-shadow:0 0 0 3px #1e2733, 0 0 40px rgba(0,0,0,.7); max-width:100%; max-height:100%; }
#hud { position:fixed; top:0; left:0; right:0; display:flex; gap:18px; align-items:center; padding:8px 16px; color:var(--ink); font-weight:bold; text-shadow:0 2px 0 #000; pointer-events:none; }
.badge { background:#12181f; border:2px solid #2c3a4d; border-radius:6px; padding:5px 12px; letter-spacing:1px; }
#turn { margin-left:auto; }
#wind .arr { display:inline-block; min-width:34px; text-align:center; color:#7fe3c2; }
#score { margin-left:14px; }
#aim { position:fixed; bottom:10px; left:50%; transform:translateX(-50%); display:flex; gap:22px; color:var(--ink); background:#12181f; border:2px solid #2c3a4d; border-radius:8px; padding:8px 18px; font-weight:bold; text-shadow:0 2px 0 #000; pointer-events:none; }
#aim span { color:#ffe14d; }
#help { position:fixed; bottom:10px; left:14px; color:#8aa0b3; font-size:12px; text-shadow:0 1px 0 #000; pointer-events:none; }
#overlay { position:fixed; inset:0; display:none; align-items:center; justify-content:center; background:rgba(6,9,13,.72); color:var(--ink); z-index:20; }
#overlay .panel { background:#10161d; border:3px solid #2c3a4d; border-radius:12px; padding:22px 26px; max-width:92%; max-height:92%; overflow:auto; box-shadow:0 0 40px rgba(0,0,0,.7); }
#overlay h1 { margin:0 0 4px; font-size:26px; letter-spacing:3px; }
#overlay h2 { margin:18px 0 8px; font-size:15px; color:#7fe3c2; letter-spacing:1px; border-bottom:1px solid #2c3a4d; padding-bottom:4px; }
table { border-collapse:collapse; width:100%; color:var(--ink); }
td { padding:4px 6px; border-bottom:1px solid #1c2631; }
td.num { width:70px; text-align:center; }
td.val { text-align:center; font-weight:bold; }
td.val.live { color:#7fe3c2; }
button { font-family:inherit; font-weight:bold; cursor:pointer; background:#1c2836; color:var(--ink); border:2px solid #33455a; border-radius:6px; padding:6px 12px; }
button:hover { background:#26384b; }
.rowbtns { display:flex; gap:8px; align-items:center; }
.ctrl { display:flex; align-items:center; gap:6px; }
.legend { font-size:12px; color:#8aa0b3; margin-top:10px; line-height:1.5; }
.livebadge { display:inline-block; background:#0e3a2e; color:#7fe3c2; border:1px solid #2c6b55; border-radius:4px; padding:1px 8px; font-size:11px; margin-left:8px; }
#winbox { text-align:center; }
#winbox .big { font-size:40px; letter-spacing:4px; margin:6px 0; }
.p1c { color:var(--p1); } .p2c { color:var(--p2); }
kbd { background:#22303f; border:1px solid #3a4c60; border-bottom-width:2px; border-radius:4px; padding:1px 6px; font-size:12px; }
</style>
</head>
<body>
<div id="wrap"><canvas id="c"></canvas></div>
<div id="hud">
<div class="badge" id="wind">WIND <span class="arr" id="windArr">→</span> <span id="windVal">0.0</span></div>
<div class="badge" id="score">P1 <span class="p1c">0</span> : <span class="p2c">0</span> P2</div>
<div class="badge" id="turn">PLAYER 1 TURN</div>
</div>
<div id="aim"><span>ANG <b id="angVal">45</b>°</span><span>PWR <b id="pwrVal">50</b></span></div>
<div id="help"><kbd>←</kbd>/<kbd>→</kbd> angle · <kbd>↑</kbd>/<kbd>↓</kbd> power · <kbd>Space</kbd> fire · <kbd>T</kbd> settings · <kbd>Enter</kbd> start/rematch</div>
<div id="overlay"><div class="panel" id="panel"></div></div>
<script>
"use strict";
(function(){
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const W = 960, H = 600, GROUND = 560;
canvas.width = W; canvas.height = H;
function fit(){
const s = Math.min(window.innerWidth/W, window.innerHeight/H);
canvas.style.width = (W*s)+'px';
canvas.style.height = (H*s)+'px';
}
window.addEventListener('resize', fit); fit();
/* ---------- Live-tunable configuration ---------- */
const CONFIG = {
blastRadius: 56,
gravity: 0.7,
windMin: 0.0,
windMax: 1.6,
windShifts: true,
buildingCount: 10,
buildingHeightMin: 110,
buildingHeightMax: 340,
powerMin: 6,
powerMax: 22,
damageMode: "radius", // "radius" (must be inside) | "falloff" (partial ring damage)
splashFalloff: 0.55 // falloff: damage factor at edge when damageMode=="falloff"
};
let tuningActive = false;
const DEFAULTS = Object.assign({}, CONFIG);
/* ---------- RNG (seeded, reproducible) ---------- */
let seed = (Math.random()*1e9)|0;
function rnd(){ seed = (seed*1103515245 + 12345) & 0x7fffffff; return seed/0x7fffffff; }
function ri(min,max){ return Math.floor(rnd()*(max-min+1))+min; }
function rf(min,max){ return rnd()*(max-min)+min; }
/* ---------- Game state ---------- */
let buildings = [];
let players = []; // {side:'L'|'R', x, y (roof top), color, alive, reaction: timer}
let projectile = null; // {x,y,vx,vy,trail}
let state = 'idle'; // idle | aiming | flying | exploding | result
let currentPlayer = 0; // 0 or 1
let angle = 45, power = 50; // aim values (0..90 deg, 0..100 power)
let wind = 0;
let score = [0,0];
let particles = [];
let scorch = null; // {x,y,r}
let windTimer = 0;
let resultTimer = 0;
const el = id => document.getElementById(id);
/* ---------- City generation ---------- */
function generateCity(){
buildings = [];
const count = Math.max(4, Math.min(14, CONFIG.buildingCount));
const gap = 14;
let totalGap = gap*(count-1);
let avail = W - totalGap;
// scale widths so the skyline fits within the canvas
let totalW = 0, widths = [];
for(let i=0;i<count;i++){
const w = Math.round(rf(44, 92));
widths.push(w); totalW += w;
}
const scale = totalW>0 ? avail/totalW : 1;
let x = 0;
for(let i=0;i<count;i++){
const w = Math.max(30, Math.round(widths[i]*scale));
const h = Math.round(rf(CONFIG.buildingHeightMin, CONFIG.buildingHeightMax));
const color = `hsl(${ri(190,260)} ${ri(28,45)}% ${ri(26,40)}%)`;
const windows = ri(3,6);
buildings.push({ x:x, y:GROUND-h, w:w, h:h, color, windows, windowColor:`hsl(${ri(30,55)} 90% ${ri(55,72)}%)` });
x += w + gap;
}
}
/* place two gorillas on left-side and right-side buildings, far apart */
function placeGorillas(){
const n = buildings.length;
let li = ri(0, Math.max(0, Math.floor(n*0.35)-1));
let ri_ = Math.floor(n*0.65)+ri(0, Math.max(0, n-1-Math.floor(n*0.65)));
if(ri_ <= li) ri_ = Math.min(n-1, li+2);
const bL = buildings[li], bR = buildings[ri_];
const gapPx = (bR.x) - (bL.x + bL.w);
const minSep = CONFIG.blastRadius*3 + 120;
// if too close, push to outer edges
if(gapPx < minSep){
li = 0; ri_ = n-1;
}
players = [
{ side:'L', x: buildings[li].x + buildings[li].w/2, y: buildings[li].y, building:li, color:'#ff4455', name:'P1', alive:true, reaction:0 },
{ side:'R', x: buildings[ri_].x + buildings[ri_].w/2, y: buildings[ri_].y, building:ri_, color:'#44aaff', name:'P2', alive:true, reaction:0 }
];
}
/* ---------- Wind ---------- */
function setWind(){
const range = Math.max(0, CONFIG.windMax - CONFIG.windMin);
const mag = rf(CONFIG.windMin, CONFIG.windMax);
wind = (rnd()<0.5 ? -1 : 1) * mag;
updateWindHUD();
}
function updateWindHUD(){
const arr = el('windArr');
if(Math.abs(wind) < 0.05){ arr.textContent='·'; arr.style.color='#556'; }
else { arr.textContent = wind>0 ? '→' : '←'; arr.style.color = '#7fe3c2'; }
el('windVal').textContent = Math.abs(wind).toFixed(2);
}
/* ---------- HUD ---------- */
function updateTurnHUD(){
const t = el('turn');
t.textContent = 'PLAYER '+(currentPlayer+1)+' TURN';
t.style.color = players[currentPlayer].color;
el('angVal').textContent = Math.round(angle);
el('pwrVal').textContent = Math.round(power);
el('score').innerHTML = 'P1 <span class="p1c">'+score[0]+'</span> : <span class="p2c">'+score[1]+'</span> P2';
}
/* ---------- Input ---------- */
const keys = {};
window.addEventListener('keydown', e=>{
const k = e.key;
if(k===' '|| k==='ArrowUp'||k==='ArrowDown'||k==='ArrowLeft'||k==='ArrowRight') e.preventDefault();
if(k==='Enter'){
if(state==='idle' || state==='result'){ startMatch(); }
return;
}
if(k==='t' || k==='T'){ toggleSettings(); return; }
if(state!=='aiming') return;
if(k===' '){ fire(); return; }
keys[k]=true;
});
window.addEventListener('keyup', e=>{ keys[e.key]=false; });
function clamp(v,a,b){ return Math.max(a,Math.min(b,v)); }
function aimTick(){
if(state!=='aiming') return;
if(keys['ArrowLeft']) angle = clamp(angle-0.8, 0, 90);
if(keys['ArrowRight']) angle = clamp(angle+0.8, 0, 90);
if(keys['ArrowUp']) power = clamp(power+0.6, CONFIG.powerMin, CONFIG.powerMax);
if(keys['ArrowDown']) power = clamp(power-0.6, CONFIG.powerMin, CONFIG.powerMax);
el('angVal').textContent = Math.round(angle);
el('pwrVal').textContent = Math.round(power);
}
/* ---------- Firing ---------- */
function fire(){
if(state!=='aiming') return;
const p = players[currentPlayer];
const rad = angle*Math.PI/180;
const speed = CONFIG.powerMin + (power-CONFIG.powerMin)/(100-CONFIG.powerMin)*(CONFIG.powerMax-CONFIG.powerMin);
const dir = p.side==='L' ? 1 : -1;
projectile = {
x: p.x + dir*18, y: p.y - 34,
vx: Math.cos(rad)*speed*dir,
vy: -Math.sin(rad)*speed,
trail: [], alive:true
};
state='flying';
playLaunch();
}
/* ---------- Physics & collision ---------- */
function circleHitsRect(cx,cy,cr, r){
const nx = Math.max(r.x, Math.min(cx, r.x+r.w));
const ny = Math.max(r.y, Math.min(cy, r.y+r.h));
const dx = cx-nx, dy = cy-ny;
return dx*dx+dy*dy <= cr*cr;
}
function explode(x,y){
scorch = { x:x, y:y, r:0, max: CONFIG.blastRadius*1.15, t:0 };
// particles
for(let i=0;i<30;i++){
const a = rnd()*Math.PI*2, sp = rf(1,7);
particles.push({ x:x, y:y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp - 1, life:1, c: rnd()<0.5?'#ffcc33':'#ff7722' });
}
playExplosion();
// damage check
const opp = players[1-currentPlayer];
const dx = opp.x-x, dy = (opp.y-30)-y;
const dist = Math.hypot(dx,dy);
let hit = false;
if(CONFIG.damageMode==='radius'){
hit = dist <= CONFIG.blastRadius;
} else {
// falloff: full damage inside radius, linear down to splashFalloff at edge ring
if(dist <= CONFIG.blastRadius*0.6) hit = true;
else if(dist <= CONFIG.blastRadius) hit = (1 - (dist - CONFIG.blastRadius*0.6)/(CONFIG.blastRadius*0.4)) > CONFIG.splashFalloff;
}
if(hit){
opp.alive = false; opp.reaction = 120;
state='exploding'; resultTimer = 90;
} else {
state='exploding'; resultTimer = 45;
}
}
function stepPhysics(){
if(state!=='flying') return;
const p = projectile;
const steps = 2;
for(let s=0;s<steps;s++){
p.vy += CONFIG.gravity*100/100*1; // gravity
p.vy += 0;
p.vx += wind*0.02;
p.x += p.vx;
p.y += p.vy;
p.trail.push({x:p.x,y:p.y});
if(p.trail.length>18) p.trail.shift();
const r = 6;
// ground
if(p.y >= GROUND - r){ explode(p.x, GROUND-r); return; }
// buildings (skip own building? still stops)
for(const b of buildings){
if(circleHitsRect(p.x, p.y, r, b)){ explode(p.x, p.y); return; }
}
// out of bounds sides
if(p.x < -20 || p.x > W+20){ explode(p.x, p.y); return; }
}
}
/* ---------- Turn / match flow ---------- */
function startMatch(){
seed = (Math.random()*1e9)|0;
generateCity();
placeGorillas();
players.forEach(p=>{ p.alive=true; p.reaction=0; });
projectile=null; particles=[]; scorch=null;
angle=45; power=50;
currentPlayer = Math.random()<0.5?0:1;
state='aiming';
setWind();
updateTurnHUD();
updateWindHUD();
hideOverlay();
playLaunch();
}
function endTurn(){
// wind shift between turns
if(CONFIG.windShifts) setWind();
currentPlayer = 1-currentPlayer;
angle=45; power=50;
updateTurnHUD();
state='aiming';
}
function onExplodeDone(){
const opp = players[1-currentPlayer];
if(!opp.alive){
// winner is current player (the one who fired)
const winner = currentPlayer;
score[winner]++;
updateTurnHUD();
state='result'; resultTimer=0;
showResult(winner);
playVictory();
} else {
endTurn();
}
}
/* ---------- Rendering ---------- */
function drawSky(){
const g = ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#1a2b4a'); g.addColorStop(0.55,'#2b3d5e'); g.addColorStop(1,'#3a2a44');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// sun/moon
ctx.fillStyle='rgba(255,240,200,0.9)';
ctx.beginPath(); ctx.arc(780,120,42,0,Math.PI*2); ctx.fill();
ctx.fillStyle='rgba(255,240,200,0.12)';
ctx.beginPath(); ctx.arc(780,120,70,0,Math.PI*2); ctx.fill();
// clouds
ctx.fillStyle='rgba(220,230,245,0.14)';
for(let i=0;i<4;i++){
const cx=(i*260+40)% (W+200), cy=70+i*40;
ctx.beginPath(); ctx.ellipse(cx,cy,60,18,0,0,Math.PI*2); ctx.fill();
}
}
function drawGround(){
ctx.fillStyle='#243040'; ctx.fillRect(0,GROUND,W,H-GROUND);
ctx.fillStyle='#35475c'; ctx.fillRect(0,GROUND,W,5);
}
function drawBuilding(b){
ctx.fillStyle=b.color;
ctx.fillRect(b.x, b.y, b.w, b.h);
// roof edge
ctx.fillStyle='rgba(255,255,255,0.08)';
ctx.fillRect(b.x, b.y, b.w, 5);
// windows
const cols = b.windows, rows = Math.max(2, Math.floor(b.h/44));
const pw = (b.w-14)/(cols);
const ph = 12;
ctx.fillStyle=b.windowColor;
for(let c=0;c<cols;c++){
for(let rr=0; rr<rows; rr++){
const wx = b.x + 8 + c*pw + 2;
const wy = b.y + 16 + rr*26;
if(wy < GROUND-10) ctx.fillRect(wx, wy, Math.max(3,pw-6), ph);
}
}
}
function drawGorilla(p){
if(!p.alive){
// defeated: slumped on ground
ctx.save();
ctx.globalAlpha = p.reaction>0?1:0.55;
ctx.fillStyle=p.color;
ctx.translate(p.x, GROUND-14);
ctx.fillRect(-14, -6, 28, 12);
ctx.beginPath(); ctx.arc(-8,-6,7,0,Math.PI*2); ctx.fill();
ctx.restore();
return;
}
const x=p.x, y=p.y;
ctx.save();
ctx.translate(x,y);
const facing = p.side==='L'?1:-1;
// body
ctx.fillStyle=p.color;
ctx.fillRect(-11,-30,22,26);
// head
ctx.beginPath(); ctx.arc(0,-36,9,0,Math.PI*2); ctx.fill();
// face
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.fillRect(-6,-40,12,8);
// eyes
ctx.fillStyle='#fff';
ctx.fillRect(-5,-38,3,3); ctx.fillRect(2,-38,3,3);
// arms
ctx.fillStyle=p.color;
ctx.fillRect(facing*10,-28,6,16);
// legs
ctx.fillRect(-9,-6,7,8); ctx.fillRect(2,-6,7,8);
ctx.restore();
}
function drawProjectile(){
if(!projectile) return;
const p=projectile;
// trail
for(let i=0;i<p.trail.length;i++){
const t=p.trail[i];
ctx.globalAlpha = i/p.trail.length*0.6;
ctx.fillStyle='#ffe14d';
ctx.beginPath(); ctx.arc(t.x,t.y, 3+(i/p.trail.length)*2, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
// banana
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(Math.atan2(p.vy,p.vx));
ctx.fillStyle='#ffe14d';
ctx.strokeStyle='#c9a820'; ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(-7,-3); ctx.quadraticCurveTo(4,-8,9,-1); ctx.quadraticCurveTo(3,-1, -7,3);
ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.restore();
}
function drawBlast(){
if(scorch){
const s=scorch;
// scorch marker
ctx.fillStyle='rgba(30,15,10,0.5)';
ctx.beginPath(); ctx.arc(s.x,s.y, s.max*0.9, 0, Math.PI*2); ctx.fill();
// explosion ring
const prog = 1 - (s.t/40);
ctx.strokeStyle='rgba(255,180,60,'+(0.9*prog)+')';
ctx.lineWidth = 6*prog+2;
ctx.beginPath(); ctx.arc(s.x,s.y, s.max*(1-prog*0.4),0,Math.PI*2); ctx.stroke();
ctx.fillStyle='rgba(255,230,120,'+(0.5*prog)+')';
ctx.beginPath(); ctx.arc(s.x,s.y, s.max*(1-prog*0.4)*0.6,0,Math.PI*2); ctx.fill();
}
for(const pt of particles){
ctx.globalAlpha = Math.max(0,pt.life);
ctx.fillStyle=pt.c;
ctx.fillRect(pt.x-2, pt.y-2, 4,4);
}
ctx.globalAlpha=1;
}
function drawAimGuide(){
if(state!=='aiming') return;
const p = players[currentPlayer];
const rad = angle*Math.PI/180;
const speed = CONFIG.powerMin + (power-CONFIG.powerMin)/(100-CONFIG.powerMin)*(CONFIG.powerMax-CONFIG.powerMin);
const dir = p.side==='L'?1:-1;
let vx=Math.cos(rad)*speed*dir, vy=-Math.sin(rad)*speed;
let x=p.x+dir*18, y=p.y-34;
ctx.strokeStyle='rgba(255,255,255,0.25)';
ctx.setLineDash([4,6]); ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x,y);
for(let i=0;i<60;i++){
vy += CONFIG.gravity*100/100*1; vx += wind*0.02;
x+=vx; y+=vy;
if(y>GROUND||x<0||x>W) break;
ctx.lineTo(x,y);
}
ctx.stroke(); ctx.setLineDash([]);
}
function render(){
drawSky();
buildings.forEach(drawBuilding);
drawGround();
drawBlast();
players.forEach(drawGorilla);
drawProjectile();
drawAimGuide();
}
/* ---------- Audio ---------- */
let actx=null;
function ac(){ if(!actx){ actx=new (window.AudioContext||window.webkitAudioContext)(); } return actx; }
function playLaunch(){
try{ const a=ac(); const o=a.createOscillator(), g=a.createGain();
o.type='square'; o.frequency.setValueAtTime(600,a.currentTime); o.frequency.exponentialRampToValueAtTime(180,a.currentTime+0.25);
g.gain.setValueAtTime(0.12,a.currentTime); g.gain.exponentialRampToValueAtTime(0.001,a.currentTime+0.25);
o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime+0.25); }catch(e){}
}
function playExplosion(){
try{ const a=ac(); const o=a.createOscillator(), g=a.createGain();
o.type='sawtooth'; o.frequency.setValueAtTime(160,a.currentTime); o.frequency.exponentialRampToValueAtTime(40,a.currentTime+0.5);
g.gain.setValueAtTime(0.25,a.currentTime); g.gain.exponentialRampToValueAtTime(0.001,a.currentTime+0.5);
o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime+0.5); }catch(e){}
}
function playVictory(){
try{ const a=ac(); const notes=[523,659,784,1046]; let t=a.currentTime;
notes.forEach((f,i)=>{ const o=a.createOscillator(), g=a.createGain(); o.type='square'; o.frequency.value=f;
g.gain.setValueAtTime(0.14,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.22); o.connect(g); g.connect(a.destination); o.start(t); o.stop(t+0.22); t+=0.16; }); }catch(e){}
}
/* ---------- Settings panel ---------- */
const SETTINGS = [
{ k:'blastRadius', label:'Blast Radius', min:20, max:120, step:1 },
{ k:'gravity', label:'Gravity Strength', min:0.02, max:2.0, step:0.005 },
{ k:'windMin', label:'Wind Min Speed', min:0, max:3, step:0.1 },
{ k:'windMax', label:'Wind Max Speed', min:0, max:5, step:0.1 },
{ k:'buildingCount', label:'Building Count', min:4, max:14, step:1 },
{ k:'buildingHeightMin', label:'Height Min', min:60, max:400, step:1 },
{ k:'buildingHeightMax', label:'Height Max', min:120, max:520, step:1 },
{ k:'powerMin', label:'Power Min', min:2, max:30, step:0.5 },
{ k:'powerMax', label:'Power Max', min:8, max:40, step:0.5 },
{ k:'splashFalloff', label:'Falloff Threshold', min:0, max:1, step:0.05 }
];
const BOOLS = [
{ k:'windShifts', label:'Wind Shifts Between Turns' },
{ k:'damageMode', label:'Damage Mode', type:'choice', options:['radius','falloff'] }
];
function toggleSettings(){
if(state==='flying') return;
if(el('overlay').style.display==='flex') { hideOverlay(); return; }
showSettings();
}
function showSettings(){
tuningActive = true;
const panel = el('panel');
let html = '<h1>⚙ SETTINGS — LIVE TUNING</h1><span class="livebadge">LIVE TUNING ACTIVE</span>';
html += '<table>';
SETTINGS.forEach(s=>{
html += '<tr><td>'+s.label+'</td><td class="num"><button data-dec="'+s.k+'">−</button></td>'+
'<td class="val" id="v_'+s.k+'">'+fmtNum(CONFIG[s.k])+'</td>'+
'<td class="num"><button data-inc="'+s.k+'">+</button></td></tr>';
});
html += '</table>';
BOOLS.forEach(s=>{
let cur;
if(s.type==='choice') cur = CONFIG[s.k];
else cur = CONFIG[s.k] ? 'On' : 'Off';
html += '<h2>'+s.label+'</h2>';
if(s.type==='choice'){
html += '<div class="rowbtns">';
s.options.forEach(o=>{ html += '<button data-choice="'+s.k+'" data-val="'+o+'" class="'+(cur===o?'':'')+'">'+o+'</button>'; });
html += '</div>';
} else {
html += '<div class="ctrl"><button data-toggle="'+s.k+'">'+cur+'</button></div>';
}
});
html += '<h2>Match</h2><div class="rowbtns">';
html += '<button id="regenBtn">Regenerate City</button>';
html += '<button id="rematchBtn">New Match</button>';
html += '<button id="resetBtn">Reset Defaults</button>';
html += '<button id="closeBtn">Close (T)</button>';
html += '</div>';
html += '<div class="legend">Edits apply immediately (or next launch). Wind range uses windMin..windMax. "Regenerate City" reshuffles the skyline for the current match; "New Match" starts a fresh round with new buildings.</div>';
panel.innerHTML = html;
el('overlay').style.display='flex';
panel.querySelectorAll('[data-inc]').forEach(b=> b.onclick=()=>{ changeSetting(b.getAttribute('data-inc'), +1, b); });
panel.querySelectorAll('[data-dec]').forEach(b=> b.onclick=()=>{ changeSetting(b.getAttribute('data-dec'), -1, b); });
panel.querySelectorAll('[data-toggle]').forEach(b=> b.onclick=()=>{ toggleBool(b.getAttribute('data-toggle'), b); });
panel.querySelectorAll('[data-choice]').forEach(b=> b.onclick=()=>{
CONFIG[b.getAttribute('data-choice')] = b.getAttribute('data-val');
showSettings();
});
el('regenBtn').onclick = ()=>{ generateCity(); placeGorillas(); projectile=null; particles=[]; scorch=null; state='aiming'; angle=45; power=50; updateTurnHUD(); };
el('rematchBtn').onclick = ()=>{ hideOverlay(); startMatch(); };
el('resetBtn').onclick = ()=>{ Object.assign(CONFIG, DEFAULTS); showSettings(); };
el('closeBtn').onclick = ()=>{ hideOverlay(); };
}
function fmtNum(v){ return Number(v).toFixed(v%1?2:0); }
function changeSetting(k, dir, btn){
const s = SETTINGS.find(x=>x.k===k);
const step = s.step;
if(dir>0) CONFIG[k] = Math.min(s.max, CONFIG[k]+step);
else CONFIG[k] = Math.max(s.min, CONFIG[k]-step);
CONFIG[k] = Math.round(CONFIG[k]/step)*step;
el('v_'+k).textContent = fmtNum(CONFIG[k]);
}
function toggleBool(k, btn){
if(CONFIG[k]==='radius'||CONFIG[k]==='falloff'){ /* skip */ }
CONFIG[k] = !CONFIG[k];
btn.textContent = CONFIG[k]?'On':'Off';
if(k==='damageMode'){ CONFIG.damageMode = CONFIG[k]?'radius':'falloff'; }
}
function showResult(winner){
const panel = el('panel');
const c = players[winner].color;
panel.innerHTML = '<div id="winbox">'+
'<h1>ROUND OVER</h1>'+
'<div class="big" style="color:'+c+'">'+players[winner].name+' WINS</div>'+
'<div>P1 '+score[0]+' : '+score[1]+' P2</div>'+
'<div class="legend" style="text-align:center;margin-top:16px">Press <kbd>Enter</kbd> or click to play another match</div>'+
'<div style="text-align:center;margin-top:12px"><button id="rematchWin">REMATCH</button> <button id="settingsWin">SETTINGS</button></div>'+
'</div>';
el('overlay').style.display='flex';
el('rematchWin').onclick = ()=>{ hideOverlay(); startMatch(); };
el('settingsWin').onclick = ()=>{ showSettings(); };
}
function hideOverlay(){ el('overlay').style.display='none'; }
/* ---------- Main loop ---------- */
let last=performance.now();
function loop(now){
const dt = Math.min(50, now-last); last=now;
aimTick();
if(state==='flying') stepPhysics();
else if(state==='exploding'){
resultTimer--;
if(scorch){ scorch.t++; }
for(const pt of particles){ pt.x+=pt.vx; pt.y+=pt.vy; pt.vy+=0.15; pt.vx*=0.98; pt.life-=0.02; }
particles = particles.filter(p=>p.life>0);
if(resultTimer<=0) onExplodeDone();
} else if(state==='result'){
for(const pt of particles){ pt.x+=pt.vx; pt.y+=pt.vy; pt.vy+=0.15; pt.vx*=0.98; pt.life-=0.015; }
particles = particles.filter(p=>p.life>0);
}
players.forEach(p=>{ if(p.reaction>0) p.reaction--; });
render();
requestAnimationFrame(loop);
}
/* ---------- Boot ---------- */
generateCity();
placeGorillas();
state='idle';
updateTurnHUD();
setWind();
render();
// Start screen overlay
(function startScreen(){
const panel = el('panel');
panel.innerHTML = '<div id="winbox">'+
'<h1 style="letter-spacing:6px">🦍 GORILLAZ 🦍</h1>'+
'<div class="big">ARTILLERY DUEL</div>'+
'<div class="legend" style="text-align:center">Two gorillas on rooftops. Hurl an exploding banana.<br>'+
'<kbd>←</kbd>/<kbd>→</kbd> angle · <kbd>↑</kbd>/<kbd>↓</kbd> power · <kbd>Space</kbd> fire · <kbd>T</kbd> settings</div>'+
'<div style="text-align:center;margin-top:16px"><button id="startBtn">START (Enter)</button></div>'+
'</div>';
el('overlay').style.display='flex';
el('startBtn').onclick = ()=>{ ac(); startMatch(); };
})();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"d4c255a7-2846-4633-87c2-8574c1b1fea1","tokensIn":5704460,"tokensOut":246240,"tokensTotal":5950700,"cost":0,"turns":114,"toolCalls":113,"failedToolCalls":1,"timestamp":"2026-09-04T23:33:54.997Z"} -->