Worms
Gemma 4 31b · composer

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 26,605 bytes · SHA-256 1662b42be8a6
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> artillery.io - Destructible Warfare</title>
<style>
/* ── css: layout ── */
body {
margin: 0;
overflow: hidden;
background: #1a1a2e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none;
}
#game-container {
position: relative;
width: 100vw;
height: 100vh;
}
#gameCanvas {
display: block;
background: linear-gradient(to bottom, #4facfe 0%, #00f2fe 100%);
cursor: crosshair;
}
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
#turn-indicator {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
font-weight: bold;
padding: 10px 20px;
background: rgba(0,0,0,0.4);
border-radius: 20px;
transition: all 0.3s;
}
#timer-bar-container {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
width: 300px;
height: 10px;
background: rgba(255,255,255,0.3);
border-radius: 5px;
overflow: hidden;
}
#timer-bar {
width: 100%;
height: 100%;
background: #ffeb3b;
transition: width 0.1s linear;
}
#unit-info {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0,0,0,0.6);
padding: 15px;
border-radius: 10px;
width: 200px;
}
#unit-name {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
#health-bar-container {
width: 100%;
height: 15px;
background: #444;
border-radius: 5px;
overflow: hidden;
}
#health-bar {
width: 100%;
height: 100%;
background: #4caf50;
transition: width 0.3s;
}
#weapon-selector {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px;
pointer-events: auto;
}
.weapon-slot {
width: 50px;
height: 50px;
background: rgba(0,0,0,0.6);
border: 2px solid transparent;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
}
.weapon-slot.active {
border-color: #fff;
transform: scale(1.2);
background: rgba(0,0,0,0.8);
}
.weapon-slot:hover {
background: rgba(0,0,0,0.8);
}
#wind-indicator {
position: absolute;
top: 20px;
right: 20px;
font-size: 18px;
background: rgba(0,0,0,0.4);
padding: 5px 15px;
border-radius: 10px;
}
#menu-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
z-index: 100;
}
#menu-overlay h1 {
font-size: 64px;
margin-bottom: 40px;
letter-spacing: 5px;
color: #ffeb3b;
text-shadow: 0 0 20px rgba(255,235,59,0.5);
}
.menu-options {
display: flex;
flex-direction: column;
gap: 20px;
font-size: 20px;
}
.menu-options label {
display: flex;
gap: 15px;
align-items: center;
}
.menu-options input {
width: 50px;
padding: 5px;
border-radius: 5px;
border: none;
}
#start-btn {
margin-top: 30px;
padding: 15px 40px;
font-size: 24px;
background: #4caf50;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
transition: transform 0.2s;
}
#start-btn:hover {
transform: scale(1.1);
background: #66bb6a;
}
#game-over {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.9);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
z-index: 200;
}
#game-over h2 {
font-size: 48px;
margin-bottom: 20px;
}
#game-over button {
padding: 10px 30px;
font-size: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div id="turn-indicator">Team 1's Turn</div>
<div id="timer-bar-container">
<div id="timer-bar"></div>
</div>
<div id="unit-info">
<span id="unit-name">Worm 1</span>
<div id="health-bar-container"><div id="health-bar"></div></div>
</div>
<div id="weapon-selector">
<div class="weapon-slot active" data-weapon="bazooka">🚀</div>
<div class="weapon-slot" data-weapon="cluster">💣</div>
<div class="weapon-slot" data-weapon="laser">⚡</div>
<div class="weapon-slot" data-weapon="nuke">☢️</div>
<div class="weapon-slot" data-weapon="airstrike">✈️</div>
<div class="weapon-slot" data-weapon="teleport">🌀</div>
</div>
<div id="wind-indicator">Wind: ➔ 0</div>
</div>
<div id="menu-overlay">
<h1>ARTILLERY.IO</h1>
<div class="menu-options">
<label>Teams: <input type="number" id="team-count" value="2" min="2" max="4"></label>
<label>Units per Team: <input type="number" id="units-per-team" value="3" min="1" max="4"></label>
<label>CPU Teams: <input type="number" id="cpu-count" value="1" min="0" max="4"></label>
<button id="start-btn">BATTLE!</button>
</div>
</div>
<div id="game-over">
<h2 id="winner-text">Team 1 Wins!</h2>
<button onclick="location.reload()">Play Again</button>
</div>
</div>
<script>
'use strict';
// ── const: CONFIG ──
const CONFIG = {
GRAVITY: 0.25,
FRICTION: 0.98,
WATER_LEVEL: 0.8, // Fraction of screen height
MAX_TURN_TIME: 30, // seconds
WORM_RADIUS: 6,
TERRAIN_RESOLUTION: 2,
EXPLOSION_MAX_RADIUS: 60,
DAMAGE_FALL_THRESHOLD: 30,
DAMAGE_FALL_MULTIPLIER: 1.5,
WIND_MAX: 0.15,
COLORS: {
teams: ['#ff4d4d', '#4dff4d', '#4d4dff', '#ffff4d'],
terrain: '#5d4037',
water: 'rgba(0, 150, 255, 0.5)',
debris: '#8d6e63'
}
};
// ── class: Vector ──
class Vector {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
add(v) { return new Vector(this.x + v.x, this.y + v.y); }
sub(v) { return new Vector(this.x - v.x, this.y - v.y); }
mul(s) { return new Vector(this.x * s, this.y * s); }
mag() { return Math.sqrt(this.x * this.x + this.y * this.y); }
normalize() {
const m = this.mag();
return m === 0 ? new Vector() : new Vector(this.x / m, this.y / m);
}
static dist(v1, v2) {
return Math.sqrt((v1.x - v2.x)**2 + (v1.y - v2.y)**2);
}
}
// ── class: Terrain ──
class Terrain {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
this.data = new Uint8Array(this.width * this.height);
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCanvas.width = this.width;
this.offscreenCanvas.height = this.height;
this.offCtx = this.offscreenCanvas.getContext('2d', {willReadFrequently: true});
}
generate(type) {
this.offCtx.clearRect(0, 0, this.width, this.height);
this.offCtx.fillStyle = CONFIG.COLORS.terrain;
if (type === 'single_island') {
this.drawBlob(this.width / 2, this.height, this.width * 0.7, this.height * 0.6);
} else if (type === 'two_islands') {
this.drawBlob(this.width * 0.25, this.height, this.width * 0.3, this.height * 0.6);
this.drawBlob(this.width * 0.75, this.height, this.width * 0.3, this.height * 0.6);
} else { // cave
this.offCtx.fillRect(0, 0, this.width, this.height);
this.drawBlob(this.width / 2, this.height / 2, this.width * 0.6, this.height * 0.4, true);
}
this.updateData();
}
drawBlob(cx, cy, w, h, hollow = false) {
this.offCtx.beginPath();
this.offCtx.ellipse(cx, cy, w/2, h/2, 0, 0, Math.PI * 2);
if (hollow) {
this.offCtx.globalCompositeOperation = 'destination-out';
this.offCtx.fill();
this.offCtx.globalCompositeOperation = 'source-over';
} else {
this.offCtx.fill();
}
}
updateData() {
const imgData = this.offCtx.getImageData(0, 0, this.width, this.height).data;
for (let i = 0; i < this.data.length; i++) {
this.data[i] = imgData[i * 4 + 3] > 128 ? 1 : 0;
}
}
destroy(x, y, radius) {
for (let i = -radius; i < radius; i++) {
for (let j = -radius; j < radius; j++) {
const dx = i;
const dy = j;
if (dx * dx + dy * dy < radius * radius) {
const tx = Math.floor(x + dx);
const ty = Math.floor(y + dy);
if (tx >= 0 && tx < this.width && ty >= 0 && ty < this.height) {
this.data[ty * this.width + tx] = 0;
}
}
}
}
}
isSolid(x, y) {
const tx = Math.floor(x);
const ty = Math.floor(y);
if (tx < 0 || tx >= this.width || ty < 0 || ty >= this.height) return false;
return this.data[ty * this.width + tx] === 1;
}
render(mainCtx) {
mainCtx.putImageData(this.offCtx.getImageData(0, 0, this.width, this.height), 0, 0);
// Actually we need to render based on the updated this.data
// Since putImageData is slow, we can use an offscreen canvas and modify pixels
const imgData = this.offCtx.getImageData(0, 0, this.width, this.height);
const d = imgData.data;
const terrainColor = {r: 93, g: 64, b: 55};
for (let i = 0; i < this.data.length; i++) {
const idx = i * 4;
if (this.data[i]) {
d[idx] = terrainColor.r;
d[idx+1] = terrainColor.g;
d[idx+2] = terrainColor.b;
d[idx+3] = 255;
} else {
d[idx+3] = 0;
}
}
mainCtx.putImageData(imgData, 0, 0);
}
}
// ── class: Worm ──
class Worm {
constructor(id, teamId, x, y) {
this.id = id;
this.teamId = teamId;
this.pos = new Vector(x, y);
this.vel = new Vector();
this.health = 100;
this.radius = CONFIG.WORM_RADIUS;
this.isDead = false;
this.color = CONFIG.COLORS.teams[teamId];
}
update(terrain, wind) {
if (this.isDead) return;
this.vel.y += CONFIG.GRAVITY;
this.vel.x += wind;
const nextX = this.pos.x + this.vel.x;
const nextY = this.pos.y + this.vel.y;
if (terrain.isSolid(nextX, this.pos.y)) {
this.vel.x *= -0.2;
} else {
this.pos.x = nextX;
}
if (terrain.isSolid(this.pos.x, nextY)) {
if (this.vel.y > CONFIG.DAMAGE_FALL_THRESHOLD) {
this.takeDamage((this.vel.y - CONFIG.DAMAGE_FALL_THRESHOLD) * CONFIG.DAMAGE_FALL_MULTIPLIER);
}
this.vel.y = 0;
} else {
this.pos.y = nextY;
}
this.vel.x *= CONFIG.FRICTION;
}
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
this.health = 0;
this.isDead = true;
}
}
draw(ctx) {
if (this.isDead) return;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.stroke();
}
}
// ── class: Projectile ──
// We need to wrap the Projectile.explode change into a function or
// better yet, just redefine the Projectile class with the new explode method.
class Projectile {
constructor(x, y, vx, vy, type, teamId, ownerId) {
this.pos = new Vector(x, y);
this.vel = new Vector(vx, vy);
this.type = type;
this.teamId = teamId;
this.ownerId = ownerId;
this.isDead = false;
this.timer = 0;
}
update(terrain, wind, game) {
this.vel.y += CONFIG.GRAVITY;
this.vel.x += wind;
const nextX = this.pos.x + this.vel.x;
const nextY = this.pos.y + this.vel.y;
if (terrain.isSolid(nextX, nextY)) {
this.explode(terrain, game);
this.isDead = true;
return;
}
this.pos.x = nextX;
this.pos.y = nextY;
this.timer++;
if (this.pos.x < 0 || this.pos.x > terrain.width || this.pos.y < 0 || this.pos.y > terrain.height) {
this.isDead = true;
}
}
explode(terrain, game) {
const effects = {
bazooka: { radius: 40, damage: 40 },
cluster: { radius: 20, damage: 20, sub: 5 },
nuke: { radius: 100, damage: 80 },
airstrike: { radius: 80, damage: 60 },
laser: { radius: 10, damage: 30 }
};
const eff = effects[this.type] || effects.bazooka;
game.createExplosion(this.pos.x, this.pos.y, eff.radius);
for (let i = 0; i < 15; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 5 + 2;
game.debris.push(new Debris(
this.pos.x, this.pos.y,
Math.cos(angle) * speed, Math.sin(angle) * speed
));
}
terrain.destroy(this.pos.x, this.pos.y, eff.radius);
game.worms.forEach(w => {
if (w.isDead) return;
const d = Vector.dist(this.pos, w.pos);
if (d < eff.radius + w.radius) {
const damage = eff.damage * (1 - d / (eff.radius + w.radius));
w.takeDamage(damage);
game.createFloatingText(w.pos.x, w.pos.y, Math.floor(damage));
}
});
if (this.type === 'cluster') {
for (let i = 0; i < eff.sub; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 5 + 2;
game.projectiles.push(new Projectile(
this.pos.x, this.pos.y,
Math.cos(angle) * speed, Math.sin(angle) * speed,
'bazooka', this.teamId, this.ownerId
));
}
}
}
draw(ctx) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, 3, 0, Math.PI * 2);
ctx.fill();
}
}
// ── class: Particle ──
class Particle {
constructor(x, y, color) {
this.pos = new Vector(x, y);
this.vel = new Vector((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10);
this.life = 1.0;
this.decay = Math.random() * 0.02 + 0.01;
this.color = color;
}
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
this.vel.y += 0.1;
this.life -= this.decay;
}
draw(ctx) {
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, 2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1.0;
}
}
// ── class: FloatingText ──
class FloatingText {
constructor(x, y, text) {
this.pos = new Vector(x, y);
this.text = text;
this.life = 1.0;
}
update() {
this.pos.y -= 0.5;
this.life -= 0.02;
}
draw(ctx) {
ctx.globalAlpha = this.life;
ctx.fillStyle = 'white';
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.fillText(this.text, this.pos.x, this.pos.y);
ctx.globalAlpha = 1.0;
}
}
// ── class: Debris ──
class Debris {
constructor(x, y, vx, vy) {
this.pos = new Vector(x, y);
this.vel = new Vector(vx, vy);
this.size = Math.random() * 4 + 2;
this.life = 1.0;
this.rot = Math.random() * Math.PI * 2;
this.rotVel = (Math.random() - 0.5) * 0.2;
}
update(terrain) {
this.vel.y += CONFIG.GRAVITY;
const nextX = this.pos.x + this.vel.x;
const nextY = this.pos.y + this.vel.y;
if (terrain.isSolid(nextX, nextY)) {
this.vel.y *= -0.3;
this.vel.x *= 0.8;
} else {
this.pos.x = nextX;
this.pos.y = nextY;
}
this.rot += this.rotVel;
this.life -= 0.005;
}
draw(ctx) {
ctx.save();
ctx.translate(this.pos.x, this.pos.y);
ctx.rotate(this.rot);
ctx.fillStyle = CONFIG.COLORS.debris;
ctx.globalAlpha = this.life;
ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
ctx.restore();
}
}
// ── main ──
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 1280;
canvas.height = 720;
const game = {
terrain: new Terrain(canvas),
worms: [],
projectiles: [],
particles: [],
debris: [],
texts: [],
teams: [],
activeTeam: 0,
activeWormIndex: 0,
turnTime: CONFIG.MAX_TURN_TIME,
timerStarted: false,
wind: 0,
currentWeapon: 'bazooka',
camera: { x: 0, y: 0, zoom: 1 },
screenShake: 0,
state: 'menu'
};
function initGame() {
const teamCount = parseInt(document.getElementById('team-count').value);
const unitsPerTeam = parseInt(document.getElementById('units-per-team').value);
const cpuCount = parseInt(document.getElementById('cpu-count').value);
game.terrain.generate(['single_island', 'two_islands', 'cave'][Math.floor(Math.random() * 3)]);
game.worms = [];
game.teams = [];
for (let t = 0; t < teamCount; t++) {
const team = {
id: t,
isCPU: t >= (teamCount - cpuCount),
alive: true,
color: CONFIG.COLORS.teams[t]
};
game.teams.push(team);
for (let u = 0; u < unitsPerTeam; u++) {
let x, y;
let attempts = 0;
do {
x = Math.random() * canvas.width;
y = Math.random() * canvas.height;
attempts++;
} while (game.terrain.isSolid(x, y) && attempts < 100);
game.worms.push(new Worm(game.worms.length, t, x, y));
}
}
game.state = 'play';
document.getElementById('menu-overlay').style.display = 'none';
startTurn();
}
function startTurn() {
game.timerStarted = true;
game.turnTime = CONFIG.MAX_TURN_TIME;
game.wind = (Math.random() - 0.5) * CONFIG.WIND_MAX * 2;
let found = false;
for (let i = 0; i < game.worms.length; i++) {
if (game.worms[i].teamId === game.activeTeam && !game.worms[i].isDead) {
game.activeWormIndex = i;
found = true;
break;
}
}
if (!found) {
nextTurn();
return;
}
updateHUD();
}
function nextTurn() {
game.activeTeam = (game.activeTeam + 1) % game.teams.length;
startTurn();
}
function updateHUD() {
const worm = game.worms[game.activeWormIndex];
document.getElementById('turn-indicator').innerText = `Team ${game.activeTeam + 1}'s Turn`;
document.getElementById('turn-indicator').style.color = game.teams[game.activeTeam].color;
document.getElementById('unit-name').innerText = `Worm ${game.activeWormIndex + 1}`;
document.getElementById('health-bar').style.width = `${worm.health}%`;
document.getElementById('wind-indicator').innerText = `Wind: ${game.wind > 0 ? '➔' : '←'} ${Math.abs(game.wind * 100).toFixed(1)}`;
}
game.createExplosion = function(x, y, radius) {
game.screenShake = radius / 5;
for (let i = 0; i < radius; i++) {
game.particles.push(new Particle(x, y, Math.random() > 0.5 ? '#ff9800' : '#f44336'));
}
};
game.createFloatingText = function(x, y, text) {
game.texts.push(new FloatingText(x, y, text));
};
let isFiring = false;
let firePower = 0;
let fireAngle = -Math.PI / 4;
window.addEventListener('keydown', e => {
if (game.state !== 'play') return;
const worm = game.worms[game.activeWormIndex];
if (game.teams[game.activeTeam].isCPU) return;
if (e.key === 'a') worm.pos.x -= 3;
if (e.key === 'd') worm.pos.x += 3;
if (e.key === 'w') worm.vel.y = -5;
if (e.key === ' ') {
isFiring = true;
firePower = 0;
}
});
window.addEventListener('keyup', e => {
if (e.key === ' ') {
if (isFiring) {
const worm = game.worms[game.activeWormIndex];
const vx = Math.cos(fireAngle) * firePower;
const vy = Math.sin(fireAngle) * firePower;
game.projectiles.push(new Projectile(worm.pos.x, worm.pos.y, vx, vy, game.currentWeapon, game.activeTeam, game.activeWormIndex));
isFiring = false;
game.timerStarted = false;
setTimeout(nextTurn, 2000);
}
}
});
window.addEventListener('mousemove', e => {
if (game.state !== 'play') return;
const worm = game.worms[game.activeWormIndex];
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const dx = mouseX - (worm.pos.x - game.camera.x);
const dy = mouseY - (worm.pos.y - game.camera.y);
fireAngle = Math.atan2(dy, dx);
});
document.querySelectorAll('.weapon-slot').forEach(slot => {
slot.addEventListener('click', () => {
document.querySelectorAll('.weapon-slot').forEach(s => s.classList.remove('active'));
slot.classList.add('active');
game.currentWeapon = slot.dataset.weapon;
});
});
document.getElementById('start-btn').addEventListener('click', initGame);
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (game.state === 'play') {
const targetWorm = game.worms[game.activeWormIndex];
if (!targetWorm) {
requestAnimationFrame(gameLoop);
return;
}
if (isFiring) {
firePower = Math.min(firePower + 0.2, 15);
}
if (game.timerStarted) {
game.turnTime -= 1/60;
document.getElementById('timer-bar').style.width = `${(game.turnTime / CONFIG.MAX_TURN_TIME) * 100}%`;
if (game.turnTime <= 0) {
game.timerStarted = false;
nextTurn();
}
}
if (game.teams[game.activeTeam].isCPU && game.timerStarted) {
const worm = game.worms[game.activeWormIndex];
const targets = game.worms.filter(w => w.teamId !== game.activeTeam && !w.isDead);
if (targets.length > 0) {
const target = targets[0];
const dx = target.pos.x - worm.pos.x;
const dy = target.pos.y - worm.pos.y;
const angle = Math.atan2(dy, dx);
const dist = Vector.dist(worm.pos, target.pos);
fireAngle = angle - 0.2;
firePower = Math.sqrt(dist) * 0.8;
setTimeout(() => {
game.projectiles.push(new Projectile(worm.pos.x, worm.pos.y, Math.cos(fireAngle) * firePower, Math.sin(fireAngle) * firePower, game.currentWeapon, game.activeTeam, game.activeWormIndex));
game.timerStarted = false;
setTimeout(nextTurn, 2000);
}, 1000);
game.timerStarted = false;
} else {
nextTurn();
}
}
game.camera.x += (targetWorm.pos.x - canvas.width/2 - game.camera.x) * 0.1;
game.camera.y += (targetWorm.pos.y - canvas.height/2 - game.camera.y) * 0.1;
ctx.save();
if (game.screenShake > 0) {
ctx.translate((Math.random()-0.5)*game.screenShake, (Math.random()-0.5)*game.screenShake);
game.screenShake *= 0.9;
}
ctx.translate(-game.camera.x, -game.camera.y);
game.terrain.render(ctx);
game.worms.forEach(w => {
w.update(game.terrain, game.wind);
w.draw(ctx);
});
game.projectiles.forEach((p, i) => {
p.update(game.terrain, game.wind, game);
p.draw(ctx);
if (p.isDead) game.projectiles.splice(i, 1);
});
game.debris.forEach((d, i) => {
d.update(game.terrain);
d.draw(ctx);
if (d.life <= 0) game.debris.splice(i, 1);
});
game.particles.forEach((p, i) => {
p.update();
p.draw(ctx);
if (p.life <= 0) game.particles.splice(i, 1);
});
game.texts.forEach((t, i) => {
t.update();
t.draw(ctx);
if (t.life <= 0) game.texts.splice(i, 1);
});
if (game.state === 'play' && !game.teams[game.activeTeam].isCPU) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
let tx = targetWorm.pos.x;
let ty = targetWorm.pos.y;
let tvx = Math.cos(fireAngle) * firePower;
let tvy = Math.sin(fireAngle) * firePower;
ctx.moveTo(tx, ty);
for (let i = 0; i < 30; i++) {
tvx += game.wind;
tvy += CONFIG.GRAVITY;
tx += tvx;
ty += tvy;
ctx.lineTo(tx, ty);
}
ctx.stroke();
}
ctx.restore();
const aliveTeams = new Set();
game.worms.forEach(w => { if (!w.isDead) aliveTeams.add(w.teamId); });
if (aliveTeams.size <= 1) {
game.state = 'gameover';
document.getElementById('game-over').style.display = 'flex';
const winner = [...aliveTeams][0];
document.getElementById('winner-text').innerText = winner !== undefined ? `Team ${winner + 1} Wins!` : 'Draw!';
}
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"gemma-4-31b","provider":"cerebras","persona":"composer","sessionId":"37d05dbd-72c9-42de-a261-3056dac2e2d8","tokensIn":58271,"tokensOut":14970,"tokensTotal":73241,"turns":4,"toolCalls":16,"failedToolCalls":1,"timestamp":"2026-07-14T16:32:59.259Z"} -->