← Gorillaz results

Gorillaz

MiMo V2.6 Distill Qwen 9B GGUF · full

Initial view of 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">&#9662;</span>
        <span id="windText">WIND 0.0 m/s</span>
      </div>
      <div id="hudScore" class="chip">SESSION<br>P1 <span id="score1">0</span> &nbsp;-&nbsp; <span id="score2">0</span> P2</div>
    </div>

    <div id="hudAim" class="hud">ANGLE <b id="angleVal">45&deg;</b> &nbsp; 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">&nbsp;&nbsp;('+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>