← Gorillaz results

Gorillaz

Ornith 1.5 35B A3B · full

Initial view of Gorillaz

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 38,644 bytes · SHA-256 e56803ebd6fb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gorillaz</title>
<style>
  :root{
    --bg:#0b1020;
    --panel:#10182f;
    --panel2:#16203c;
    --ink:#e8ecff;
    --muted:#8ea0c9;
    --accent:#ffd23f;
    --p1:#ff5252;
    --p2:#4d8dff;
    --ok:#3ddc84;
    --border:#26305a;
  }
  *{box-sizing:border-box;}
  html,body{
    margin:0;height:100%;
    background:var(--bg);
    color:var(--ink);
    font-family:"Segoe UI",Verdana,sans-serif;
    -webkit-font-smoothing:antialiased;
    overflow:hidden;
  }
  #wrap{
    position:relative;
    width:100%;height:100%;
    display:flex;align-items:center;justify-content:center;
  }
  #stage{
    position:relative;
    box-shadow:0 0 0 3px #202a4d,0 20px 60px rgba(0,0,0,.6);
    background:#000;
  }
  canvas{display:block;image-rendering:auto;}
  .overlay{
    position:absolute;inset:0;
    pointer-events:none;
  }
  /* HUD top bar */
  #hud{
    position:absolute;top:0;left:0;right:0;
    display:flex;align-items:center;justify-content:space-between;
    padding:8px 14px;
    font-weight:700;
    text-shadow:0 2px 4px rgba(0,0,0,.7);
    pointer-events:none;
    font-size:14px;
  }
  #wind{
    display:flex;align-items:center;gap:8px;
    background:rgba(8,14,32,.55);
    padding:6px 12px;border-radius:8px;
    border:1px solid var(--border);
  }
  #windArrow{
    font-size:20px;line-height:1;
    transition:transform .3s ease;
  }
  #turn{
    background:rgba(8,14,32,.55);
    padding:6px 14px;border-radius:8px;
    border:1px solid var(--border);
    font-size:15px;
  }
  #score{
    display:flex;gap:14px;align-items:center;
    background:rgba(8,14,32,.55);
    padding:6px 12px;border-radius:8px;
    border:1px solid var(--border);
  }
  .dot{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:6px;}
  .p1c{color:var(--p1);}
  .p2c{color:var(--p2);}

  /* Aiming readout bottom */
  #aim{
    position:absolute;left:50%;bottom:12px;transform:translateX(-50%);
    display:flex;gap:18px;align-items:center;
    background:rgba(8,14,32,.6);
    padding:8px 18px;border-radius:10px;
    border:1px solid var(--border);
    font-size:15px;pointer-events:none;
  }
  #aim b{color:var(--accent);}
  #aim .lbl{color:var(--muted);font-weight:600;}

  /* Buttons */
  .btn{
    pointer-events:auto;
    cursor:pointer;
    background:linear-gradient(#263463,#1a2447);
    color:var(--ink);
    border:1px solid var(--border);
    padding:8px 14px;border-radius:8px;
    font-weight:700;font-size:14px;
    text-transform:uppercase;letter-spacing:.5px;
    transition:transform .08s ease, background .15s ease;
  }
  .btn:hover{background:linear-gradient(#31437f,#222f5c);}
  .btn:active{transform:translateY(1px);}
  .btn.primary{background:linear-gradient(#ffd23f,#f5b900);color:#201400;border-color:#e0a300;}
  .btn.fire{background:linear-gradient(#ff6b4d,#e23b2a);color:#fff;border-color:#c22b1c;}

  /* Controls hint bar */
  #hint{
    position:absolute;left:0;right:0;bottom:56px;
    text-align:center;color:var(--muted);
    font-size:12px;pointer-events:none;
    text-shadow:0 1px 3px rgba(0,0,0,.8);
  }

  /* Settings gear */
  #gear{
    position:absolute;top:44px;right:14px;
    pointer-events:auto;cursor:pointer;
    width:34px;height:34px;border-radius:8px;
    background:rgba(8,14,32,.55);
    border:1px solid var(--border);
    display:flex;align-items:center;justify-content:center;
    font-size:18px;
  }
  #gear:hover{background:rgba(38,52,95,.8);}

  /* Tuning panel overlay */
  #tune{
    position:absolute;top:0;right:0;bottom:0;width:360px;
    background:rgba(9,14,30,.96);
    border-left:2px solid var(--border);
    transform:translateX(100%);
    transition:transform .25s ease;
    pointer-events:auto;
    padding:16px 16px 16px 18px;
    overflow-y:auto;
  }
  #tune.open{transform:translateX(0);}
  #tune h2{margin:0 0 4px;font-size:18px;letter-spacing:1px;}
  #tune .sub{color:var(--muted);font-size:12px;margin-bottom:12px;}
  #liveBadge{
    display:inline-block;font-size:11px;font-weight:700;
    background:#1c3a2c;color:var(--ok);
    border:1px solid var(--ok);
    padding:2px 8px;border-radius:20px;margin-left:8px;
    vertical-align:middle;
  }
  .grp{margin-bottom:14px;}
  .grp .title{
    font-size:12px;font-weight:700;color:var(--accent);
    text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px;
    display:flex;justify-content:space-between;align-items:center;
  }
  .row{
    display:flex;align-items:center;gap:6px;margin:5px 0;
  }
  .row label{font-size:12px;color:var(--muted);width:96px;flex:none;}
  .row .val{
    width:54px;text-align:center;font-weight:700;font-size:13px;
    background:#0c1428;border:1px solid var(--border);
    padding:4px 2px;border-radius:6px;color:var(--ink);
  }
  .row input[type=number]{
    background:#0c1428;border:1px solid var(--border);
    color:var(--ink);border-radius:6px;padding:4px;font-size:13px;width:100%;
  }
  .spin{
    display:flex;align-items:center;gap:2px;
  }
  .spin button{
    width:26px;height:26px;border-radius:6px;cursor:pointer;
    background:linear-gradient(#2a396c,#1c274c);
    border:1px solid var(--border);color:var(--ink);font-weight:700;font-size:15px;
    line-height:1;
  }
  .spin button:active{transform:translateY(1px);}
  .spin .v{
    flex:1;text-align:center;font-size:12px;font-weight:700;
    background:#0c1428;border:1px solid var(--border);border-radius:6px;
    padding:4px 0;color:var(--accent);
  }
  .chk{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);}
  .chk input{width:16px;height:16px;accent-color:var(--accent);}
  .tuneActions{display:flex;gap:8px;margin-top:8px;}
  .tuneActions .btn{flex:1;font-size:12px;padding:8px;}
  #closeTune{position:absolute;top:14px;right:14px;background:none;border:none;
    color:var(--muted);font-size:22px;cursor:pointer;line-height:1;}
  #closeTune:hover{color:var(--ink);}

  /* Modal */
  #modal{
    position:absolute;inset:0;
    display:none;align-items:center;justify-content:center;
    background:rgba(4,7,16,.78);
    pointer-events:auto;z-index:20;
    text-align:center;
  }
  #modal.show{display:flex;}
  #modal .card{
    background:linear-gradient(#16203c,#10182f);
    border:2px solid var(--border);
    border-radius:16px;padding:30px 40px;
    box-shadow:0 20px 60px rgba(0,0,0,.7);
    max-width:460px;
  }
  #modal h1{margin:0 0 6px;font-size:42px;letter-spacing:2px;
    background:linear-gradient(#ffd23f,#ff8a3d);
    -webkit-background-clip:text;background-clip:text;color:transparent;}
  #modal .winner{font-size:26px;font-weight:800;margin:6px 0 4px;}
  #modal p{color:var(--muted);margin:6px 0 18px;line-height:1.5;}
  #modal .actions{display:flex;gap:12px;justify-content:center;}
  .big{font-size:16px;padding:12px 22px;}

  /* Mode selector at start */
  #start{
    position:absolute;inset:0;
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    background:radial-gradient(circle at 50% 35%,#1a2547,#080c1c);
    pointer-events:auto;z-index:25;text-align:center;
  }
  #start.hide{display:none;}
  #start h1{
    font-size:72px;margin:0;letter-spacing:4px;
    background:linear-gradient(#ffd23f,#ff5252 55%,#4d8dff);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    text-shadow:0 6px 30px rgba(255,180,60,.25);
  }
  #start .tag{color:var(--muted);margin:4px 0 26px;letter-spacing:6px;font-size:13px;text-transform:uppercase;}
  #start .modes{display:flex;gap:14px;}
  #start .mode{
    cursor:pointer;border:2px solid var(--border);border-radius:12px;
    padding:18px 26px;background:rgba(22,32,60,.6);
    transition:transform .1s ease,border-color .15s ease;
  }
  #start .mode:hover{border-color:var(--accent);transform:translateY(-3px);}
  #start .mode b{display:block;font-size:20px;margin-bottom:4px;}
  #start .mode span{color:var(--muted);font-size:12px;}
  #start .foot{color:var(--muted);font-size:12px;margin-top:26px;}
