Gorillaz
Ornith 1.5 35B A3B · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 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: ◀ ▶ • Power: ▲ ▼ • 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 & 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"} -->