Gorillaz

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 31,488 bytes · SHA-256 80bbe7c24a8b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Gorillaz</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;background:#080816;overflow:hidden;font-family:"Courier New",ui-monospace,monospace;color:#fff}
#stage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
#screen{
position:relative;width:960px;height:540px;flex:0 0 auto;
background:#000;overflow:hidden;
border:3px solid #2f6fed;box-shadow:0 0 0 2px #1b4bb0,0 0 0 4px #0a0a1a,0 18px 60px rgba(0,0,0,.75);
user-select:none;
}
canvas{width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges}
/* ---------- HUD ---------- */
.hud{position:absolute;z-index:5;pointer-events:none;text-shadow:2px 2px 0 #000}
#hudTop{top:8px;left:10px;right:10px;display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
.chip{background:#12121f;border:2px solid #3a3a63;padding:5px 9px;font-size:14px;letter-spacing:.5px;white-space:nowrap}
.chip.turn{min-width:150px;font-weight:bold}
#hudTurn{top:8px;left:10px}
#hudWind{top:36px;left:10px;background:#0c1420;border:2px solid #2f6fed;padding:5px 10px;display:flex;align-items:center;gap:9px;font-size:14px}
#windArrow{font-size:22px;line-height:1;transition:transform .25s}
#hudScore{top:8px;right:10px;text-align:right;font-size:14px;line-height:1.35}
#hudAim{bottom:12px;left:10px;background:#0c1420;border:2px solid #d97706;padding:5px 10px;font-size:14px;display:flex;gap:12px}
#hudAim b{color:#ffd23f}
#hudLive{bottom:12px;right:10px;text-align:right;font-size:12px;letter-spacing:1px;color:#7a7a9e}
#liveBadge{display:inline-flex;align-items:center;gap:5px;margin-top:4px;font-size:11px;font-weight:bold;letter-spacing:1px;background:#123d1f;border:1px solid #3fae4f;color:#5cff8a;padding:3px 8px}
#liveBadge.off{background:#1b1b26;border-color:#4a4a63;color:#6b6b86}
.dot{width:7px;height:7px;background:currentColor;border-radius:50%;animation:pulse 1.4s infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}}
/* ---------- buttons ---------- */
.btn{pointer-events:auto;cursor:pointer;font-family:inherit;font-size:13px;font-weight:bold;letter-spacing:1px;
background:#2f6fed;color:#fff;border:2px solid #7fb0ff;padding:7px 14px;transition:transform .06s,filter .12s}
.btn:hover{filter:brightness(1.15)}
.btn:active{transform:translateY(1px)}
.btn.alt{background:#d97706;border-color:#ffbe3f}
.btn.gear{position:absolute;top:8px;right:10px;z-index:7;background:#12121f;border:2px solid #3a3a63}
.btn.gear:hover{filter:brightness(1.3)}
/* ---------- overlay ---------- */
#overlay{position:absolute;inset:0;z-index:8;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;
background:rgba(4,4,14,.78);backdrop-filter:blur(2px)}
#overlay.show{display:flex}
#overlayBox{border:3px solid #ffd23f;background:#0d0d1c;padding:22px 34px;text-align:center;box-shadow:0 0 0 2px #ffd23f,0 14px 40px rgba(0,0,0,.6)}
#overlayTitle{font-size:30px;font-weight:bold;letter-spacing:2px;color:#ffd23f;text-shadow:3px 3px 0 #6b3f00}
#overlaySub{font-size:15px;color:#cfcfe0;margin-top:6px;letter-spacing:1px}
#overlayBtns{display:flex;gap:10px;justify-content:center;margin-top:12px;flex-wrap:wrap}
#overlayBtns .btn{min-width:150px}
/* ---------- config panel ---------- */
#config{position:absolute;inset:0;z-index:9;display:none;overflow-y:auto;background:rgba(6,6,16,.96)}
#config.show{display:block}
.cfgHead{position:sticky;top:0;background:#141428;padding:14px 20px 10px;border-bottom:3px solid #2f6fed;z-index:2}
.cfgHead h2{font-size:22px;letter-spacing:2px;color:#ffd23f;text-shadow:2px 2px 0 #6b3f00}
.cfgHead p{font-size:12px;color:#9a9ab8;margin-top:4px;letter-spacing:.5px}
.cfgRows{padding:14px 20px 24px;display:grid;grid-template-columns:1fr 1fr;gap:12px}
.cfgRow{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;
background:#111120;border:2px solid #2a2a44;padding:9px 11px}
.cfgRow .lbl{font-size:12.5px;color:#cfcfe0;line-height:1.35;letter-spacing:.4px}
.cfgRow .val{font-size:16px;font-weight:bold;color:#ffd23f;min-width:82px;text-align:right;transition:color .2s}
.cfgRow.live .val{color:#5cff8a}
.cfgRow.live{border-color:#3fae4f}
.cfgRow .val small{display:block;font-size:10px;color:#7a7a9e;font-weight:normal;margin-top:2px;letter-spacing:1px}
.stepper{display:flex;gap:5px}
.stepper button{width:34px;height:30px;background:#2f6fed;border:1px solid #7fb0ff;color:#fff;font-size:15px;cursor:pointer;line-height:1}
.stepper button:hover{filter:brightness(1.25)}
.stepper input{width:56px;height:30px;background:#000;border:1px solid #55557a;color:#ffd23f;font:600 13px monospace;text-align:center}
.cfgFoot{padding:10px 20px 26px;display:flex;gap:10px;align-items:center}
.cfgFoot .btn{flex:1}
.cfgNote{font-size:11px;color:#7a7a9e;letter-spacing:.5px}
</style>
</head>
<body>
<div id="stage">
<div id="screen">
<canvas id="game" width="960" height="540"></canvas>
<div id="hudTop" class="hud">
<div id="hudTurn" class="chip turn">PLAYER 1 TURN</div>
<div id="hudWind" class="chip">
<span id="windArrow">▾</span>
<span id="windText">WIND 0.0 m/s</span>
</div>
<div id="hudScore" class="chip">SESSION<br>P1 <span id="score1">0</span> - <span id="score2">0</span> P2</div>
</div>
<div id="hudAim" class="hud">ANGLE <b id="angleVal">45°</b> POWER <b id="powerVal">50</b></div>
<div id="hudLive" class="hud"><span id="liveBadge"><span class="dot"></span>LIVE TUNING</span></div>
<button id="btnPause" class="btn gear" title="Pause (Esc)">II</button>
<div id="overlay">
<div id="overlayBox">
<div id="overlayTitle">Gorillaz</div>
<div id="overlaySub">LOCAL TWO-PLAYER ARTILLERY</div>
<div id="overlayBtns"></div>
</div>
</div>
<div id="config">
<div class="cfgHead">
<h2>LIVE CONFIGURATION</h2>
<p>Edits apply to the next projectile launch. No restart required.</p>
</div>
<div class="cfgRows" id="cfgRows"></div>
<div class="cfgFoot">
<button class="btn alt" id="btnReset">RESET TO DEFAULTS</button>
<span class="cfgNote">Seed makes the skyline reproducible.</span>
</div>
</div>
</div>
</div>
<script>
(function(){
"use strict";
/* ============================================================
CONSTANTS / LIVE CONFIGURATION
============================================================ */
const DEFAULTS = {
blastRadius: 70, // 1 Blast / Explosion Radius
gravity: 1400, // 2 Gravity Strength (px/s^2)
windMin: 0, // 3 Wind Range: min speed
windMax: 260, // max speed
windShift: true, // 4 Whether wind direction shifts between turns
buildingCount: 12, // 5 Building Count
heightMin: 90, // 6 Building Height Range
heightMax: 210,
velocityMin: 300, // 7 Projectile Velocity Scale
velocityMax: 1000,
damageMode: "inside", // 8 splash damage: "inside" | "falloff"
seed: 42, // reproducible skyline seed
windPreset: "auto" // "auto" | "left" | "right"
};
const CFG = Object.assign({}, DEFAULTS);
const W = 960, H = 540, GROUND = 480;
const BANANA_R = 6, GORILLA_R = 15;
const PLAYER_COLORS = ["#e8352e", "#2f6fed"];
const BUILDING_COLORS = ["#2f7d4f","#7d4fa3","#d97706","#0f766e","#c048a0","#6a8d3f","#e08a2e","#1f6f8b"];
/* ============================================================
AUDIO (WebAudio, user-gesture gated)
============================================================ */
let AC = null;
function audio(){ if(!AC) AC = new (window.AudioContext||window.webkitAudioContext)(); return AC; }
function tone(freq, dur, type, vol, slideTo){
try{
const c = audio(); if(c.state==="suspended") c.resume();
const o = c.createOscillator(), g = c.createGain();
o.type = type||"square"; o.frequency.setValueAtTime(freq,c.currentTime);
if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, c.currentTime+dur);
g.gain.setValueAtTime(0.0001,c.currentTime);
g.gain.exponentialRampToValueAtTime(vol||0.12, c.currentTime+0.01);
g.gain.exponentialRampToValueAtTime(0.0001, c.currentTime+dur);
o.connect(g); g.connect(c.destination); o.start(); o.stop(c.currentTime+dur+0.02);
}catch(e){}
}
function noise(dur, vol){
try{
const c = audio(); if(c.state==="suspended") c.resume();
const n = c.sampleRate*dur, buf = c.createBuffer(1,n,c.sampleRate), d = buf.getChannelData(0);
for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(1-i/n);
const src = c.createBufferSource(); src.buffer=buf;
const g = c.createGain(); g.gain.value=vol||0.25;
const f = c.createBiquadFilter(); f.type="lowpass"; f.frequency.value=900;
src.connect(f); f.connect(g); g.connect(c.destination); src.start();
}catch(e){}
}
function sfxLaunch(){
tone(220,0.35,"sawtooth",0.10,760);
noise(0.28,0.10);
}
function sfxExplosion(big){
noise(0.55, big?0.5:0.3);
tone(big?180:140,0.4,"square",0.22,60);
}
function sfxTurn(){ tone(660,0.10,"square",0.08); }
function sfxWin(){
[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.22,"square",0.10),i*130));
}
function sfxLose(){
[392,330,262].forEach((f,i)=>setTimeout(()=>tone(f,0.25,"sawtooth",0.10),i*150));
}
function windAmbience(){
try{
const c = audio(); if(!c) return;
if(c.state==="suspended") c.resume();
if(c._wamb){ c._wamb.gain.setValueAtTime(0.0001,c.currentTime); return; }
const o=c.createOscillator(), g=c.createGain(), f=c.createBiquadFilter();
o.type="sine"; o.frequency.value=120;
f.type="lowpass"; f.frequency.value=420;
g.gain.value=0.035;
o.connect(f); f.connect(g); g.connect(c.destination); o.start();
c._wamb={o,g};
}catch(e){}
}
/* ============================================================
DETERMINISTIC RNG
============================================================ */
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;
};
}
/* ============================================================
STATE
============================================================ */
const S = {
city: [],
p1: {x:0,y:0}, p2: {x:0,y:0},
active: 0,
angle: 45, power: 50,
projectile: null,
wind: {x:0,y:0},
fx: [], // effects (explosion rings, scorches, particles)
particles: [],
scorches: [],
paused: false,
finished: false,
winner: null,
scores: [0,0],
bestOf: 3,
windShiftTimer: 0
};
/* ============================================================
SKYLINE GENERATION
============================================================ */
function pickColor(rng, skip){
const c = BUILDING_COLORS[Math.floor(rng()*BUILDING_COLORS.length)];
return (skip && skip.indexOf(c)>-1) ? pickColor(rng,skip) : c;
}
function genCity(seed){
const rng = mulberry32(seed);
const city = [];
let x = 26;
while(x < W-16){
const w = 46 + Math.floor(rng()*96);
const h = CFG.heightMin + Math.floor(rng()*(CFG.heightMax-CFG.heightMin+1));
const gap = 18 + Math.floor(rng()*46);
city.push({x:Math.round(x), w:w, h:h, top:GROUND-h, color:pickColor(rng,[])});
x += w + gap;
}
return city;
}
function findBuildingAt(px, py){
for(const b of S.city){
if(px >= b.x && px <= b.x+b.w && py >= b.top && py <= GROUND){
if(py >= b.top && py <= GROUND-b.h+8) return b;
return b;
}
}
return null;
}
function placeGorillas(){
const mid = W/2;
const left = S.city.filter(b => b.x + b.w < mid);
const right = S.city.filter(b => b.x > mid);
const a = left[Math.floor(Math.random()*left.length)];
const b = right[Math.floor(Math.random()*right.length)];
// ensure neither starts inside blast range of the other
for(let i=0;i<12;i++){
const c = left[Math.floor(Math.random()*left.length)];
const d = right[Math.floor(Math.random()*right.length)];
if(Math.abs((c.x+c.w)-d.x) > CFG.blastRadius*2 &&
Math.abs((d.x+d.w)-c.x) > CFG.blastRadius*2){ a=c; b=d; break; }
}
S.p1 = {x:a.x+a.w/2, y:a.top - GORILLA_R - 2};
S.p2 = {x:b.x+b.w/2, y:b.top - GORILLA_R - 2};
}
function setWind(){
let sp = CFG.windMin + Math.random()*(CFG.windMax-CFG.windMin);
let dir = Math.random()<0.5 ? -1 : 1;
if(CFG.windPreset==="left") dir=-1;
if(CFG.windPreset==="right") dir=1;
if(CFG.windPreset==="auto") dir = Math.random()<0.5?-1:1;
S.wind = {x:dir*sp, y:0};
}
/* ============================================================
PHYSICS
============================================================ */
function fire(){
if(S.projectile || S.finished || S.paused) return;
const deg = S.angle;
const rad = deg*Math.PI/180;
let speed = (S.power/100)*CFG.velocityMax;
speed = Math.max(CFG.velocityMin, Math.min(CFG.velocityMax, speed));
S.projectile = {
x:S.p1.x, y:S.p1.y, vx:Math.cos(rad)*speed, vy:Math.sin(rad)*speed,
r:BANANA_R, trail:[], age:0
};
sfxLaunch();
S.active = 1-S.active;
if(CFG.windShift){
S.windShiftTimer = 1;
setWind();
}
updateHUD();
}
function updateProjectile(dt){
const p = S.projectile; if(!p) return;
const steps = 5, sdt = dt/steps;
for(let i=0;i<steps;i++){
const nx = p.x + p.vx*sdt;
const ny = p.y + p.vy*sdt;
p.vy -= CFG.gravity*sdt;
p.vx += S.wind.x*sdt;
const hit = collideBuilding(nx,ny);
if(hit){ p.x=hit.x; p.y=hit.y; resolveImpact(hit.x,hit.y); return; }
if(ny + p.r >= GROUND && p.y + p.r <= GROUND){ p.x=nx; p.y=GROUND-p.r; resolveImpact(p.x,GROUND); return; }
const g = collideGorilla(nx,ny);
if(g){ p.x=nx; p.y=ny; resolveImpact(g.x,g.y,true); return; }
p.x=nx; p.y=ny;
}
}
function collideBuilding(x,y){
for(const b of S.city){
if(x > b.x-BANANA_R && x < b.x+b.w+BANANA_R &&
y > b.top-BANANA_R && y < GROUND){
if(y > b.top-BANANA_R && y < GROUND-b.h+8) return {x:x, y:b.top};
return {x:b.x+b.w, y:y};
}
}
return null;
}
function collideGorilla(x,y){
const a=S.p1, b=S.p2;
if(dist(x,y,a.x,a.y) < GORILLA_R+BANANA_R) return a;
if(dist(x,y,b.x,b.y) < GORILLA_R+BANANA_R) return b;
return null;
}
function dist(x1,y1,x2,y2){ const dx=x1-x2,dy=y1-y2; return Math.sqrt(dx*dx+dy*dy); }
function resolveImpact(x,y,gorilla){
const R = CFG.blastRadius;
S.fx.push({type:"ring", x:x, y:y, r:6, R:R, t:0, big:!!gorilla});
S.fx.push({type:"ring", x:x, y:y, r:R*0.35, R:R, t:0.06, big:!!gorilla, thin:true});
for(let i=0;i<28;i++){
const ang = Math.random()*Math.PI*2, sp = 70+Math.random()*280;
S.particles.push({x:x,y:y,vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,life:1,col:
Math.random()<0.5?"#ffd23f":(Math.random()<0.5?"#ff7a1a":"#8a8a9e")});
}
for(let i=0;i<10;i++){
S.particles.push({x:x,y:y,vy:20+Math.random()*60,life:1,col:"#2a2418"});
}
sfxExplosion(!!gorilla);
S.scorches.push({x:x, y:y, t:0});
// damage check
if(CFG.damageMode==="inside"){
for(const g of [S.p1,S.p2]){
if(dist(x,y,g.x,g.y) < R) eliminate(1-g.active);
}
}else{
for(const g of [S.p1,S.p2]){
const d = dist(x,y,g.x,g.y), R2=CFG.blastRadius;
if(d < R){
const dmg = R*(1-d/R);
if(dmg >= GORILLA_R) eliminate(1-g.active);
}
}
}
}
function eliminate(idx){
const who = idx===0 ? S.p1 : S.p2;
who.dead = true;
S.winner = idx;
S.finished = true;
S.scores[idx]++;
sfxWin();
tone(120,0.5,"sawtooth",0.16,60);
windAmbience();
if(S.scores[idx] >= S.bestOf) endMatch();
}
function endMatch(){
const win = S.scores[0]>S.scores[1] ? 0 : 1;
document.getElementById("overlayTitle").textContent = win===0 ? "P1 WINS" : "P2 WINS";
document.getElementById("overlaySub").textContent = win===0 ? "GORILLA 1 BLOWN AWAY" : "GORILLA 2 BLOWN AWAY";
const btns = document.getElementById("overlayBtns");
btns.innerHTML = "";
const b1=document.createElement("button"); b1.className="btn alt"; b1.textContent="NEW MATCH";
const b2=document.createElement("button"); b2.className="btn"; b2.textContent="REMATCH";
b1.onclick=newMatch; b2.onclick=newMatch;
btns.appendChild(b1); btns.appendChild(b2);
document.getElementById("overlay").classList.add("show");
}
function newMatch(){
S.city = genCity(CFG.seed);
placeGorillas();
S.p1.dead=S.p2.dead=false;
S.projectile=null; S.fx=[]; S.particles=[]; S.scorches=[];
S.active=0; S.angle=45; S.power=50; S.finished=false; S.winner=null;
S.scores=[0,0];
setWind();
document.getElementById("overlay").classList.remove("show");
updateHUD();
}
/* ============================================================
HUD
============================================================ */
const $=id=>document.getElementById(id);
function updateHUD(){
const names=["PLAYER 1 TURN","PLAYER 2 TURN"];
$("hudTurn").textContent = names[S.active];
const sp = Math.abs(S.wind.x);
const left = S.wind.x<0;
$("windArrow").textContent = left ? "\u25C0" : "\u25B6";
$("windArrow").style.transform = left ? "rotate(180deg)" : "none";
$("windText").textContent = "WIND " + sp.toFixed(1) + " m/s " + (left?"LEFT":"RIGHT");
$("score1").textContent = S.scores[0];
$("score2").textContent = S.scores[1];
$("angleVal").textContent = Math.round(S.angle) + "\u00B0";
$("powerVal").textContent = Math.round(S.power);
}
/* ============================================================
RENDERING
============================================================ */
const cv=$("game"), ctx=cv.getContext("2d");
ctx.imageSmoothingEnabled=false;
function drawBackground(){
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,"#1a1a4a"); g.addColorStop(0.55,"#2b2a6e"); g.addColorStop(1,"#0d0d24");
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// sun / moon
ctx.fillStyle="#ffd23f";
ctx.beginPath(); ctx.arc(790,110,34,0,Math.PI*2); ctx.fill();
ctx.fillStyle="rgba(255,210,63,.25)";
ctx.beginPath(); ctx.arc(790,110,50,0,Math.PI*2); ctx.fill();
// clouds
ctx.fillStyle="rgba(255,255,255,.14)";
drawCloud(140,120,1.0); drawCloud(430,86,0.7); drawCloud(650,160,0.85); drawCloud(880,74,0.6);
// ground
ctx.fillStyle="#0a0a16"; ctx.fillRect(0,GROUND,W,H-GROUND);
ctx.fillStyle="#1c1c36"; ctx.fillRect(0,GROUND,W,5);
ctx.fillStyle="#0a0a16"; ctx.fillRect(0,GROUND+5,W,H-GROUND-5);
// ground dashes
ctx.fillStyle="rgba(120,120,180,.20)";
for(let x=0;x<W;x+=26){ ctx.fillRect(x,GROUND+14,14,2); }
}
function drawCloud(x,y,s){
ctx.beginPath();
ctx.arc(x,y,18*s,0,Math.PI*2);
ctx.arc(x+24*s,y+4*s,24*s,0,Math.PI*2);
ctx.arc(x+50*s,y,18*s,0,Math.PI*2);
ctx.arc(x+18*s,y+16*s,28*s,0,Math.PI*2);
ctx.fill();
}
function drawBuilding(b){
ctx.fillStyle=b.color; ctx.fillRect(b.x,b.top,b.w,b.h);
ctx.fillStyle="rgba(0,0,0,.35)"; ctx.fillRect(b.x,b.top,b.w,3);
// windows
const cols=Math.max(1,Math.floor(b.w/26)), rows=Math.max(1,Math.floor(b.h/26));
ctx.fillStyle="rgba(255,255,255,.10)";
for(let i=0;i<cols;i++) for(let j=0;j<rows;j++){
ctx.fillRect(b.x+9+i*((b.w-18)/cols), b.top+9+j*((b.h-18)/rows), 6, 6);
}
}
function drawGorilla(p, pose){
if(p.dead) return;
const bob = Math.sin(performance.now()/260 + (p===S.p1?0:1.7))*2;
const scale = p===S.p1 ? 1 : 0.92;
const ox = p.x, oy = p.y + bob;
ctx.save();
ctx.translate(ox,oy);
ctx.scale(scale,scale);
if(pose==="windup"){ ctx.rotate(-0.16); }
if(pose==="win"){ ctx.translate(0,-6); ctx.scale(1.06,1.06); }
const body=PLAYER_COLORS[p===S.p1?0:1];
const light="#ffe8a8";
const sp=GORILLA;
const s=2.6;
for(let r=0;r<sp.length;r++){
const row=sp[r];
for(let c=0;c<row.length;c++){
const ch=row[c];
if(ch===".") continue;
const x=(c-7)*s, y=(r-7)*s;
const w=ch==="o"?s*1.35:s;
ctx.fillStyle = ch==="o"||ch==="E" ? light : body;
ctx.fillRect(Math.round(x),Math.round(y),Math.ceil(w),Math.ceil(s));
}
}
// eyes
ctx.fillStyle="#0a0a16";
ctx.fillRect(-3*s,2*s,2*s,2*s); ctx.fillRect(3*s,2*s,2*s,2*s);
// shadow
ctx.fillStyle="rgba(0,0,0,.45)";
ctx.beginPath(); ctx.ellipse(0,7*s,13*s,4*s,0,0,Math.PI*2); ctx.fill();
ctx.restore();
}
const GORILLA=[
"....##......",
"...#######..",
"..##########",
".###..oo..###",
".###..oo..###",
"###..oo..###.",
"###..oo..###.",
".###.###.###.",
".###.###.###.",
"...#####.####",
"...#####.####",
"..#...#...#..",
"..##...##....",
".####.....###"
];
function drawProjectile(){
const p=S.projectile; if(!p) return;
// trail
for(let i=1;i<p.trail.length;i++){
const a=p.trail[i-1], b=p.trail[i];
ctx.strokeStyle="rgba(255,210,63,"+(0.06+i*0.045)+")";
ctx.lineWidth=5; ctx.lineCap="round";
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
ctx.strokeStyle="rgba(120,70,10,.55)"; ctx.lineWidth=4;
ctx.beginPath(); ctx.moveTo(p.trail[0].x,p.trail[0].y);
ctx.lineTo(p.trail[p.trail.length-1].x,p.trail[p.trail.length-1].y); ctx.stroke();
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(Math.atan2(p.vy,p.vx));
const s=1.35;
ctx.fillStyle="#ffd23f";
ctx.beginPath();
ctx.moveTo(-10*s,-3*s);
ctx.quadraticCurveTo(0,-11*s,10*s,-3*s);
ctx.quadraticCurveTo(1*s,7*s,-10*s,3*s);
ctx.closePath(); ctx.fill();
ctx.fillStyle="#8b4a2b";
ctx.fillRect(8*s,-2*s,4*s,5*s);
ctx.fillStyle="rgba(255,255,255,.35)";
ctx.fillRect(-8*s,-4*s,6*s,3*s);
ctx.restore();
}
function drawEffects(){
for(const e of S.fx){
if(e.type==="ring"){
const t=Math.min(1,e.t/0.55);
const r=e.r+(e.R-e.r)*t;
ctx.save();
ctx.globalAlpha=(1-t)*0.95;
if(e.big){
const rg=ctx.createRadialGradient(e.x,e.y,0,e.x,e.y,r);
rg.addColorStop(0,"rgba(255,255,220,1)");
rg.addColorStop(0.28,"rgba(255,180,40,.95)");
rg.addColorStop(0.6,"rgba(255,90,20,.5)");
rg.addColorStop(1,"rgba(255,60,10,0)");
ctx.fillStyle=rg;
ctx.beginPath(); ctx.arc(e.x,e.y,r,0,Math.PI*2); ctx.fill();
}else{
ctx.strokeStyle="rgba(255,210,63,"+(0.6*(1-t))+")";
ctx.lineWidth=4;
ctx.beginPath(); ctx.arc(e.x,e.y,r,0,Math.PI*2); ctx.stroke();
}
ctx.restore();
}
}
// particles
for(const p of S.particles){
ctx.globalAlpha=Math.max(0,p.life);
ctx.fillStyle=p.col;
ctx.fillRect(p.x,p.y,4,4);
}
ctx.globalAlpha=1;
// scorches
for(const s of S.scorches){
ctx.globalAlpha=Math.max(0,1-s.t/8);
ctx.fillStyle="#241d14";
ctx.fillRect(s.x-3,s.y-2,6,4);
ctx.fillStyle="#3a2f1e";
ctx.fillRect(s.x-1,s.y-1,2,2);
}
ctx.globalAlpha=1;
}
function drawAimPreview(){
if(S.projectile || S.finished || S.paused) return;
const deg=S.angle, rad=deg*Math.PI/180;
let x=S.p1.x, y=S.p1.y;
ctx.save();
ctx.strokeStyle="rgba(255,255,255,.18)";
ctx.lineWidth=1.5; ctx.setLineDash([4,5]);
ctx.beginPath();
for(let i=0;i<=100;i++){
x+=Math.cos(rad)*2; y+=Math.sin(rad)*2;
if(y>=GROUND||x>W-14||x<14||collideBuilding(x,y)){ break; }
ctx.lineTo(x,y);
}
ctx.stroke(); ctx.restore();
}
function render(){
drawBackground();
for(const b of S.city) drawBuilding(b);
drawAimPreview();
drawGorilla(S.p1,"idle");
drawGorilla(S.p2,"idle");
drawProjectile();
drawEffects();
// active glow
if(!S.projectile && !S.paused){
ctx.save();
ctx.globalAlpha=.16;
ctx.fillStyle=PLAYER_COLORS[S.active];
ctx.beginPath(); ctx.arc(S.p1.x,S.p1.y,34,0,Math.PI*2); ctx.fill();
ctx.restore();
}
}
/* ============================================================
LOOP
============================================================ */
let last=performance.now();
function frame(now){
const dt=Math.min(0.05,(now-last)/1000); last=now;
if(!S.paused && !S.finished){
if(S.windShiftTimer>0){ S.windShiftTimer-=dt; if(S.windShiftTimer<=0) setWind(); }
updateProjectile(dt);
updateFx(dt);
windAmbience();
}
render();
requestAnimationFrame(frame);
}
function updateFx(dt){
for(const p of S.particles){
p.vy -= CFG.gravity*0.18*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.life-=dt*0.9;
}
for(const s of S.scorches) s.t+=dt;
for(const e of S.fx){ e.t+=dt; if(e.t>1.4) e.dead=true; }
for(const p of S.particles){ if(p.life<=0) p.dead=true; }
S.particles=S.particles.filter(p=>!p.dead);
S.fx=S.fx.filter(e=>!e.dead);
S.scorches=S.scorches.filter(s=>s.t<8);
}
/* ============================================================
LIVE CONFIG UI
============================================================ */
const CFG_ROWS = [
{k:"blastRadius", t:"Blast / Explosion Radius", u:"px", step:5},
{k:"gravity", t:"Gravity Strength", u:"px/s\u00B2", step:50},
{k:"windMin", t:"Wind Variance \u2014 Min Speed", u:"m/s", step:10},
{k:"windMax", t:"Wind Variance \u2014 Max Speed", u:"m/s", step:10},
{k:"buildingCount", t:"Building Count", u:"min 8 / max 14", step:1},
{k:"heightMin", t:"Building Height Range \u2014 Min", u:"px", step:5},
{k:"heightMax", t:"Building Height Range \u2014 Max", u:"px", step:5},
{k:"velocityMin", t:"Projectile Velocity Scale \u2014 Min", u:"px/s", step:20},
{k:"velocityMax", t:"Projectile Velocity Scale \u2014 Max", u:"px/s", step:20},
{k:"damageMode", t:"Explosion Damage Threshold", u:"inside / falloff", step:0},
{k:"seed", t:"Skyline Seed (reproducible)", u:"integer", step:1},
{k:"windPreset", t:"Wind Direction Preset", u:"auto / left / right", step:0}
];
function renderConfig(){
const wrap=$("cfgRows"); wrap.innerHTML="";
CFG_ROWS.forEach(r=>{
const row=document.createElement("div");
row.className="cfgRow"; row.dataset.k=r.k;
row.innerHTML='<div class="lbl">'+r.t+(r.step? ' <span style="color:#7a7a9e"> ('+r.u+')</span>':'')+'</div>'+
'<div class="stepper">'+
'<button data-act="dec">-\u2212</button>'+
'<input type="number" value="'+CFG[r.k]+'" step="'+r.step+'" data-act="set">'+
'<button data-act="inc">+\u2212</button>'+
'</div>';
wrap.appendChild(row);
});
}
function markLive(){
document.querySelectorAll(".cfgRow").forEach(row=>{
const k=row.dataset.k;
row.classList.toggle("live", CFG[k]!==DEFAULTS[k]);
});
const badge=$("liveBadge");
const editing = CFG.blastRadius!==DEFAULTS.blastRadius || CFG.gravity!==DEFAULTS.gravity ||
CFG.windMin!==DEFAULTS.windMin || CFG.windMax!==DEFAULTS.windMax ||
CFG.buildingCount!==DEFAULTS.buildingCount || CFG.heightMin!==DEFAULTS.heightMin ||
CFG.heightMax!==DEFAULTS.heightMax || CFG.velocityMin!==DEFAULTS.velocityMin ||
CFG.velocityMax!==DEFAULTS.velocityMax || CFG.damageMode!==DEFAULTS.damageMode ||
CFG.seed!==DEFAULTS.seed || CFG.windPreset!==DEFAULTS.windPreset;
badge.classList.toggle("off", !editing);
badge.querySelector(".dot").style.animation = editing ? "pulse 1.4s infinite" : "none";
}
function bindConfig(){
const wrap=$("cfgRows");
wrap.addEventListener("click", e=>{
const b=e.target.closest("button[data-act]"); if(!b) return;
const row=b.closest(".cfgRow"); const k=row.dataset.k;
let v=CFG[k];
if(k==="windPreset"||k==="damageMode"){
const opts = k==="windPreset" ? ["auto","left","right"] : ["inside","falloff"];
v = opts[(opts.indexOf(String(v))+1)%opts.length];
if(k==="windPreset"){ CFG.windShift = (v==="auto"); markWindShift(); }
}else if(r.type==="number"){
v=CFG[k]+(b.dataset.act==="inc"?1:-1);
if(k==="windMin") v=Math.max(0,v);
if(k==="windMax") v=Math.max(CFG.windMin+5,v);
if(k==="heightMin") v=Math.max(40,v);
if(k==="heightMax") v=Math.max(CFG.heightMin+1,v);
if(k==="velocityMin") v=Math.min(CFG.velocityMax-1,v);
if(k==="velocityMax") v=Math.max(CFG.velocityMin+1,v);
if(k==="blastRadius") v=Math.min(160,v);
if(k==="gravity") v=Math.min(2000,v);
if(k==="seed") v=Math.max(0,v);
}else{ v=CFG[k]; }
CFG[k]=v;
const inp=wrap.querySelector('.cfgRow[data-k="'+k+'"] input');
if(inp) inp.value=v;
markLive();
});
wrap.addEventListener("change", e=>{
const inp=e.target.closest("input[data-act='set']"); if(!inp) return;
const k=inp.closest(".cfgRow").dataset.k;
let v=parseInt(inp.value,10);
if(isNaN(v)) v=CFG[k];
const lim = k==="windMin"?0 : k==="windMax"?CFG.windMin+5 : k==="heightMin"?40 : k==="heightMax"?CFG.heightMin+1 : k==="velocityMin"?CFG.velocityMax-1 : k==="velocityMax"?CFG.velocityMin+1 : k==="blastRadius"?160 : k==="gravity"?2000 : 0;
v=Math.max(lim,Math.min(infFor(k),v));
CFG[k]=v; inp.value=v; markLive();
});
function infFor(k){ return k==="windMin"?CFG.windMax : k==="windMax"?CFG.windMin+5 : k==="heightMin"?CFG.heightMax : k==="heightMax"?CFG.heightMin+1 : k==="velocityMin"?CFG.velocityMax : k==="velocityMax"?CFG.velocityMin : Infinity; }
$("btnReset").onclick=()=>{
Object.assign(CFG,DEFAULTS);
renderConfig(); markLive();
};
}
let windShiftLocked=false;
function markWindShift(){
windShiftLocked=true;
setTimeout(()=>{windShiftLocked=false;},1500);
}
/* ============================================================
INPUT
============================================================ */
function bindInput(){
window.addEventListener("keydown", e=>{
if($("overlay").classList.contains("show")){
if(e.key==="Enter"||e.key===" "){ e.preventDefault(); newMatch(); }
return;
}
if(e.key==="Escape"){ togglePause(); return; }
if($("config").classList.contains("show")){
const r=document.activeElement.closest && document.activeElement.closest(".cfgRow");
if(!r){ $("config").classList.remove("show"); return; }
}
if(S.finished || S.paused) return;
const step=e.shiftKey?4:1;
if(e.key==="ArrowLeft"){ S.angle=Math.max(0,S.angle-step); updateHUD(); e.preventDefault(); }
else if(e.key==="ArrowRight"){ S.angle=Math.min(90,S.angle+step); updateHUD(); e.preventDefault(); }
else if(e.key==="ArrowDown"){ S.power=Math.max(1,S.power-step); updateHUD(); e.preventDefault(); }
else if(e.key==="ArrowUp"){ S.power=Math.min(100,S.power+step); updateHUD(); e.preventDefault(); }
else if(e.key===" "){ fire(); }
else if(e.key==="c"||e.key==="C"||e.key==="g"||e.key==="G"){ toggleConfig(); }
else if(e.key==="r"||e.key==="R"){ newMatch(); }
else if(e.key==="p"||e.key==="P"){ togglePause(); }
});
$("btnPause").onclick=togglePause;
}
function togglePause(){
S.paused=!S.paused;
$("btnPause").textContent=S.paused?"▶":"II";
$("btnPause").title=S.paused?"Resume (Esc)":"Pause (Esc)";
$("hudTurn").style.opacity=S.paused?"0.45":"1";
}
function toggleConfig(){
const c=$("config");
c.classList.toggle("show");
if(c.classList.contains("show")){ c.scrollTop=0; markLive(); }
}
/* ============================================================
INIT
============================================================ */
function init(){
renderConfig();
bindConfig();
bindInput();
newMatch();
updateHUD();
markLive();
windAmbience();
requestAnimationFrame(frame);
}
init();
})();
</script>
</body>
</html>