</style>
</head>
<body>
<div id="wrap">
  <div id="stage">
    <canvas id="c"></canvas>
    <div class="overlay">
      <div id="hud">
        <div id="score">
          <span><span class="dot" style="background:var(--p1)"></span><span class="p1c">P1</span> <b id="score1">0</b></span>
          <span><span class="dot" style="background:var(--p2)"></span><span class="p2c">P2</span> <b id="score2">0</b></span>
        </div>
        <div id="turn">Player 1 Turn</div>
        <div id="wind">
          <span id="windArrow">➤</span>
          <span><b id="windVal">0</b> km/h <span id="windDir">→</span></span>
        </div>
      </div>
      <div id="gear" title="Live Tuning">⚙</div>

      <div id="aim">
        <span><span class="lbl">ANGLE</span> <b id="angVal">45</b>°</span>
        <span><span class="lbl">POWER</span> <b id="powVal">50</b></span>
      </div>
      <div id="hint">Aim: ◀ ▶ &nbsp;•&nbsp; Power: ▲ ▼ &nbsp;•&nbsp; Fire: SPACE</div>

      <div id="tune">
        <button id="closeTune">×</button>
        <h2>Live Tuning <span id="liveBadge">ACTIVE</span></h2>
        <div class="sub">Edit constants — applied immediately (next turn) or on next match. No restart required.</div>
        <div id="tuneBody"></div>
        <div class="tuneActions">
          <button class="btn" id="resetBtn">Reset Defaults</button>
          <button class="btn primary" id="applyBtn">Apply &amp; New Match</button>
        </div>
      </div>

      <div id="modal">
        <div class="card">
          <h1 id="mTitle">GORILLAZ</h1>
          <div class="winner" id="mWinner"></div>
          <p id="mText"></p>
          <div class="actions" id="mActions"></div>
        </div>
      </div>

      <div id="start">
        <h1>GORILLAZ</h1>
        <div class="tag">Exploding Banana Artillery</div>
        <div class="modes">
          <div class="mode" data-mode="1"><b>1 Round</b><span>Quick match</span></div>
          <div class="mode" data-mode="3"><b>Best of 3</b><span>First to 2</span></div>
          <div class="mode" data-mode="5"><b>Best of 5</b><span>First to 3</span></div>
        </div>
        <div class="foot">Local 2-player • Aim with arrows, Fire with Space • ⚙ for live tuning</div>
      </div>
    </div>
  </div>
