Gorillaz
Deepseek v4.1 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 51,746 bytes · SHA-256 1b350475c5f6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GORILLAZ — Banana Bomb Artillery</title>
<style>
:root{
--p1:#ff5252; --p2:#4d9dff;
--ink:#e9e9f5; --panel:#12172a; --edge:#2b3255;
--accent:#ffd166; --live:#54e39a;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;min-height:100%;}
body{
background:
radial-gradient(1200px 600px at 50% -10%, #1a1f3d 0%, #0a0c17 60%, #06070d 100%);
color:var(--ink);
font-family:"Courier New", ui-monospace, SFMono-Regular, Menlo, monospace;
-webkit-font-smoothing:none;
overflow-x:hidden;
}
#app{
display:flex; flex-direction:column; align-items:center;
gap:9px; padding:10px 10px 26px;
}
.stage{
position:relative;
width:min(97vw, 1000px);
aspect-ratio:1000 / 625;
border:3px solid var(--edge);
border-radius:7px;
overflow:hidden;
background:#05060c;
box-shadow:0 0 0 3px #0c0f1c, 0 18px 50px rgba(0,0,0,.75), 0 0 60px rgba(80,110,255,.10);
flex:0 0 auto;
}
canvas{display:block; width:100%; height:100%; image-rendering:pixelated;}
.scan{
position:absolute; inset:0; pointer-events:none; z-index:6;
background:repeating-linear-gradient(to bottom, rgba(0,0,0,.20) 0 1px, transparent 1px 3px);
opacity:.55; mix-blend-mode:multiply;
}
.vig{position:absolute;inset:0;pointer-events:none;z-index:5;
background:radial-gradient(120% 100% at 50% 45%, transparent 55%, rgba(0,0,0,.55) 100%);}
/* ---------- HUD ---------- */
.hud-top{
position:absolute; top:0; left:0; right:0; z-index:4;
display:flex; justify-content:space-between; align-items:flex-start;
gap:8px; padding:7px 10px; pointer-events:none;
font-size:clamp(8.5px, 1.42vw, 14px); letter-spacing:.09em; font-weight:bold;
}
.chip{
background:rgba(8,10,20,.72); border:2px solid var(--edge); border-radius:4px;
padding:4px 9px; text-shadow:0 2px 0 rgba(0,0,0,.65); white-space:nowrap;
backdrop-filter:blur(2px);
}
.turn-chip{border-color:var(--p1); color:#fff; transition:border-color .2s, color .2s;}
.turn-chip.p2{border-color:var(--p2);}
.turn-chip small{display:block;font-size:.72em;opacity:.6;letter-spacing:.14em;font-weight:normal;}
.center-hud{display:flex;flex-direction:column;align-items:center;gap:4px;}
.wind-box{display:flex;align-items:center;gap:7px;}
.wind-arrow{
display:inline-block; font-size:1.35em; line-height:1;
color:var(--accent); transform-origin:50% 50%;
transition:transform .35s cubic-bezier(.2,1.4,.4,1), color .3s;
text-shadow:0 0 10px rgba(255,209,102,.55);
}
.wind-bar{width:80px;height:6px;background:#0a0d18;border:1px solid var(--edge);position:relative;}
.wind-bar i{position:absolute;top:0;bottom:0;background:var(--accent);opacity:.85;}
.score-box{display:flex;align-items:center;gap:8px;}
.score-box .s1{color:var(--p1);} .score-box .s2{color:var(--p2);}
.score-box b{font-size:1.45em;}
.ro-label{font-size:.74em;opacity:.62;letter-spacing:.18em;}
/* ---------- bottom bar ---------- */
.bar{width:min(97vw,1000px);display:flex;gap:8px;flex-wrap:wrap;align-items:stretch;}
.readout{
flex:1 1 190px; min-width:165px;
background:linear-gradient(180deg,#151b30,#0e1222);
border:2px solid var(--edge); border-radius:5px; padding:6px 10px 8px;
display:flex; flex-direction:column; gap:4px;
}
.ro-top{display:flex;justify-content:space-between;align-items:baseline;gap:6px;}
.ro-val{font-size:clamp(16px,2.4vw,24px);font-weight:bold;color:#fff;text-shadow:0 0 12px rgba(120,170,255,.35);}
.ro-val span{font-size:.55em;opacity:.65;}
.meter{height:9px;background:#080b14;border:1px solid var(--edge);overflow:hidden;}
.meter i{display:block;height:100%;width:0%;transition:width .08s linear;}
.meter.ang i{background:linear-gradient(90deg,#6d8cff,#c9d6ff);}
.meter.pow i{background:linear-gradient(90deg,#ffb347,#ff5252);}
.keys{font-size:10px;opacity:.5;letter-spacing:.06em;}
.btn{
appearance:none; font-family:inherit; font-weight:bold; letter-spacing:.1em;
background:linear-gradient(180deg,#1d2440,#111629); color:var(--ink);
border:2px solid var(--edge); border-radius:5px; padding:8px 12px; cursor:pointer;
font-size:clamp(9px,1.3vw,13px); text-transform:uppercase;
box-shadow:inset 0 1px 0 rgba(255,255,255,.07), 0 3px 0 #05070f;
transition:transform .05s, filter .15s;
user-select:none; touch-action:manipulation;
}
.btn:hover{filter:brightness(1.25);}
.btn:active{transform:translateY(2px); box-shadow:inset 0 1px 0 rgba(255,255,255,.07), 0 1px 0 #05070f;}
.btn:disabled{opacity:.35;cursor:not-allowed;transform:none;}
.btn.fire{border-color:#ff8a3d; color:#ffd9a8; background:linear-gradient(180deg,#3a2013,#20120b);}
.btn.tune{border-color:#4b7a6a; color:#b8f0d8;}
.pad{display:flex;gap:2px;}
.pad .btn{padding:8px 10px;min-width:40px;}
.hint{width:min(97vw,1000px);font-size:10.5px;opacity:.42;letter-spacing:.05em;text-align:center;line-height:1.5;}
/* ---------- banner ---------- */
.banner{
position:absolute; inset:0; z-index:8; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:12px; text-align:center;
background:radial-gradient(circle at 50% 45%, rgba(10,12,22,.82), rgba(4,5,10,.95));
backdrop-filter:blur(3px); padding:16px;
}
.banner.hidden{display:none;}
.banner h2{
margin:0; font-size:clamp(20px,4.4vw,44px); letter-spacing:.14em; color:#fff;
text-shadow:0 0 24px currentColor, 0 4px 0 rgba(0,0,0,.6);
}
.banner .sub{font-size:clamp(9px,1.5vw,14px); letter-spacing:.22em; opacity:.75;}
.banner .btns{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-top:6px;}
.banner .trophy{font-size:clamp(28px,6vw,58px);line-height:1;filter:drop-shadow(0 0 18px rgba(255,209,102,.6));}
.flash{animation:pop .5s cubic-bezier(.2,1.6,.4,1);}
@keyframes pop{0%{transform:scale(.7);opacity:0}100%{transform:scale(1);opacity:1}}
/* ---------- config panel ---------- */
#cfgPanel{
position:fixed; top:0; right:0; bottom:0; width:min(360px,94vw); z-index:20;
background:linear-gradient(180deg,#101526,#0a0d18);
border-left:3px solid var(--edge);
box-shadow:-14px 0 44px rgba(0,0,0,.75);
transform:translateX(102%); transition:transform .22s ease-out;
display:flex; flex-direction:column;
}
#cfgPanel.open{transform:translateX(0);}
.cfg-head{
padding:11px 13px; border-bottom:2px solid var(--edge); display:flex;
flex-direction:column; gap:7px; flex:0 0 auto; background:#0d1122;
}
.cfg-head .t{display:flex;justify-content:space-between;align-items:center;gap:8px;}
.cfg-head .t b{letter-spacing:.16em;font-size:13px;}
.badge{
font-size:9px; letter-spacing:.14em; padding:3px 7px; border-radius:3px;
border:1px solid #35406b; color:#7f8ac0; background:#0a0e1c; white-space:nowrap;
}
.badge.on{color:#06130d; background:var(--live); border-color:var(--live);
box-shadow:0 0 14px rgba(84,227,154,.6); animation:pulse 1.6s ease-in-out infinite;}
@keyframes pulse{0%,100%{box-shadow:0 0 10px rgba(84,227,154,.4)}50%{box-shadow:0 0 20px rgba(84,227,154,.85)}}
.cfg-body{overflow-y:auto; padding:10px 13px 30px; flex:1 1 auto;}
.cfg-group{margin-bottom:14px;}
.cfg-group h3{
margin:0 0 7px; font-size:10px; letter-spacing:.22em; color:#8f9bd6;
border-bottom:1px dashed #2b3255; padding-bottom:4px; font-weight:bold;
}
.cfg-item{
display:flex; flex-direction:column; gap:3px; padding:5px 6px 8px;
border-left:2px solid transparent; border-radius:3px; transition:background .15s;
}
.cfg-item.changed{background:rgba(84,227,154,.055); border-left-color:var(--live);}
.cfg-item.deferred.changed{background:rgba(255,209,102,.06); border-left-color:var(--accent);}
.cfg-item .top{display:flex;justify-content:space-between;align-items:center;gap:6px;font-size:10.5px;letter-spacing:.07em;}
.cfg-item .top .nm{opacity:.85;}
.cfg-item .top .nm i{font-style:normal;color:var(--accent);font-size:.86em;margin-left:4px;}
.cfg-item input[type=number]{
width:64px; font-family:inherit; font-size:11px; font-weight:bold; text-align:right;
background:#070a14; color:#fff; border:1px solid #2b3255; border-radius:3px; padding:2px 5px;
}
.cfg-item.changed input[type=number]{border-color:var(--live);color:var(--live);}
.cfg-item input[type=range]{width:100%; accent-color:#6d8cff; height:16px;}
.cfg-item.changed input[type=range]{accent-color:var(--live);}
.cfg-item select{
font-family:inherit;font-size:11px;background:#070a14;color:#fff;border:1px solid #2b3255;
border-radius:3px;padding:3px;width:100%;
}
.cfg-item .cur{font-size:9.5px;opacity:.5;letter-spacing:.06em;}
.switch{display:flex;align-items:center;gap:8px;font-size:10.5px;letter-spacing:.07em;padding:5px 6px;}
.switch input{width:16px;height:16px;accent-color:var(--live);}
.cfg-note{font-size:9.5px;opacity:.5;line-height:1.55;letter-spacing:.04em;
border:1px dashed #2b3255;padding:7px;border-radius:4px;margin-top:6px;}
.cfg-foot{flex:0 0 auto;padding:9px 13px;border-top:2px solid var(--edge);display:flex;gap:7px;background:#0d1122;}
.cfg-foot .btn{flex:1;text-align:center;padding:9px 6px;}
#cfgPanel::-webkit-scrollbar{width:9px;}
#cfgPanel::-webkit-scrollbar-thumb{background:#242c4c;}
#openCfg{align-self:stretch;}
@media (max-width:720px){
.keys{display:none;}
.hud-top{font-size:9px;padding:6px 6px 0;}
.chip{padding:3px 6px;}
.chip small{font-size:.68em;}
.wind-bar{width:44px;}
.btn{padding:7px 9px;}
}
</style>
</head>
<body>
<div id="app">
<div class="stage">
<canvas id="cv" width="1000" height="625"></canvas>
<div class="vig"></div>
<div class="scan"></div>
<div class="hud-top">
<div class="chip turn-chip" id="turnChip">
<small>NOW THROWING</small>PLAYER 1 TURN
</div>
<div class="center-hud">
<div class="chip wind-box">
<span>WIND</span>
<span class="wind-arrow" id="windArrow">➤</span>
<b id="windVal">0</b>
<span id="windUnit">MPH</span>
<div class="wind-bar"><i id="windBar"></i></div>
</div>
</div>
<div class="chip score-box">
<span class="s1">P1 <b id="s1">0</b></span>
<span style="opacity:.4">/</span>
<span class="s2"><b id="s2">0</b> P2</span>
</div>
</div>
<div class="banner hidden" id="banner">
<div class="trophy" id="bTrophy">🏆</div>
<h2 id="bTitle">PLAYER 1 WINS</h2>
<div class="sub" id="bSub">DIRECT HIT — ROUND 1</div>
<div class="btns" id="bBtns"></div>
</div>
</div>
<div class="bar">
<div class="readout">
<div class="ro-top"><span class="ro-label">ANGLE</span><span class="ro-val" id="roAngle">45<span>°</span></span></div>
<div class="meter ang"><i id="mAngle"></i></div>
<div class="keys">◀ ▶ or A/D — SHIFT = ×5</div>
</div>
<div class="readout">
<div class="ro-top"><span class="ro-label">POWER</span><span class="ro-val" id="roPower">70<span>%</span></span></div>
<div class="meter pow"><i id="mPower"></i></div>
<div class="keys">▲ ▼ or W/S — SHIFT = ×5</div>
</div>
<div class="readout" style="flex:0 1 auto;min-width:0;justify-content:center;gap:6px;">
<div class="ro-top" style="justify-content:center;"><span class="ro-label">FIRE</span></div>
<div style="display:flex;gap:6px;align-items:center;">
<button class="btn fire" id="fireBtn" style="padding:11px 18px;">THROW 🍌</button>
</div>
</div>
<div class="readout" style="flex:0 1 auto;min-width:0;justify-content:center;">
<div class="ro-top" style="justify-content:center;"><span class="ro-label">CTRL</span></div>
<div style="display:flex;gap:6px;align-items:center;justify-content:center;">
<div class="pad"><button class="btn" data-hold="angle:-1" >◀</button><button class="btn" data-hold="angle:1">▶</button></div>
<div class="pad"><button class="btn" data-hold="power:-1">▼</button><button class="btn" data-hold="power:1">▲</button></div>
</div>
</div>
<button class="btn tune" id="openCfg" style="flex:0 0 auto;padding:10px 14px;">⚙ TUNE</button>
</div>
<div class="hint">SPACE = THROW · C = LIVE TUNING PANEL · R = REMATCH · TWO PLAYERS, ONE KEYBOARD — SAME KEYS, TAKE TURNS</div>
</div>
<div id="cfgPanel">
<div class="cfg-head">
<div class="t">
<b>LIVE TUNING</b>
<span class="badge" id="liveBadge">STOCK VALUES</span>
</div>
<div style="font-size:9.5px;opacity:.55;letter-spacing:.07em;">
Edits take effect immediately — or at the next throw for layout.
</div>
</div>
<div class="cfg-body" id="cfgBody"></div>
<div class="cfg-foot">
<button class="btn" id="resetCfg">RESET ALL</button>
<button class="btn tune" id="closeCfg">CLOSE</button>
</div>
</div>
<script>
"use strict";
/* ============================================================
GORILLAZ — turn-based banana-bomb artillery
Player 1 (red) vs Player 2 (blue), local, one keyboard.
============================================================ */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 1000, H = 625, GROUND_Y = 578;
/* ---------------- LIVE CONFIGURATION LAYER ---------------- */
const DEFAULTS = {
blastRadius : 45, // px
falloff : 0, // % (extra partial-damage shell beyond blastRadius)
gravity : 320, // px/s^2
windMin : 0, // min wind magnitude
windMax : 18, // max wind magnitude
windShift : true, // may direction change between turns?
windMult : 4, // px/s^2 per wind unit
buildingCount: 11,
bMinH : 70,
bMaxH : 300,
powerMin : 10,
powerMax : 100,
velMult : 6.2, // px/s of launch speed per power unit
roundFormat : 'single'
};
const CFG = Object.assign({}, DEFAULTS);
const DEF_STR = JSON.stringify(DEFAULTS);
const deferredKeys = ['buildingCount','bMinH','bMaxH'];
let layoutDirty = false;
const SCHEMA = [
{ group:'EXPLOSION', items:[
{ k:'blastRadius', label:'Blast Radius', min:10, max:160, step:1, unit:'px',
note:'circular damage zone on impact' },
{ k:'falloff', label:'Damage Threshold', min:0, max:100, step:1, unit:'%',
note:'0% = hurl must land INSIDE the radius. Higher % adds a partial-damage shell that extends the kill radius.' }
]},
{ group:'PHYSICS', items:[
{ k:'gravity', label:'Gravity Strength', min:40, max:900, step:10, unit:'px/s²' },
{ k:'windMult', label:'Wind Force', min:0, max:14, step:0.5, unit:'×' },
{ k:'windMin', label:'Wind Range Min', min:0, max:60, step:1, unit:'mph' },
{ k:'windMax', label:'Wind Range Max', min:0, max:60, step:1, unit:'mph' },
{ k:'windShift', label:'Wind May Shift Each Turn', type:'bool' }
]},
{ group:'SKYLINE', deferred:true, items:[
{ k:'buildingCount', label:'Building Count', min:4, max:20, step:1, unit:'bldgs' },
{ k:'bMinH', label:'Building Height Min', min:20, max:400, step:5, unit:'px' },
{ k:'bMaxH', label:'Building Height Max', min:40, max:480, step:5, unit:'px' }
]},
{ group:'PROJECTILE', items:[
{ k:'powerMin', label:'Power Range Min', min:1, max:80, step:1, unit:'pts' },
{ k:'powerMax', label:'Power Range Max', min:20, max:150, step:1, unit:'pts' },
{ k:'velMult', label:'Velocity Scale', min:1, max:16, step:0.1, unit:'px/s per pt' }
]},
{ group:'MATCH', items:[
{ k:'roundFormat', label:'Session Format', type:'select',
options:[['single','Single Round'],['bo3','Best of 3'],['bo5','Best of 5']],
deferred:true }
]}
];
/* ---------------- AUDIO (tiny WebAudio synth) ---------------- */
let AC = null, windOsc = null, windGain = null;
function audio(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC = null; } } return AC; }
function beep(freq, dur, type, vol, slideTo){
const a = audio(); if(!a) return;
const o = a.createOscillator(), g = a.createGain();
o.type = type||'square'; o.frequency.setValueAtTime(freq, a.currentTime);
if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo), a.currentTime+dur);
g.gain.setValueAtTime(0.0001, a.currentTime);
g.gain.exponentialRampToValueAtTime(vol||0.12, a.currentTime+0.008);
g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime+dur);
o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime+dur+0.02);
}
function sfxLaunch(){ beep(160,0.22,'sawtooth',0.10,620); }
function sfxBoom(){
const a = audio(); if(!a) return;
const len = Math.floor(a.sampleRate*0.5);
const buf = a.createBuffer(1, len, a.sampleRate);
const d = buf.getChannelData(0);
for(let i=0;i<len;i++){ d[i] = (Math.random()*2-1) * Math.pow(1-i/len, 2.6); }
const s = a.createBufferSource(); s.buffer = buf;
const f = a.createBiquadFilter(); f.type='lowpass'; f.frequency.setValueAtTime(1400,a.currentTime);
f.frequency.exponentialRampToValueAtTime(120, a.currentTime+0.45);
const g = a.createGain(); g.gain.value = 0.32;
s.connect(f); f.connect(g); g.connect(a.destination); s.start();
beep(70,0.35,'triangle',0.16,28);
}
function sfxWin(){ [523,659,784,1047].forEach((f,i)=> setTimeout(()=>beep(f,0.16,'square',0.10), i*110)); }
function sfxLose(){ [392,330,262].forEach((f,i)=> setTimeout(()=>beep(f,0.22,'sawtooth',0.09), i*150)); }
function syncWindTone(){
const a = audio(); if(!a) return;
if(!windOsc){
windOsc = a.createOscillator(); windGain = a.createGain();
windOsc.type='sine'; windOsc.frequency.value=70;
windGain.gain.value = 0; windOsc.connect(windGain); windGain.connect(a.destination); windOsc.start();
}
const s = Math.abs(wind)/Math.max(1,CFG.windMax);
windGain.gain.setTargetAtTime(s*0.035, a.currentTime, 0.4);
windOsc.frequency.setTargetAtTime(55+s*60, a.currentTime, 0.4);
}
/* ---------------- STATE ---------------- */
let buildings = [];
let players = []; // two gorillas
let turn = 0; // 0 or 1
let wind = 0; // signed
let proj = null;
let phase = 'aim'; // aim | fly | boom | over
let effects = []; // explosions + particles
let decals = []; // scorch marks
let clouds = [];
let timer = 0;
let roundNo = 1;
let scores = [0,0];
let sessionWins = [0,0];
let matchOver = false;
let shake = 0;
let lastImpact = null;
let flashT = 0;
const P_COL = [
{ body:'#ff5252', dark:'#a02121', light:'#ff9d9d', name:'PLAYER 1', hex:'#ff5252' },
{ body:'#4d9dff', dark:'#1f4f96', light:'#a9d4ff', name:'PLAYER 2', hex:'#4d9dff' }
];
const B_COLS = ['#39406e','#4a4f8c','#2f3560','#54528f','#3d4a7d','#5b4470','#2c4a6e','#474b7a'];
/* ---------------- HELPERS ---------------- */
const rand = (a,b)=> a + Math.random()*(b-a);
const randi = (a,b)=> Math.floor(rand(a,b+1));
const pick = arr => arr[Math.floor(Math.random()*arr.length)];
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
/* ---------------- WORLD GENERATION ---------------- */
function genSkyline(){
buildings = [];
const n = clamp(Math.round(CFG.buildingCount), 3, 24);
const maxH = Math.max(CFG.bMinH + 20, CFG.bMaxH);
const minH = Math.min(CFG.bMinH, maxH - 20);
const wid = [], gaps = [];
let tw = 0, tg = 0;
for(let i=0;i<n;i++){ const w = rand(48,118); wid.push(w); tw += w; }
for(let i=0;i<n-1;i++){ const g = rand(5,18); gaps.push(g); tg += g; }
const k = (W - tg) / tw;
let x = 0;
for(let i=0;i<n;i++){
const bw = wid[i]*k;
const h = rand(minH, maxH);
buildings.push({
x: x, w: bw, top: GROUND_Y - h, h: h,
col: B_COLS[i % B_COLS.length],
lit: Math.random() < .5
});
x += bw + (gaps[i]||0);
}
}
function roofAt(cx){
for(const b of buildings){ if(cx >= b.x && cx <= b.x+b.w) return b.top; }
return GROUND_Y;
}
function placeGorillas(){
const leftPool = buildings.map((b,i)=>i).filter(i => buildings[i].x + buildings[i].w/2 < W*0.38);
const rightPool = buildings.map((b,i)=>i).filter(i => buildings[i].x + buildings[i].w/2 > W*0.62);
const L = leftPool.length ? leftPool : [0];
const R = rightPool.length ? rightPool : [buildings.length-1];
const minSep = Math.max(300, CFG.blastRadius*4.5);
let bi = pick(L), ci = pick(R), tries = 0;
while(tries++ < 60){
bi = pick(L); ci = pick(R);
const ax = buildings[bi].x + buildings[bi].w/2;
const bx = buildings[ci].x + buildings[ci].w/2;
if(bx - ax >= minSep) break;
}
const A = buildings[bi], B = buildings[ci];
const ax = clamp(A.x + A.w/2, 40, W-40);
const bx = clamp(B.x + B.w/2, 40, W-40);
players = [
{ id:0, x:ax, gy:A.top, dir: 1, angle:45, power:Math.round((CFG.powerMin+CFG.powerMax)/2),
dead:false, deadT:0, winT:-1, throwT:-1, blob:0, col:P_COL[0] },
{ id:1, x:bx, gy:B.top, dir:-1, angle:135, power:Math.round((CFG.powerMin+CFG.powerMax)/2),
dead:false, deadT:0, winT:-1, throwT:-1, blob:0, col:P_COL[1] }
];
if(A.top < B.top) players[0].id = 0;
}
function genClouds(){
clouds = [];
for(let i=0;i<6;i++){
clouds.push({ x: rand(0,W), y: rand(40,230), s: rand(.6,1.5), v: rand(3,11)*(Math.random()<.5?-1:1), a: rand(.10,.26) });
}
}
function newRound(keepScore){
if(!keepScore){ scores = [0,0]; roundNo = 1; }
else roundNo++;
layoutDirty = false;
genSkyline();
placeGorillas();
genClouds();
decals = []; effects = []; proj = null;
turn = Math.random() < .5 ? 0 : 1;
wind = rollWind();
phase = 'aim'; timer = 0; matchOver = false;
hideBanner();
syncWindTone();
refreshPanelState();
syncHUD();
}
function rollWind(){
const lo = Math.min(CFG.windMin, CFG.windMax), hi = Math.max(CFG.windMin, CFG.windMax);
const m = rand(lo, hi);
const sign = Math.random() < .5 ? -1 : 1;
return Math.round(m*sign*10)/10;
}
/* ---------------- TUNING PANEL ---------------- */
const cfgBody = document.getElementById('cfgBody');
const liveBadge = document.getElementById('liveBadge');
const inputsByKey = {};
function buildPanel(){
cfgBody.innerHTML = '';
SCHEMA.forEach(grp => {
const g = document.createElement('div'); g.className = 'cfg-group';
const h = document.createElement('h3'); h.textContent = grp.group + (grp.deferred ? ' · APPLIES NEXT ROUND' : '');
g.appendChild(h);
grp.items.forEach(it => {
const row = document.createElement('div');
row.className = 'cfg-item';
if(grp.deferred || it.deferred) row.classList.add('deferred');
if(it.type === 'bool'){
const sw = document.createElement('label'); sw.className = 'switch';
const cb = document.createElement('input'); cb.type = 'checkbox';
cb.checked = !!CFG[it.k];
cb.addEventListener('change', ()=>{ CFG[it.k] = cb.checked; onCfgChange(it.k); });
const sp = document.createElement('span'); sp.textContent = it.label;
sw.appendChild(cb); sw.appendChild(sp); row.appendChild(sw);
inputsByKey[it.k] = { type:'bool', el:cb, row:row };
} else if(it.type === 'select'){
const top = document.createElement('div'); top.className = 'top';
const nm = document.createElement('span'); nm.className = 'nm'; nm.textContent = it.label;
top.appendChild(nm); row.appendChild(top);
const sel = document.createElement('select');
it.options.forEach(o => { const op = document.createElement('option'); op.value = o[0]; op.textContent = o[1]; sel.appendChild(op); });
sel.value = CFG[it.k];
sel.addEventListener('change', ()=>{ CFG[it.k] = sel.value; onCfgChange(it.k); });
row.appendChild(sel);
inputsByKey[it.k] = { type:'select', el:sel, row:row };
} else {
const top = document.createElement('div'); top.className = 'top';
const nm = document.createElement('span'); nm.className = 'nm';
nm.innerHTML = it.label + (it.unit ? ' <i>'+it.unit+'</i>' : '');
const num = document.createElement('input'); num.type = 'number';
num.min = it.min; num.max = it.max; num.step = it.step; num.value = CFG[it.k];
top.appendChild(nm); top.appendChild(num); row.appendChild(top);
const rng = document.createElement('input'); rng.type = 'range';
rng.min = it.min; rng.max = it.max; rng.step = it.step; rng.value = CFG[it.k];
row.appendChild(rng);
const cur = document.createElement('div'); cur.className = 'cur';
cur.textContent = it.note || '';
if(it.note) row.appendChild(cur);
const apply = (v, from)=>{
v = clamp(parseFloat(v), it.min, it.max);
if(isNaN(v)) v = CFG[it.k];
CFG[it.k] = v;
if(from !== 'num') num.value = v;
if(from !== 'rng') rng.value = v;
if(it.k === 'windMin' && CFG.windMin > CFG.windMax){ CFG.windMax = CFG.windMin; }
if(it.k === 'windMax' && CFG.windMax < CFG.windMin){ CFG.windMin = CFG.windMax; }
if(it.k === 'bMinH' && CFG.bMinH > CFG.bMaxH){ CFG.bMaxH = CFG.bMinH + 20; }
if(it.k === 'bMaxH' && CFG.bMaxH < CFG.bMinH){ CFG.bMinH = Math.max(20, CFG.bMaxH - 20); }
if(it.k === 'powerMin' && CFG.powerMin > CFG.powerMax){ CFG.powerMax = CFG.powerMin + 10; }
if(it.k === 'powerMax' && CFG.powerMax < CFG.powerMin){ CFG.powerMin = Math.max(1, CFG.powerMax - 10); }
onCfgChange(it.k);
};
num.addEventListener('input', ()=> apply(num.value,'num'));
rng.addEventListener('input', ()=> apply(rng.value,'rng'));
inputsByKey[it.k] = { type:'num', num:num, rng:rng, row:row, max:it.max, min:it.min };
}
g.appendChild(row);
});
cfgBody.appendChild(g);
});
const note = document.createElement('div');
note.className = 'cfg-note';
note.textContent = 'TIP: blast radius, gravity, wind and velocity take effect on the very next throw. Skyline values regenerate the city at the start of the next round.';
cfgBody.appendChild(note);
}
function onCfgChange(key){
if(deferredKeys.indexOf(key) >= 0) layoutDirty = true;
if(key === 'windMult' || key === 'windMax' || key === 'windMin') syncWindTone();
if(key === 'windMin' || key === 'windMax' || key === 'powerMin' || key === 'powerMax'){
syncMirroredInputs();
players.forEach(p => p.power = clamp(p.power, CFG.powerMin, CFG.powerMax));
}
refreshPanelState();
syncHUD();
}
function syncMirroredInputs(){
['windMin','windMax','powerMin','powerMax','bMinH','bMaxH'].forEach(k=>{
const io = inputsByKey[k]; if(!io) return;
io.num.value = CFG[k]; io.rng.value = CFG[k];
});
}
function refreshPanelState(){
Object.keys(inputsByKey).forEach(k => {
const io = inputsByKey[k];
const changed = (k === 'roundFormat') ? (CFG[k] !== DEFAULTS[k])
: (typeof DEFAULTS[k] === 'boolean') ? (CFG[k] !== DEFAULTS[k])
: Math.abs(Number(CFG[k]) - Number(DEFAULTS[k])) > 1e-9;
io.row.classList.toggle('changed', changed);
});
const tuned = JSON.stringify(CFG) !== DEF_STR || layoutDirty;
liveBadge.classList.toggle('on', tuned);
liveBadge.textContent = tuned ? '● LIVE TUNING ACTIVE' : 'STOCK VALUES';
}
function resetConfig(){
Object.assign(CFG, DEFAULTS);
layoutDirty = true;
Object.keys(inputsByKey).forEach(k=>{
const io = inputsByKey[k];
if(io.type==='num'){ io.num.value = CFG[k]; io.rng.value = CFG[k]; }
else if(io.type==='bool'){ io.el.checked = CFG[k]; }
else if(io.type==='select'){ io.el.value = CFG[k]; }
});
players.forEach(p => p.power = clamp(p.power, CFG.powerMin, CFG.powerMax));
refreshPanelState(); syncHUD();
}
const cfgPanel = document.getElementById('cfgPanel');
function togglePanel(force){
const open = (force === undefined) ? !cfgPanel.classList.contains('open') : force;
cfgPanel.classList.toggle('open', open);
}
document.getElementById('openCfg').addEventListener('click', ()=>togglePanel());
document.getElementById('closeCfg').addEventListener('click', ()=>togglePanel(false));
document.getElementById('resetCfg').addEventListener('click', resetConfig);
/* ---------------- HUD SYNC ---------------- */
const turnChip = document.getElementById('turnChip');
const windArrow = document.getElementById('windArrow');
const windVal = document.getElementById('windVal');
const windBar = document.getElementById('windBar');
const s1El = document.getElementById('s1'), s2El = document.getElementById('s2');
const roAngle = document.getElementById('roAngle'), roPower = document.getElementById('roPower');
const mAngle = document.getElementById('mAngle'), mPower = document.getElementById('mPower');
const _hud = {};
function setTxt(el, k, v){ if(_hud[k] !== v){ _hud[k] = v; el.textContent = v; } }
function setHTML(el, k, v){ if(_hud[k] !== v){ _hud[k] = v; el.innerHTML = v; } }
function setStyle(el, k, prop, v){ if(_hud[k] !== v){ _hud[k] = v; el.style[prop] = v; } }
function syncHUD(){
if(!players.length) return;
const p = players[turn];
turnChip.className = 'chip turn-chip' + (turn === 1 ? ' p2' : '');
setStyle(turnChip, 'tc', 'color', turn === 1 ? '#bcdcff' : '#ffd2d2');
setHTML(turnChip, 'tcH', '<small>'+(phase==='fly'?'IN FLIGHT':'NOW THROWING')+'</small>' +
(matchOver ? 'MATCH OVER' : P_COL[turn].name + ' TURN'));
const sgn = wind > 0 ? 1 : -1;
const mag = Math.abs(wind);
const s = mag / Math.max(1, Math.max(CFG.windMin, CFG.windMax));
setTxt(windVal, 'wv', mag.toFixed(1));
setStyle(windArrow, 'wa', 'transform', 'rotate(' + (sgn > 0 ? -12 * s : 180 + 12 * s) + 'deg)');
const wcol = mag < 0.6 ? '#5d6a92' : (mag > CFG.windMax * 0.7 ? '#ff5252' : '#ffd166');
setStyle(windArrow, 'wac', 'color', wcol);
const half = s * 50;
setStyle(windBar, 'wb', 'left', (sgn > 0 ? 50 : 50 - half) + '%');
setStyle(windBar, 'wbw', 'width', half + '%');
setStyle(windBar, 'wbg', 'background', wcol);
setTxt(s1El, 'sc1', scores[0]); setTxt(s2El, 'sc2', scores[1]);
const p2 = players[turn];
setHTML(roAngle, 'ra', Math.round(p2.angle) + '<span>°</span>');
setHTML(roPower, 'rp', Math.round(p2.power) + '<span>/' + CFG.powerMax + '</span>');
setStyle(mAngle, 'ma', 'width', (p2.angle / 180 * 100) + '%');
const span = Math.max(1, CFG.powerMax - CFG.powerMin);
setStyle(mPower, 'mp', 'width', clamp((p2.power - CFG.powerMin) / span * 100, 0, 100) + '%');
const dis = (phase !== 'aim' || matchOver);
if(_hud.fireDis !== dis){ _hud.fireDis = dis; document.getElementById('fireBtn').disabled = dis; }
}
/* ---------------- INPUT ---------------- */
function adjust(kind, dir, big){
if(phase !== 'aim' || matchOver || !players.length) return;
const p = players[turn];
const step = big ? 5 : 1;
if(kind === 'angle'){ p.angle = clamp(p.angle + dir*step, 0, 180); }
else { p.power = clamp(p.power + dir*(big?5:1), CFG.powerMin, CFG.powerMax); }
syncHUD();
}
document.addEventListener('keydown', e=>{
if(AC && AC.state === 'suspended') AC.resume();
const k = e.key, low = k.toLowerCase();
if(k === ' ' || k === 'Enter'){ e.preventDefault(); fireBtn_click(); return; }
if(low === 'c'){ e.preventDefault(); togglePanel(); return; }
if(low === 'r' && phase === 'over'){ e.preventDefault(); nextRound(); return; }
const big = e.shiftKey;
if(k === 'ArrowLeft' || low === 'a'){ e.preventDefault(); adjust('angle', -1, big); return; }
if(k === 'ArrowRight' || low === 'd'){ e.preventDefault(); adjust('angle', 1, big); return; }
if(k === 'ArrowUp' || low === 'w'){ e.preventDefault(); adjust('power', 1, big); return; }
if(k === 'ArrowDown' || low === 's'){ e.preventDefault(); adjust('power', -1, big); return; }
});
/* hold-to-repeat pad buttons */
document.querySelectorAll('[data-hold]').forEach(btn=>{
const [kind, d] = btn.dataset.hold.split(':');
let iv = null, to = null;
const start = e=>{
e.preventDefault();
if(AC && AC.state==='suspended') AC.resume();
adjust(kind, parseInt(d,10), false);
to = setTimeout(()=>{ iv = setInterval(()=>adjust(kind, parseInt(d,10), false), 55); }, 320);
};
const stop = ()=>{ clearTimeout(to); clearInterval(iv); iv=null; to=null; };
btn.addEventListener('pointerdown', start);
['pointerup','pointerleave','pointercancel'].forEach(ev=>btn.addEventListener(ev, stop));
});
function fireBtn_click(){ fire(); }
document.getElementById('fireBtn').addEventListener('click', ()=>{ if(AC&&AC.state==='suspended')AC.resume(); fire(); });
/* ---------------- BANNER ---------------- */
const bannerEl = document.getElementById('banner');
const bTitle = document.getElementById('bTitle'), bSub = document.getElementById('bSub');
const bBtns = document.getElementById('bBtns'), bTrophy = document.getElementById('bTrophy');
function hideBanner(){ bannerEl.classList.add('hidden'); }
function showBanner(trophy, title, sub, color, buttons){
bTrophy.textContent = trophy;
bTitle.textContent = title;
bTitle.style.color = color;
bSub.textContent = sub;
bBtns.innerHTML = '';
buttons.forEach(bt=>{
const el = document.createElement('button');
el.className = 'btn ' + (bt.cls || '');
el.textContent = bt.label;
el.addEventListener('click', bt.fn);
bBtns.appendChild(el);
});
bannerEl.classList.remove('hidden');
bannerEl.classList.remove('flash'); void bannerEl.offsetWidth; bannerEl.classList.add('flash');
}
function nextRound(){
newRound(true);
syncHUD();
}
/* ---------------- FIRING & PHYSICS ---------------- */
function muzzleOf(p){ return { x: p.x + p.dir*15, y: p.gy - 33 }; }
function fire(){
if(phase !== 'aim' || matchOver || !players.length) return;
const p = players[turn];
const rad = p.angle * Math.PI/180;
const sp = p.power * CFG.velMult;
const m = muzzleOf(p);
proj = {
x:m.x, y:m.y, vx:Math.cos(rad)*sp, vy:-Math.sin(rad)*sp,
trail:[], t:0, owner:p
};
p.throwT = 0;
phase = 'fly';
syncHUD();
sfxLaunch();
}
function stepProj(dt){
const ax = CFG.windMult * wind;
proj.vx += ax * dt;
proj.vy += CFG.gravity * dt;
const nx = proj.x + proj.vx*dt;
const ny = proj.y + proj.vy*dt;
const hit = castHit(proj.x, proj.y, nx, ny);
if(hit){ proj.x = hit.x; proj.y = hit.y; impact(hit); return; }
proj.x = nx; proj.y = ny; proj.t += dt;
proj.trail.push({x:proj.x, y:proj.y});
if(proj.trail.length > 30) proj.trail.shift();
if(proj.x < -300 || proj.x > W+300 || proj.y > H+500){
proj = null; phase='boom'; timer = 0.35; lastImpact = null;
}
}
function castHit(x0,y0,x1,y1){
const dx = x1-x0, dy = y1-y0;
const dist = Math.hypot(dx,dy);
const steps = Math.max(1, Math.ceil(dist/2.2));
for(let i=1;i<=steps;i++){
const t = i/steps, x = x0+dx*t, y = y0+dy*t;
if(y >= GROUND_Y) return {x:x, y:GROUND_Y, type:'ground'};
for(let b=0;b<buildings.length;b++){
const bd = buildings[b];
if(x >= bd.x && x <= bd.x+bd.w && y >= bd.top) return {x:x, y:y, type:'building'};
}
for(let pi=0; pi<players.length; pi++){
const p = players[pi];
if(p.dead) continue;
if(proj && p === proj.owner && proj.t < 0.14) continue; // clear our own muzzle
if(x >= p.x-17 && x <= p.x+17 && y >= p.gy-44 && y <= p.gy){
return {x:x, y:y, type:'gorilla', who:p};
}
}
}
return null;
}
function impact(hit){
phase = 'boom'; timer = 1.2;
lastImpact = hit;
const R = CFG.blastRadius;
effects.push({kind:'blast', x:hit.x, y:hit.y, t:0, life:0.65, R:R});
effects.push({kind:'dust', x:hit.x, y:hit.y, t:0, life:0.95, R:R*1.9});
for(let i=0;i<34;i++){
const a = rand(0, Math.PI*2), s = rand(50, R*6);
effects.push({kind:'p', x:hit.x, y:hit.y, vx:Math.cos(a)*s, vy:Math.sin(a)*s*0.8 - 60,
t:0, life:rand(.4,1.15), r:rand(1.5,4.5),
col:pick(['#ffd166','#ff8a3d','#ff5252','#fff3c4','#b9b9cc'])});
}
decals.push({x:hit.x, y:hit.y, r:Math.max(10, R*0.55)});
if(decals.length > 26) decals.shift();
shake = 14; flashT = 1;
sfxBoom();
const killR = R * (1 + CFG.falloff/100);
players.forEach(p=>{
if(p.dead) return;
const d = Math.hypot(p.x - hit.x, (p.gy-22) - hit.y);
if(d <= killR || hit.who === p){
p.dead = true; p.deadT = 0;
p.kill = (hit.who === p) ? 'DIRECT HIT' : 'BLAST';
p.selfHit = (hit.who === p);
}
});
}
let carry = 0;
const FIXED = 1/480;
function flyStep(dt){
carry += dt;
let guard = 0;
while(carry >= FIXED && guard++ < 900){
carry -= FIXED;
stepProj(FIXED);
if(phase !== 'fly') break;
}
if(carry > 0.4) carry = 0;
}
/* ---------------- TURN / ROUND RESOLUTION ---------------- */
function resolve(){
const a = players[turn], b = players[1-turn];
if(a.dead || b.dead){
if(a.dead && b.dead) roundWin(-1);
else if(b.dead) roundWin(turn);
else roundWin(1-turn, true);
} else {
nextTurn();
}
}
function nextTurn(){
turn = 1 - turn;
if(CFG.windShift) wind = rollWind();
syncWindTone();
phase = 'aim'; proj = null; carry = 0; lastImpact = null;
players.forEach(p=>{ if(!p.dead) p.throwT = -1; });
players[turn].power = clamp(players[turn].power, CFG.powerMin, CFG.powerMax);
players[turn].throwT = -1;
syncHUD();
}
function roundWin(wIdx, suicide){
phase = 'over';
const target = CFG.roundFormat === 'bo3' ? 2 : CFG.roundFormat === 'bo5' ? 3 : 1;
let color = '#ffffff';
if(wIdx >= 0){ scores[wIdx]++; players[wIdx].winT = 0; color = P_COL[wIdx].hex; }
if(wIdx >= 0) sfxWin(); else sfxLose();
syncHUD();
const done = (wIdx >= 0 && scores[wIdx] >= target);
matchOver = done;
if(done && wIdx >= 0) sessionWins[wIdx]++;
const btns = [];
const fmtTxt = CFG.roundFormat === 'bo3' ? 'BEST OF 3' : CFG.roundFormat === 'bo5' ? 'BEST OF 5' : 'SINGLE ROUND';
if(done){
showBanner('🏆', P_COL[wIdx].name + ' WINS THE MATCH',
'MATCH ' + scores[0] + ' — ' + scores[1] + ' · ' + fmtTxt +
' · SESSION ' + sessionWins[0] + ' — ' + sessionWins[1],
color,
[{label:'NEW MATCH', cls:'fire', fn:()=>{ newRound(false); syncHUD(); }}]);
} else if(wIdx < 0){
showBanner('💥', 'DOUBLE KO — DRAW',
'BOTH GORILLAS DOWN · ROUND ' + roundNo,
'#ffd166',
[{label:'NEXT ROUND', cls:'fire', fn:nextRound}]);
} else {
showBanner('🍌', P_COL[wIdx].name + ' WINS THE ROUND',
(players[wIdx].kill || 'BLAST') + ' · ROUND ' + roundNo + ' · ' + fmtTxt + ' ' + scores[0] + '—' + scores[1],
color,
[{label:'NEXT ROUND', cls:'fire', fn:nextRound},
{label:'RESET SCORES', fn:()=>{ scores=[0,0]; roundNo=1; newRound(true); syncHUD(); }}]);
}
syncHUD();
}
/* ---------------- UPDATE ---------------- */
function update(dt){
if(shake > 0) shake = Math.max(0, shake - dt*58);
if(flashT > 0) flashT = Math.max(0, flashT - dt*3.2);
clouds.forEach(c=>{
c.x += (c.v + wind*0.35) * dt * c.s;
if(c.x < -180) c.x = W + 150;
if(c.x > W + 180) c.x = -150;
});
players.forEach(p=>{
if(p.throwT >= 0){ p.throwT += dt; if(p.throwT > 0.95) p.throwT = -1; }
if(p.dead) p.deadT += dt;
if(p.winT >= 0) p.winT += dt;
});
effects.forEach(e=>{
e.t += dt;
if(e.kind === 'p'){ e.vy += CFG.gravity*0.55*dt; e.x += e.vx*dt; e.y += e.vy*dt; }
});
effects = effects.filter(e=> e.t < e.life);
if(phase === 'fly' && proj) flyStep(dt);
else if(phase === 'boom'){ timer -= dt; if(timer <= 0) resolve(); }
}
/* ---------------- RENDER ---------------- */
function drawSky(){
const g = ctx.createLinearGradient(0,0,0,GROUND_Y);
g.addColorStop(0, '#120c2e');
g.addColorStop(0.42,'#33235f');
g.addColorStop(0.72,'#7a3f77');
g.addColorStop(0.88,'#d3607a');
g.addColorStop(1, '#ffa15c');
ctx.fillStyle = g; ctx.fillRect(0,0,W,GROUND_Y+2);
}
function drawSun(){
const x = 812, y = 168, r = 46;
const gg = ctx.createRadialGradient(x,y,4,x,y,r*3.2);
gg.addColorStop(0,'rgba(255,220,150,.42)');
gg.addColorStop(0.35,'rgba(255,140,90,.16)');
gg.addColorStop(1,'rgba(255,120,80,0)');
ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(x,y,r*3.2,0,7); ctx.fill();
const g2 = ctx.createLinearGradient(0,y-r,0,y+r);
g2.addColorStop(0,'#fff2b8'); g2.addColorStop(.55,'#ffca63'); g2.addColorStop(1,'#ff7a4d');
ctx.fillStyle = g2; ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fill();
ctx.fillStyle = 'rgba(20,10,40,.20)';
for(let i=0;i<4;i++){ ctx.fillRect(x-r, y-8+i*15, r*2, 3+i); }
}
function drawClouds(){
clouds.forEach(c=>{
ctx.save(); ctx.translate(c.x, c.y); ctx.scale(c.s, c.s);
ctx.fillStyle = 'rgba(255,190,160,'+c.a+')';
ctx.beginPath();
ctx.arc(0,0,26,0,7); ctx.arc(30,-8,20,0,7); ctx.arc(-30,4,18,0,7); ctx.arc(8,10,17,0,7);
ctx.fill(); ctx.restore();
});
}
let far = [];
function genFar(){
far = [];
let x = -30;
while(x < W+40){
const w = rand(60,150), h = rand(90,300);
far.push({x:x, w:w, h:h, a: rand(.16,.34)});
x += w + rand(4,26);
}
}
function drawFar(){
far.forEach((b,i)=>{
ctx.fillStyle = 'rgba(38,28,72,'+b.a+')';
ctx.fillRect(b.x, GROUND_Y-b.h, b.w, b.h);
ctx.fillStyle = 'rgba(255,200,150,'+(b.a*0.30)+')';
for(let yy = GROUND_Y-b.h+14; yy < GROUND_Y-10; yy += 20){
for(let xx = b.x+8; xx < b.x+b.w-8; xx += 16){
if(hsh(i,xx,yy) > 0.62) ctx.fillRect(xx, yy, 5, 7);
}
}
});
}
function hsh(a,b,c){ const v = Math.sin(a*12.9898 + b*78.233 + c*37.719) * 43758.5453; return v - Math.floor(v); }
function drawBuildings(){
buildings.forEach((b,i)=>{
ctx.fillStyle = b.col;
ctx.fillRect(b.x, b.top, b.w, GROUND_Y - b.top);
// side shading
ctx.fillStyle = 'rgba(0,0,0,.22)';
ctx.fillRect(b.x + b.w - Math.min(9, b.w*0.18), b.top, Math.min(9, b.w*0.18), GROUND_Y-b.top);
ctx.fillStyle = 'rgba(255,255,255,.10)';
ctx.fillRect(b.x, b.top, Math.min(6, b.w*0.14), GROUND_Y-b.top);
// roof cap
ctx.fillStyle = 'rgba(0,0,0,.35)';
ctx.fillRect(b.x, b.top, b.w, 4);
ctx.fillStyle = b.lit ? '#ffd166' : '#9fb0d8';
ctx.fillRect(b.x, b.top, b.w, 2);
// windows
const cols = Math.max(1, Math.floor(b.w/15));
const rows = Math.max(1, Math.floor((GROUND_Y-b.top-16)/19));
const padX = (b.w - cols*10) / (cols+1);
for(let c=0;c<cols;c++){
for(let r=0;r<rows;r++){
const wx = b.x + padX + c*(10+padX);
const wy = b.top + 12 + r*19;
if(wy > GROUND_Y-10) continue;
const h = hsh(i, c, r);
if(h > 0.72){ ctx.fillStyle = 'rgba(255,214,102,.92)'; }
else if(h > 0.42){ ctx.fillStyle = 'rgba(150,175,225,.30)'; }
else { ctx.fillStyle = 'rgba(10,12,28,.42)'; }
ctx.fillRect(wx, wy, 10, 11);
}
}
});
// ground
const g = ctx.createLinearGradient(0,GROUND_Y,0,H);
g.addColorStop(0,'#241b3a'); g.addColorStop(1,'#0d0a17');
ctx.fillStyle = g; ctx.fillRect(0, GROUND_Y, W, H-GROUND_Y);
ctx.fillStyle = 'rgba(255,180,120,.30)'; ctx.fillRect(0, GROUND_Y, W, 2);
}
function drawDecals(){
decals.forEach(d=>{
const g = ctx.createRadialGradient(d.x,d.y,1,d.x,d.y,d.r);
g.addColorStop(0,'rgba(12,8,18,.62)');
g.addColorStop(0.6,'rgba(20,12,26,.34)');
g.addColorStop(1,'rgba(24,14,30,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(d.x,d.y,d.r,0,7); ctx.fill();
});
}
function drawGorilla(p){
const s = 2;
ctx.save();
ctx.translate(p.x, p.gy);
// shadow
ctx.fillStyle = 'rgba(0,0,0,.35)';
ctx.beginPath(); ctx.ellipse(0, 0, 17*s/2*1.25, 4, 0, 0, 7); ctx.fill();
if(p.dead){
const t = Math.min(1, p.deadT/0.45);
ctx.rotate(p.dir * t * Math.PI/2 * 0.96);
ctx.translate(0, -1);
} else if(p.winT >= 0){
const hop = Math.abs(Math.sin(p.winT*6.5)) * 9;
ctx.translate(0, -hop);
}
ctx.scale(p.dir, 1);
const C = p.col;
const R = (x,yT,w,h,c)=>{ ctx.fillStyle = c; ctx.fillRect(x*s, -(yT+h)*s, w*s, h*s); };
// legs
R(-8, 0, 6, 11, C.dark);
R(2, 0, 6, 11, C.dark);
// torso
R(-9, 9, 18, 18, C.body);
R(-9, 9, 18, 4, C.light);
// head
R(-8, 27, 16, 15, C.body);
R(-11, 32, 4, 6, C.dark);
R(7, 32, 4, 6, C.dark);
// muzzle
R(-5, 28, 10, 8, C.light);
// eyes
if(p.dead){
ctx.strokeStyle = '#100c16'; ctx.lineWidth = 2*s;
ctx.beginPath();
ctx.moveTo(-7*s, -38*s); ctx.lineTo(-2*s, -33*s);
ctx.moveTo(-2*s, -38*s); ctx.lineTo(-7*s, -33*s);
ctx.moveTo(2*s, -38*s); ctx.lineTo(7*s, -33*s);
ctx.moveTo(7*s, -38*s); ctx.lineTo(2*s, -33*s);
ctx.stroke();
} else {
R(-6, 35.5, 3, 3, '#0d0d12');
R(2, 35.5, 3, 3, '#0d0d12');
}
R(-7, 39, 14, 2, C.dark);
// arms
let armA = -0.18, armB = -0.18;
if(p.winT >= 0){ armA = 2.5; armB = 2.5; }
else if(p.throwT >= 0){
const t = p.throwT / 0.95;
if(t < 0.4) { const u = t/0.4; armA = -0.18 + u*2.6; armB = -0.18 + u*1.2; }
else if(t < 0.65) { const u = (t-0.4)/0.25; armA = 2.42 - u*3.1; armB = 1.02 - u*1.6; }
else { armA = -0.68; armB = -0.58; }
}
const arm = (shoulderX, ang)=>{
ctx.save();
ctx.translate(shoulderX*s, -26*s);
ctx.rotate(-ang);
ctx.fillStyle = C.dark;
ctx.fillRect(-3*s, 0, 6*s, 17*s);
ctx.fillStyle = C.body;
ctx.fillRect(-3*s, 12*s, 6*s, 5*s);
ctx.restore();
};
arm(0, armA);
arm(6, armB);
ctx.restore();
}
function drawAim(){
if(phase !== 'aim' || matchOver || !players.length) return;
const p = players[turn];
const m = muzzleOf(p);
const rad = p.angle*Math.PI/180;
const span = Math.max(1, CFG.powerMax - CFG.powerMin);
const pw = clamp((p.power - CFG.powerMin)/span, 0, 1);
const len = 26 + pw*56;
const ex = m.x + Math.cos(rad)*len, ey = m.y - Math.sin(rad)*len;
ctx.save();
ctx.strokeStyle = p.col.hex; ctx.lineWidth = 3; ctx.globalAlpha = 0.85;
ctx.setLineDash([6,6]); ctx.lineDashOffset = -(performance.now()/60)%12;
ctx.beginPath(); ctx.moveTo(m.x, m.y); ctx.lineTo(ex, ey); ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
ctx.fillStyle = p.col.hex;
ctx.beginPath();
ctx.arc(ex, ey, 4, 0, 7); ctx.fill();
// power ticks
for(let i=1;i<=4;i++){
const t = i/5;
const tx = m.x + Math.cos(rad)*(20+t*54), ty = m.y - Math.sin(rad)*(20+t*54);
ctx.globalAlpha = pw > t ? 0.9 : 0.22;
ctx.fillRect(tx-2, ty-2, 4, 4);
}
ctx.restore();
}
function drawTrail(){
if(!proj) return;
const p = proj.owner;
proj.trail.forEach((pt,i)=>{
const a = (i/proj.trail.length)*0.55;
ctx.fillStyle = 'rgba(255,214,102,'+a+')';
const r = 1 + (i/proj.trail.length)*3.2;
ctx.beginPath(); ctx.arc(pt.x, pt.y, r, 0, 7); ctx.fill();
});
}
function drawProjectile(){
if(!proj) return;
const ang = Math.atan2(proj.vy, proj.vx);
ctx.save();
ctx.translate(proj.x, proj.y);
ctx.rotate(ang);
// glow
const g = ctx.createRadialGradient(0,0,1,0,0,16);
g.addColorStop(0,'rgba(255,220,120,.55)'); g.addColorStop(1,'rgba(255,200,80,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,0,16,0,7); ctx.fill();
// banana body
ctx.fillStyle = '#ffd84d';
ctx.beginPath();
ctx.moveTo(-11, 2.5);
ctx.quadraticCurveTo(0, -7.5, 11, 0.5);
ctx.quadraticCurveTo(0, 3.6, -11, 2.5);
ctx.fill();
ctx.fillStyle = '#ffefa8';
ctx.beginPath();
ctx.moveTo(-8, 0.4); ctx.quadraticCurveTo(0,-4.2, 8, -0.6);
ctx.quadraticCurveTo(0, 1.6, -8, 0.4); ctx.fill();
ctx.fillStyle = '#7a4a12';
ctx.fillRect(-12.5, 1.2, 3.5, 3.5);
ctx.fillRect(9.5, -1.6, 3.5, 3.5);
ctx.restore();
}
function drawEffects(){
effects.forEach(e=>{
const k = e.t / e.life;
if(e.kind === 'blast'){
const r = e.R * (0.35 + k*0.85);
ctx.globalAlpha = (1-k)*0.95;
const g = ctx.createRadialGradient(e.x,e.y,1,e.x,e.y,r*1.15);
g.addColorStop(0, 'rgba(255,255,235,1)');
g.addColorStop(0.32,'rgba(255,214,102,.95)');
g.addColorStop(0.62,'rgba(255,120,60,.72)');
g.addColorStop(1, 'rgba(180,40,40,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(e.x,e.y,r*1.15,0,7); ctx.fill();
ctx.globalAlpha = (1-k)*0.9;
ctx.strokeStyle = 'rgba(255,255,240,.9)';
ctx.lineWidth = Math.max(1, 4*(1-k));
ctx.beginPath(); ctx.arc(e.x,e.y,r,0,7); ctx.stroke();
} else if(e.kind === 'dust'){
const r = e.R*(0.25 + k*0.9);
ctx.globalAlpha = (1-k)*0.32;
ctx.strokeStyle = '#d8c8b0'; ctx.lineWidth = 5*(1-k)+1;
ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, 7); ctx.stroke();
} else {
ctx.globalAlpha = Math.max(0, 1-k);
ctx.fillStyle = e.col;
ctx.fillRect(e.x-e.r, e.y-e.r, e.r*2, e.r*2);
}
});
ctx.globalAlpha = 1;
}
function render(){
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,W,H);
ctx.save();
if(shake > 0.4) ctx.translate(rand(-shake,shake)*0.45, rand(-shake,shake)*0.45);
drawSky(); drawSun(); drawClouds(); drawFar(); drawBuildings(); drawDecals();
players.forEach(drawGorilla);
drawTrail(); drawProjectile();
ctx.restore();
drawEffects();
drawAim();
if(flashT > 0){
ctx.fillStyle = 'rgba(255,240,210,' + (flashT*0.38) + ')';
ctx.fillRect(0,0,W,H);
}
// scanline-ish frame
ctx.strokeStyle = 'rgba(120,140,255,.10)'; ctx.lineWidth = 2;
ctx.strokeRect(1,1,W-2,H-2);
}
/* ---------------- LOOP ---------------- */
let last = performance.now();
let lastFrame = performance.now();
function frame(t){
let dt = (t - last)/1000; last = t;
if(dt > 0.06) dt = 0.06;
if(dt < 0) dt = 0;
lastFrame = performance.now();
update(dt);
render();
syncHUD();
}
function rafLoop(t){ frame(t); requestAnimationFrame(rafLoop); }
/* watchdog: keeps the simulation advancing if rAF is throttled (hidden tab / headless) */
setInterval(()=>{ if(performance.now() - lastFrame > 220) frame(performance.now()); }, 60);
/* ---------------- BOOT ---------------- */
buildPanel();
newRound(false);
genFar();
refreshPanelState();
syncHUD();
requestAnimationFrame(frame);
/* expose for debugging / automated checks */
function simShot(angle, power, who){
const p = players[who === undefined ? turn : who];
const rad = angle*Math.PI/180, sp = power*CFG.velMult;
const m = muzzleOf(p);
const st = {x:m.x, y:m.y, vx:Math.cos(rad)*sp, vy:-Math.sin(rad)*sp, trail:[], t:0, owner:p};
const saved = proj; proj = st;
const dt = 1/480;
let hit = null, i = 0;
while(i++ < 8000){
const ax = CFG.windMult*wind;
st.vx += ax*dt; st.vy += CFG.gravity*dt;
const nx = st.x + st.vx*dt, ny = st.y + st.vy*dt;
hit = castHit(st.x, st.y, nx, ny);
if(hit) break;
st.x = nx; st.y = ny; st.t += dt;
if(st.x < -300 || st.x > W+300 || st.y > H+500) break;
}
proj = saved;
return hit ? {x:hit.x, y:hit.y, type:hit.type, who:hit.who ? hit.who.id : null} : null;
}
window.GORILLAZ = {
CFG: CFG, DEFAULTS: DEFAULTS, simShot: simShot,
setAim: (a, pw)=>{ const p = players[turn]; if(a!=null)p.angle=clamp(a,0,180); if(pw!=null)p.power=clamp(pw,CFG.powerMin,CFG.powerMax); syncHUD(); },
get state(){ return { phase, turn, wind, scores, roundNo, matchOver, effects: effects.length,
players: players.map(p=>({x:p.x, y:p.gy, angle:p.angle, power:p.power, dead:p.dead, dir:p.dir})),
buildings: buildings.map(b=>({x:b.x, w:b.w, top:b.top})),
proj: proj ? {x:proj.x, y:proj.y, vx:proj.vx, vy:proj.vy} : null }; },
fire: fire, adjust: adjust, newRound: newRound, rollWind: rollWind,
setWind: v => { wind = v; syncHUD(); },
cfgKey: (k,v) => { CFG[k] = v; onCfgChange(k); }
};
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"39e8a34e-492b-4d4b-b230-108a4fd62422","tokensIn":2620132,"tokensOut":53305,"tokensTotal":2673437,"cost":0.04708343999999999,"turns":39,"toolCalls":38,"failedToolCalls":0,"timestamp":"2026-09-10T21:35:10.469Z"} -->