← Worms results

Worms

Gemma 4 31b · composer

Initial view of Worms

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"} -->