</div>

<script>
(function(){
"use strict";

/* =========================================================
   GORILLAZ — a turn-based artillery game
   ========================================================= */

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

// Logical resolution (retro 16:9-ish, no distortion regardless of screen)
const W = 960, H = 600;
canvas.width = W; canvas.height = H;

function fitStage(){
  const wrap = document.getElementById('wrap');
  const w = wrap.clientWidth, h = wrap.clientHeight;
  const scale = Math.min(w / W, h / H);
  const stage = document.getElementById('stage');
  stage.style.width = (W*scale) + 'px';
  stage.style.height = (H*scale) + 'px';
  canvas.style.width = (W*scale) + 'px';
  canvas.style.height = (H*scale) + 'px';
}
window.addEventListener('resize', fitStage);

/* =========================================================
   CONFIGURATION (live-tunable). Defaults = classic reference.
   ========================================================= */
const DEFAULTS = {
  blastRadius:      58,   // circular damage radius on impact (px)
  gravity:          0.45, // downward acceleration per physics step
  windMin:          0,    // min |wind| magnitude applied each shift
  windMax:          1.15, // max |wind| magnitude
  windShift:        true, // allow direction/magnitude shifts between turns
  buildingCount:    10,   // number of buildings in skyline
  buildingMinH:     120,  // min building height (px)
  buildingMaxH:     260,  // max building height (px)
  powerMin:         4,    // lower bound of throw power
  powerMax:         95,   // upper bound of throw power
  damageMode:       "hard", // "hard" = must be inside radius; "falloff" = partial outside
  matchMode:        1     // rounds to win (1,3,5)
};

let CFG = Object.assign({}, DEFAULTS);

// Tuning panel definition (label, key, min, max, step, type)
const TUNE_FIELDS = [
  {key:'blastRadius',    label:'Blast Radius',   min:20,  max:140, step:1, type:'spin'},
  {key:'gravity',        label:'Gravity',        min:0.1, max:1.5,  step:0.05, type:'spin'},
  {key:'windMin',        label:'Wind Min',       min:0,   max:3,   step:0.05, type:'spin'},
  {key:'windMax',        label:'Wind Max',       min:0,   max:5,   step:0.05, type:'spin'},
  {key:'buildingCount',  label:'Building Count', min:4,   max:20,  step:1, type:'spin'},
  {key:'buildingMinH',   label:'Building Min H', min:60,  max:300, step:5, type:'spin'},
  {key:'buildingMaxH',   label:'Building Max H', min:100, max:420, step:5, type:'spin'},
  {key:'powerMin',       label:'Power Min',      min:1,   max:50,  step:1, type:'spin'},
  {key:'powerMax',       label:'Power Max',      min:20,  max:140, step:1, type:'spin'},
];

/* =========================================================
   GAME STATE
   ========================================================= */
let buildings = [];
let groundY = H - 40;
let gorillas = [];   // {x, y (feet on roof), team, alive, color, reaction}
let wind = {v:0, dir:1}; // dir 1 = right (→), -1 = left (←)
let current = 0;     // active player index 0/1
let angle = 45;      // degrees
let power = 50;      // 0..100 user-set
let state = 'start'; // start | aiming | flying | boom | over
let projectile = null;
let particles = [];
let scorch = null;
let blastRing = null;
let score = [0,0];
let roundsToWin = 1;
let roundsPlayed = 0;
let lastExplosion = null;

// seeded RNG so layouts/replays are reproducible
let seed = (Math.random()*1e9)|0;
function rnd(){ seed = (seed*1103515245 + 12345) & 0x7fffffff; return seed/0x7fffffff; }
function ri(min,max){ return Math.floor(rnd()*(max-min+1))+min; }

/* =========================================================
   CITY GENERATION
   ========================================================= */
function generateCity(){
  buildings = [];
  const n = Math.max(4, Math.min(20, CFG.buildingCount));
  const minH = CFG.buildingMinH, maxH = Math.max(minH+40, CFG.buildingMaxH);
  const totalGap = n + 1;
  const minGap = 26, maxGap = 70;
  // distribute width among buildings + gaps
  let widths = [];
  let gaps = [];
  let remaining = W;
  for(let i=0;i<n;i++){
    widths.push(ri(60, 140));
  }
  for(let i=0;i<=n;i++){
    gaps.push(ri(minGap,maxGap));
  }
  // scale to fit
  let wsum = widths.reduce((a,b)=>a+b,0);
  let gsum = gaps.reduce((a,b)=>a+b,0);
  let total = wsum + gsum;
  let scale = W / total;
  // rebuild scaled
  let x = 0;
  const scaledGaps = gaps.map(g=>Math.round(g*scale));
  const scaledW = widths.map(w=>Math.max(40,Math.round(w*scale)));
  // left gap
  let gx = scaledGaps[0];
  for(let i=0;i<n;i++){
    const h = ri(minH, maxH);
    const w = scaledW[i];
    const color = buildingColor(i, n);
    buildings.push({x: gx, y: groundY - h, w: w, h: h, color});
    x = gx + w;
    gx += x + scaledGaps[i+1] - x; // advance
    // simpler:
  }
  // recompute cleanly
  buildings = [];
  let cx = scaledGaps[0];
  for(let i=0;i<n;i++){
    const w = scaledW[i];
    const h = ri(minH, maxH);
    const color = buildingColor(i, n);
    buildings.push({x: cx, y: groundY - h, w: w, h: h, color});
    cx += w + scaledGaps[i+1];
  }
}

function buildingColor(i,n){
  const palette = ['#4b5a8a','#5a4d86','#864d5a','#867a4d','#4d8678',
                   '#6a4d86','#864d7a','#5a864d','#4d6a86','#865e4d',
                   '#7a5a4d','#4d8686'];
  return palette[i % palette.length];
}

function placeGorillas(){
  // one on a left-side building, one on a right-side building
  const n = buildings.length;
  let li = ri(0, Math.floor(n*0.45));
  let ri_ = Math.max(Math.floor(n*0.55), n-1);
  // ensure distinct
  if(ri_ === li) ri_ = Math.min(n-1, li+1);
  const g0 = buildings[li];
  const g1 = buildings[ri_];
  gorillas = [
    {x: g0.x + g0.w/2, y: g0.y, team:0, color:'#ff5252', alive:true, reaction:0},
    {x: g1.x + g1.w/2, y: g1.y, team:1, color:'#4d8dff', alive:true, reaction:0}
  ];
  // ensure far enough apart (not in initial blast range)
  const dx = Math.abs(gorillas[0].x - gorillas[1].x);
  if(dx < CFG.blastRadius*3 + 120){
    // move them to outermost
    gorillas[0].x = buildings[0].x + buildings[0].w/2;
    gorillas[0].y = buildings[0].y;
    gorillas[1].x = buildings[n-1].x + buildings[n-1].w/2;
    gorillas[1].y = buildings[n-1].y;
  }
}

/* =========================================================
   WIND
   ========================================================= */
function setWind(random){
  if(random && CFG.windShift){
    const mag = CFG.windMin + rnd()*(CFG.windMax - CFG.windMin);
    wind.dir = rnd() < 0.5 ? 1 : -1;
    wind.v = mag * wind.dir;
  } else {
    wind.v = 0;
  }
  updateWindHUD();
}

function updateWindHUD(){
  const speed = Math.abs(wind.v);
  document.getElementById('windVal').textContent = (speed).toFixed(1);
  const arrow = document.getElementById('windArrow');
  const dir = document.getElementById('windDir');
  arrow.style.transform = 'rotate(' + (wind.v < 0 ? 180 : 0) + 'deg)';
  dir.textContent = wind.v < 0 ? '←' : '→';
  document.getElementById('windDir').style.color = Math.abs(wind.v) < 0.05 ? '#5c6b95' : '#e8ecff';
}

/* =========================================================
   RENDERING
   ========================================================= */
let clouds = [];
function makeClouds(){
  clouds = [];
  for(let i=0;i<5;i++){
    clouds.push({x: rnd()*W, y: 40+rnd()*180, s: 0.5+rnd()*0.9, spd: 0.05+rnd()*0.12});
  }
}
const sunX = 780, sunY = 110;

function drawBackground(){
  // sky gradient
  const g = ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#243a6b');
  g.addColorStop(0.45,'#3f5a9c');
  g.addColorStop(0.75,'#8aa0c9');
  g.addColorStop(1,'#c9d6ea');
  ctx.fillStyle = g;
  ctx.fillRect(0,0,W,H);
  // sun/moon
  ctx.save();
  ctx.globalAlpha = 0.9;
  const sg = ctx.createRadialGradient(sunX,sunY,10,sunX,sunY,70);
  sg.addColorStop(0,'#fff4c2');
  sg.addColorStop(0.4,'#ffe07a');
  sg.addColorStop(1,'rgba(255,200,90,0)');
  ctx.fillStyle = sg;
  ctx.beginPath(); ctx.arc(sunX,sunY,70,0,Math.PI*2); ctx.fill();
  ctx.globalAlpha = 1;
  ctx.fillStyle = '#fff0b0';
  ctx.beginPath(); ctx.arc(sunX,sunY,34,0,Math.PI*2); ctx.fill();
  ctx.restore();
  // clouds
  for(const c of clouds){
    drawCloud(c.x, c.y, c.s);
    c.x += c.spd * (wind.v*0.3 + 0.2);
    if(c.x > W+120) c.x = -120;
  }
}
function drawCloud(x,y,s){
  ctx.save();
  ctx.globalAlpha = 0.75;
  ctx.fillStyle = '#f2f6ff';
  ctx.translate(x,y); ctx.scale(s,s);
  ctx.beginPath();
  ctx.arc(0,0,22,0,Math.PI*2);
  ctx.arc(24,6,18,0,Math.PI*2);
  ctx.arc(-24,6,18,0,Math.PI*2);
  ctx.arc(0,10,26,0,Math.PI*2);
  ctx.fill();
  ctx.restore();
}

function drawBuildings(){
  for(const b of buildings){
    // body
    ctx.fillStyle = b.color;
    ctx.fillRect(b.x, b.y, b.w, b.h);
    // shaded side
    ctx.fillStyle = 'rgba(0,0,0,0.18)';
    ctx.fillRect(b.x + b.w*0.72, b.y, b.w*0.28, b.h);
    // roof line highlight
    ctx.fillStyle = 'rgba(255,255,255,0.14)';
    ctx.fillRect(b.x, b.y, b.w, 5);
    // windows
    ctx.fillStyle = 'rgba(255,240,150,0.5)';
    const cols = Math.max(1, Math.floor(b.w/26));
    const rows = Math.max(1, Math.floor(b.h/34));
    const pw = (b.w - 20) / cols;
    for(let r=0;r<rows;r++){
      for(let cc=0;cc<cols;cc++){
        const wx = b.x + 12 + cc*pw + (pw-10)/2;
        const wy = b.y + 16 + r*34;
        if(wy < b.y + b.h - 8){
          ctx.fillStyle = (r+cc)%3===0 ? 'rgba(120,150,200,0.35)' : 'rgba(255,240,150,0.45)';
          ctx.fillRect(wx, wy, 10, 18);
        }
      }
    }
  }
  // ground
  ctx.fillStyle = '#2c3757';
  ctx.fillRect(0, groundY, W, H-groundY);
  ctx.fillStyle = '#3a4a72';
  ctx.fillRect(0, groundY, W, 6);
}

function drawGorilla(g){
  if(!g.alive) return;
  const x = g.x, y = g.y; // feet position
  const c = g.color;
  const scale = g.team===0 ? 1 : -1;
  ctx.save();
  ctx.translate(x, y);
  ctx.scale(scale, 1);
  const reaction = g.reaction; // 0..1 pulse
  // shadow ellipse
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.beginPath(); ctx.ellipse(0, 2, 20, 6, 0, 0, Math.PI*2); ctx.fill();

  // legs
  ctx.fillStyle = '#3a2f26';
  ctx.fillRect(-12, -14, 8, 14);
  ctx.fillRect(4, -14, 8, 14);
  // body
  ctx.fillStyle = c;
  roundRect(-14, -40, 28, 30, 6); ctx.fill();
  // belly
  ctx.fillStyle = 'rgba(255,255,255,0.18)';
  roundRect(-9, -34, 18, 20, 5); ctx.fill();
  // arms
  ctx.fillStyle = c;
  ctx.fillRect(-20, -38, 7, 22);
  ctx.fillRect(13, -38, 7, 22);
  // head
  ctx.fillStyle = c;
  ctx.beginPath(); ctx.arc(0, -50, 13, 0, Math.PI*2); ctx.fill();
  // face
  ctx.fillStyle = '#2a211a';
  ctx.beginPath(); ctx.ellipse(0, -47, 8, 9, 0, 0, Math.PI*2); ctx.fill();
  // muzzle
  ctx.fillStyle = '#5a4a3c';
  ctx.beginPath(); ctx.ellipse(0, -42, 5, 4, 0, 0, Math.PI*2); ctx.fill();
  // eyes
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(-5, -53, 3, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(5, -53, 3, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = '#111';
  ctx.beginPath(); ctx.arc(-4, -53, 1.5, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(6, -53, 1.5, 0, Math.PI*2); ctx.fill();
  // ears
  ctx.fillStyle = c;
  ctx.beginPath(); ctx.arc(-12, -54, 4, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(12, -54, 4, 0, Math.PI*2); ctx.fill();

  // reaction: arms up when celebrating
  if(reaction > 0){
    ctx.fillStyle = c;
    const lift = reaction*10;
    ctx.fillRect(-20, -40-lift, 7, 6);
    ctx.fillRect(13, -40-lift, 7, 6);
  }
  ctx.restore();
}

function roundRect(x,y,w,h,r){
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,x+w,y+h,r);
  ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r);
  ctx.arcTo(x,y,x+w,y,r);
  ctx.closePath();
}

function drawProjectile(){
  if(!projectile) return;
  // trail
  ctx.save();
  for(let i=0;i<projectile.trail.length;i++){
    const t = projectile.trail[i];
    const a = i / projectile.trail.length;
    ctx.globalAlpha = a*0.6;
    ctx.fillStyle = '#ffd23f';
    ctx.beginPath();
    ctx.arc(t.x, t.y, 5*a+1, 0, Math.PI*2);
    ctx.fill();
  }
  ctx.restore();
  // banana bomb
  ctx.save();
  ctx.translate(projectile.x, projectile.y);
  ctx.rotate(projectile.a);
  ctx.fillStyle = '#ffe13d';
  ctx.strokeStyle = '#c9a400';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(-8,-3);
  ctx.quadraticCurveTo(0,-12, 9,-3);
  ctx.quadraticCurveTo(4,2, -2,2);
  ctx.quadraticCurveTo(-7,1, -8,-3);
  ctx.fill(); ctx.stroke();
  // tip
  ctx.fillStyle = '#8a6d1a';
  ctx.beginPath(); ctx.arc(8,-3,2,0,Math.PI*2); ctx.fill();
  ctx.restore();
}

function drawAimGuide(){
  if(state !== 'aiming') return;
  const g = gorillas[current];
  const dir = current===0 ? 1 : -1;
  const rad = angle*Math.PI/180;
  const vx = Math.cos(rad)*power*1.15*dir;
  const vy = -Math.sin(rad)*power*1.15;
  // simulate a few steps to show trajectory (ignores buildings for preview)
  let px=g.x, py=g.y-46, pvx=vx, pvy=vy;
  ctx.save();
  ctx.setLineDash([4,6]);
  ctx.strokeStyle = 'rgba(255,255,255,0.5)';
  ctx.beginPath(); ctx.moveTo(px,py);
  for(let i=0;i<90;i++){
    pvy += CFG.gravity;
    pvx += wind.v*0.02;
    px += pvx; py += pvy;
    if(py > groundY) break;
    ctx.lineTo(px,py);
  }
  ctx.stroke();
  // arrow
  ctx.restore();
  // barrel
  ctx.save();
  ctx.translate(g.x, g.y-40);
  ctx.scale(dir,1);
  ctx.rotate(-rad);
  ctx.fillStyle = '#333';
  ctx.fillRect(0,-4,26,8);
  ctx.restore();
}

function drawExplosion(){
  if(blastRing){
    const r = blastRing.r;
    const g = ctx.createRadialGradient(blastRing.x,blastRing.y,0,blastRing.x,blastRing.y,r);
    g.addColorStop(0,'#fff6c0');
    g.addColorStop(0.4,'#ffb020');
    g.addColorStop(0.75,'#ff5018');
    g.addColorStop(1,'rgba(120,20,0,0)');
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(blastRing.x,blastRing.y,r,0,Math.PI*2); ctx.fill();
  }
  // scorch marker
  if(scorch){
    ctx.save();
    ctx.globalAlpha = scorch.alpha;
    ctx.fillStyle = '#241a12';
    ctx.beginPath(); ctx.arc(scorch.x,scorch.y, scorch.r, 0, Math.PI*2); ctx.fill();
    ctx.globalAlpha = scorch.alpha*0.6;
    ctx.fillStyle = '#3a2a1a';
    ctx.beginPath(); ctx.arc(scorch.x,scorch.y, scorch.r*0.6, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
  // particles
  for(const p of particles){
    ctx.save();
    ctx.globalAlpha = p.life;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x-2.5, p.y-2.5, 5, 5);
    ctx.restore();
  }
}

/* =========================================================
   PHYSICS / TURN
   ========================================================= */
function fire(){
  if(state !== 'aiming') return;
  const g = gorillas[current];
  const dir = current===0 ? 1 : -1;
  const rad = angle*Math.PI/180;
  const speed = power*1.15;
  projectile = {
    x: g.x + dir*22,
    y: g.y - 44,
    vx: Math.cos(rad)*speed*dir,
    vy: -Math.sin(rad)*speed,
    a: rad,
    trail: []
  };
  state = 'flying';
  lastExplosion = null;
  spawnSound(660, 0.08);
}

function stepProjectile(){
  if(!projectile || state !== 'flying') return;
  const p = projectile;
  // integrate
  p.vy += CFG.gravity;
  p.vx += wind.v*0.02;
  p.x += p.vx;
  p.y += p.vy;
  p.a += p.vx*0.01;
  p.trail.push({x:p.x,y:p.y});
  if(p.trail.length > 24) p.trail.shift();

  // off screen (top or sides) -> explode
  if(p.x < -40 || p.x > W+40){
    explode(p.x, p.y); return;
  }
  // ground
  if(p.y >= groundY){
    explode(p.x, groundY); return;
  }
  // buildings (AABB) - stop at first collision
  for(const b of buildings){
    if(p.x > b.x && p.x < b.x+b.w && p.y > b.y && p.y < b.y+b.h){
      explode(p.x, p.y); return;
    }
  }
  // gorillas (direct hit)
  for(let i=0;i<gorillas.length;i++){
    const g = gorillas[i];
    if(g.alive && Math.abs(p.x-g.x) < 16 && Math.abs(p.y-(g.y-24)) < 30){
      explode(p.x, p.y); return;
    }
  }
}

function explode(x,y){
  state = 'boom';
  projectile = null;
  lastExplosion = {x:x, y:y};
  blastRing = {x:x, y:y, r:1, max: CFG.blastRadius*1.7, t:0};
  scorch = {x:x, y:y, r: CFG.blastRadius*0.55, alpha:0.9};
  // particles
  particles = [];
  const cols = ['#ffb020','#ff5018','#fff6c0','#8a4a10','#ffcf40'];
  for(let i=0;i<40;i++){
    const an = rnd()*Math.PI*2;
    const sp = 1+rnd()*6;
    particles.push({
      x:x, y:y,
      vx: Math.cos(an)*sp, vy: Math.sin(an)*sp - 2,
      life: 1, color: cols[ri(0,cols.length-1)],
      decay: 0.01+rnd()*0.025
    });
  }
  spawnSound(120, 0.25, 'sawtooth');
  // resolve damage immediately
  resolveExplosion(x,y);
}

function resolveExplosion(x,y){
  let kills = [];
  for(let i=0;i<gorillas.length;i++){
    const g = gorillas[i];
    if(!g.alive) continue;
    const d = Math.hypot(g.x - x, (g.y-24) - y);
    let dead = false;
    if(d <= CFG.blastRadius){
      dead = true;
    } else if(CFG.damageMode === 'falloff'){
      // falloff: partial damage beyond radius
      const outer = CFG.blastRadius*1.8;
      if(d < outer){
        const falloff = 1 - (d - CFG.blastRadius)/(outer - CFG.blastRadius);
        if(falloff > 0.5) dead = true;
      }
    }
    if(dead){ g.alive = false; g.reaction = 0; kills.push(i); }
  }
  setTimeout(()=>{
    if(kills.length){
      endRound(kills);
    } else {
      // turn passes
      state = 'aiming';
      switchTurn();
    }
  }, 750);
}

function switchTurn(){
  current = current===0 ? 1 : 0;
  roundsPlayed++;
  setWind(true);
  updateTurnHUD();
}

function endRound(kills){
  const winner = current; // whoever fired
  score[winner]++;
  const loser = kills[0];
  gorillas[loser].reaction = 0;
  gorillas[winner].reaction = 1;
  updateScoreHUD();
  state = 'over';
  spawnSound(523, 0.12); setTimeout(()=>spawnSound(659,0.12),120); setTimeout(()=>spawnSound(784,0.2),240);
  const modal = document.getElementById('modal');
  const wGor = gorillas[winner];
  document.getElementById('mTitle').textContent = 'GORILLAZ';
  document.getElementById('mWinner').textContent = 'Player '+(winner+1)+' wins the round!';
  document.getElementById('mWinner').style.color = wGor.color;
  document.getElementById('mText').textContent =
    'Score — P1: '+score[0]+'  •  P2: '+score[1];
  const actions = document.getElementById('mActions');
  actions.innerHTML = '';
  const need = Math.ceil(roundsToWin/2);
  const finished = score[0]>=need || score[1]>=need;
  if(finished){
    const btn = document.createElement('button');
    btn.className = 'btn primary big';
    btn.textContent = 'New Session';
    btn.onclick = ()=>{ modal.classList.remove('show'); startSession(roundsToWin, true); };
    actions.appendChild(btn);
  } else {
    const rem = document.createElement('button');
    rem.className = 'btn primary big';
    rem.textContent = 'Rematch';
    rem.onclick = ()=>{ modal.classList.remove('show'); startRound(); };
    actions.appendChild(rem);
    const reset = document.createElement('button');
    reset.className = 'btn big';
    reset.textContent = 'Reset Score';
    reset.onclick = ()=>{ modal.classList.remove('show'); startSession(roundsToWin, true); };
    actions.appendChild(reset);
  }
  modal.classList.add('show');
}

/* =========================================================
   HUD helpers
   ========================================================= */
function updateTurnHUD(){
  const el = document.getElementById('turn');
  el.textContent = 'Player '+(current+1)+' Turn';
  el.style.color = current===0 ? 'var(--p1)' : 'var(--p2)';
  document.getElementById('angVal').textContent = angle;
  document.getElementById('powVal').textContent = power;
}
function updateScoreHUD(){
  document.getElementById('score1').textContent = score[0];
  document.getElementById('score2').textContent = score[1];
}

/* =========================================================
   SESSION / MATCH FLOW
   ========================================================= */
function startSession(mode, fresh){
  roundsToWin = mode;
  if(fresh){ score = [0,0]; roundsPlayed = 0; }
  updateScoreHUD();
  document.getElementById('start').classList.add('hide');
  startRound();
}

function startRound(){
  seed = (Math.random()*1e9)|0;
  generateCity();
  makeClouds();
  placeGorillas();
  current = (roundsPlayed % 2); // alternate who fires first
  angle = current===0 ? 45 : 45;
  power = 50;
  state = 'aiming';
  projectile = null; blastRing=null; scorch=null; particles=[];
  setWind(false); // wind starts at 0, shifts when turn passes
  updateTurnHUD();
  updateScoreHUD();
}

/* =========================================================
   INPUT
   ========================================================= */
document.addEventListener('keydown', function(e){
  if(state === 'aiming'){
    if(e.key === 'ArrowLeft'){ angle = Math.max(0, angle-1); e.preventDefault(); }
    else if(e.key === 'ArrowRight'){ angle = Math.min(90, angle+1); e.preventDefault(); }
    else if(e.key === 'ArrowUp'){ power = Math.min(100, power+1); e.preventDefault(); }
    else if(e.key === 'ArrowDown'){ power = Math.max(CFG.powerMin, power-1); e.preventDefault(); }
    else if(e.key === ' '){ fire(); e.preventDefault(); }
    updateAimHUD();
  }
});
function updateAimHUD(){
  document.getElementById('angVal').textContent = angle;
  document.getElementById('powVal').textContent = power;
}
// clamp power to configured max on the fly
function clampPower(){ if(power > CFG.powerMax) power = CFG.powerMax; if(power < CFG.powerMin) power = CFG.powerMin; }

// mouse fire button via hint
document.getElementById('hint').addEventListener('click', function(){ if(state==='aiming') fire(); });

/* =========================================================
   TUNING PANEL
   ========================================================= */
const tuneEl = document.getElementById('tune');
document.getElementById('gear').addEventListener('click', ()=>{ tuneEl.classList.toggle('open'); });
document.getElementById('closeTune').addEventListener('click', ()=>{ tuneEl.classList.remove('open'); });

function buildTunePanel(){
  const body = document.getElementById('tuneBody');
  body.innerHTML = '';
  for(const f of TUNE_FIELDS){
    const grp = document.createElement('div');
    grp.className = 'grp';
    grp.innerHTML = '<div class="title">'+f.label+'</div>';
    const row = document.createElement('div');
    row.className = 'row';
    // spin control
    const spin = document.createElement('div');
    spin.className = 'spin';
    const dec = document.createElement('button'); dec.textContent = '−';
    const val = document.createElement('div'); val.className = 'v'; val.id = 't-'+f.key; val.textContent = fmt(CFG[f.key]);
    const inc = document.createElement('button'); inc.textContent = '+';
    dec.onclick = ()=>{ CFG[f.key] = roundStep(CFG[f.key], f.step, false); applyTune(f.key); };
    inc.onclick = ()=>{ CFG[f.key] = roundStep(CFG[f.key], f.step, true); applyTune(f.key); };
    spin.appendChild(dec); spin.appendChild(val); spin.appendChild(inc);
    row.appendChild(spin);
    grp.appendChild(row);
    // direct number entry
    const enter = document.createElement('div');
    enter.className='row';
    const inp = document.createElement('input');
    inp.type='number'; inp.step=f.step; inp.min=f.min; inp.max=f.max; inp.value=CFG[f.key];
    inp.onchange = ()=>{
      let v = parseFloat(inp.value);
      if(isNaN(v)) return;
      v = Math.min(f.max, Math.max(f.min, v));
      CFG[f.key] = v; applyTune(f.key);
    };
    enter.appendChild(inp);
    grp.appendChild(enter);
    body.appendChild(grp);
  }
  // damage mode checkbox / select
  const dm = document.createElement('div');
  dm.className='grp';
  dm.innerHTML = '<div class="title">Explosion Damage</div>';
  const chk = document.createElement('div');
  chk.className='chk';
  chk.innerHTML = '<input type="checkbox" id="dmgFalloff" '+(CFG.damageMode==='falloff'?'checked':'')+'><label for="dmgFalloff">Allow falloff (partial damage outside radius)</label>';
  chk.querySelector('input').onchange = function(){
    CFG.damageMode = this.checked ? 'falloff' : 'hard';
  };
  dm.appendChild(chk);
  body.appendChild(dm);

  // wind shift checkbox
  const wm = document.createElement('div');
  wm.className='grp';
  wm.innerHTML = '<div class="title">Wind</div>';
  const cw = document.createElement('div');
  cw.className='chk';
  cw.innerHTML = '<input type="checkbox" id="windShiftC" '+(CFG.windShift?'checked':'')+'><label for="windShiftC">Shift wind between turns</label>';
  cw.querySelector('input').onchange = function(){
    CFG.windShift = this.checked;
    if(!this.checked){ wind.v = 0; updateWindHUD(); }
  };
  wm.appendChild(cw);
  body.appendChild(wm);
}
function fmt(v){ return Number.isInteger(v) ? v.toString() : (+v).toFixed(2); }
function roundStep(v, step, up){
  const n = up ? Math.ceil(v/step - 1e-9)*step : Math.floor(v/step + 1e-9)*step;
  return Math.max(0, n);
}
function applyTune(){
  // reflect numeric spin value
  for(const f of TUNE_FIELDS){
    const el = document.getElementById('t-'+f.key);
    if(el) el.textContent = fmt(CFG[f.key]);
  }
  if(power > CFG.powerMax) power = CFG.powerMax;
  if(power < CFG.powerMin) power = CFG.powerMin;
  updateAimHUD();
}
document.getElementById('resetBtn').onclick = function(){
  CFG = Object.assign({}, DEFAULTS);
  buildTunePanel();
  applyTune();
};
document.getElementById('applyBtn').onclick = function(){
  applyTune();
  // apply building height bounds ordering
  if(CFG.buildingMinH > CFG.buildingMaxH){ const t=CFG.buildingMinH; CFG.buildingMinH=CFG.buildingMaxH; CFG.buildingMaxH=t; }
  startSession(roundsToWin, true);
};

/* =========================================================
   START MODE SELECTOR
   ========================================================= */
document.querySelectorAll('#start .mode').forEach(m=>{
  m.addEventListener('click', function(){
    const mode = parseInt(this.dataset.mode,10);
    roundsToWin = mode;
    startSession(mode, true);
  });
});

/* =========================================================
   AUDIO (simple WebAudio blips; safe no-op if unavailable)
   ========================================================= */
let actx = null;
function initAudio(){
  try{ actx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){}
}
document.addEventListener('click', ()=>{ if(!actx) initAudio(); if(actx && actx.state==='suspended') actx.resume(); }, {once:false});
function spawnSound(freq, dur, type){
  if(!actx) return;
  try{
    const o = actx.createOscillator();
    const g = actx.createGain();
    o.type = type || 'square';
    o.frequency.value = freq;
    g.gain.setValueAtTime(0.12, actx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, actx.currentTime+dur);
    o.connect(g); g.connect(actx.destination);
    o.start(); o.stop(actx.currentTime+dur);
  }catch(e){}
}

/* =========================================================
   MAIN LOOP
   ========================================================= */
let lastT = performance.now();
function loop(now){
  const dt = Math.min(50, now - lastT);
  lastT = now;

  stepProjectile();
  // update explosion rings / particles
  if(blastRing){
    blastRing.t += dt;
    blastRing.r = Math.min(blastRing.max, blastRing.r + dt*0.06);
  }
  for(const p of particles){
    p.vy += 0.2;
    p.x += p.vx; p.y += p.vy;
    p.life -= p.decay;
  }
  particles = particles.filter(p=>p.life>0);
  if(scorch) scorch.alpha = Math.max(0.4, scorch.alpha - 0.002);

  // gorilla reaction decay
  for(const g of gorillas){
    if(g.alive && g.reaction>0){ g.reaction = Math.max(0.3, g.reaction - 0.01); }
  }

  // render
  drawBackground();
  drawBuildings();
  drawAimGuide();
  for(const g of gorillas) drawGorilla(g);
  drawProjectile();
  drawExplosion();

  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

/* =========================================================
   INIT
   ========================================================= */
buildTunePanel();
updateTurnHUD();
updateScoreHUD();
updateWindHUD();
makeClouds();
fitStage();
// seed RNG
seed = (Math.random()*1e9)|0;

})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"ed2b2364-5c52-44ae-bd08-017c81b5a6c2","tokensIn":3943257,"tokensOut":31950,"tokensTotal":3975207,"cost":0,"turns":67,"toolCalls":66,"failedToolCalls":10,"timestamp":"2026-08-23T22:28:40.778Z"} -->