Worms
Gemini 3.6 Flash · basic

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 76,058 bytes · SHA-256 8ad451ef509e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worm Chaos - Artillery Mayhem</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body, html {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #0b0e14;
font-family: 'Fredoka', cursive, sans-serif;
color: #fff;
}
#game-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #111827;
}
canvas {
display: block;
width: 100%;
height: 100%;
background: #000;
}
/* UI OVERLAYS */
.ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
z-index: 10;
}
/* TOP HUD */
.top-bar {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.teams-summary {
display: flex;
gap: 15px;
background: rgba(15, 23, 42, 0.75);
padding: 12px 20px;
border-radius: 16px;
backdrop-filter: blur(8px);
border: 2px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
}
.team-card {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 110px;
}
.team-card .team-name {
font-size: 14px;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
display: flex;
align-items: center;
gap: 6px;
}
.team-card .hp-bar-bg {
width: 100%;
height: 12px;
background: rgba(0, 0, 0, 0.6);
border-radius: 6px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.15);
}
.team-card .hp-bar-fill {
height: 100%;
width: 100%;
transition: width 0.3s ease-out, background-color 0.3s;
border-radius: 6px;
}
/* TIMER & WIND GAUGE */
.center-hud {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.timer-box {
background: radial-gradient(circle, #1e293b 0%, #0f172a 100%);
border: 3px solid #38bdf8;
border-radius: 50%;
width: 76px;
height: 76px;
display: flex;
justify-content: center;
align-items: center;
font-size: 32px;
font-weight: 700;
color: #fff;
box-shadow: 0 0 20px rgba(56, 189, 248, 0.4), inset 0 0 10px rgba(0,0,0,0.8);
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.timer-box.urgent {
border-color: #ef4444;
color: #ef4444;
box-shadow: 0 0 25px rgba(239, 68, 68, 0.7);
animation: pulse-urgent 0.5s infinite alternate;
}
@keyframes pulse-urgent {
0% { transform: scale(1); }
100% { transform: scale(1.15); }
}
.wind-gauge {
background: rgba(15, 23, 42, 0.85);
border: 2px solid rgba(255,255,255,0.15);
border-radius: 20px;
padding: 6px 16px;
display: flex;
align-items: center;
gap: 10px;
backdrop-filter: blur(8px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.wind-arrow {
font-size: 18px;
font-weight: bold;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.wind-text {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
color: #94a3b8;
}
/* BOTTOM HUD */
.bottom-bar {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
}
.weapon-btn {
pointer-events: auto;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border: 3px solid #60a5fa;
border-radius: 16px;
padding: 12px 24px;
color: white;
font-size: 18px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 8px 20px rgba(0,0,0,0.4), 0 0 15px rgba(59, 130, 246, 0.4);
display: flex;
align-items: center;
gap: 10px;
transition: all 0.15s ease;
}
.weapon-btn:hover {
transform: translateY(-3px) scale(1.03);
box-shadow: 0 12px 25px rgba(0,0,0,0.5), 0 0 20px rgba(59, 130, 246, 0.6);
background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%);
}
.weapon-btn:active {
transform: translateY(1px) scale(0.98);
}
.status-msg {
font-size: 22px;
font-weight: 700;
text-shadow: 0 3px 6px rgba(0,0,0,0.9), 0 0 10px rgba(0,0,0,0.5);
background: rgba(15, 23, 42, 0.7);
padding: 8px 20px;
border-radius: 30px;
border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(6px);
color: #f1f5f9;
}
/* WEAPON SELECTION MODAL */
.weapon-menu {
position: absolute;
bottom: 90px;
left: 20px;
background: rgba(15, 23, 42, 0.95);
border: 2px solid #38bdf8;
border-radius: 20px;
padding: 16px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
backdrop-filter: blur(12px);
box-shadow: 0 15px 35px rgba(0,0,0,0.6);
pointer-events: auto;
transform: translateY(20px) scale(0.95);
opacity: 0;
visibility: hidden;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 20;
}
.weapon-menu.open {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
}
.weapon-item {
background: rgba(30, 41, 59, 0.8);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.15s ease;
width: 85px;
}
.weapon-item:hover {
background: rgba(51, 65, 85, 1);
border-color: #38bdf8;
transform: translateY(-2px);
}
.weapon-item.active {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.2);
}
.weapon-icon {
font-size: 26px;
}
.weapon-name {
font-size: 11px;
font-weight: 600;
text-align: center;
color: #e2e8f0;
}
.weapon-cat {
font-size: 8px;
text-transform: uppercase;
padding: 2px 4px;
border-radius: 4px;
font-weight: 700;
}
.cat-indirect { background: #3b82f6; color: #fff; }
.cat-direct { background: #ef4444; color: #fff; }
.cat-aoe { background: #8b5cf6; color: #fff; }
.cat-utility { background: #10b981; color: #fff; }
/* START MENU OVERLAY */
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(11, 15, 20, 0.85);
backdrop-filter: blur(10px);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
pointer-events: auto;
}
.menu-card {
background: linear-gradient(145deg, #1e293b, #0f172a);
border: 3px solid #38bdf8;
border-radius: 24px;
padding: 35px 45px;
width: 520px;
max-width: 90vw;
box-shadow: 0 20px 50px rgba(0,0,0,0.7), 0 0 30px rgba(56, 189, 248, 0.3);
display: flex;
flex-direction: column;
gap: 20px;
text-align: center;
}
.menu-card h1 {
font-size: 42px;
font-weight: 700;
background: linear-gradient(to right, #38bdf8, #f43f5e, #f59e0b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 4px 10px rgba(0,0,0,0.5);
letter-spacing: 1px;
}
.menu-option {
display: flex;
flex-direction: column;
gap: 8px;
text-align: left;
}
.menu-option label {
font-size: 14px;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-group {
display: flex;
gap: 10px;
}
.opt-btn {
flex: 1;
padding: 10px 14px;
background: rgba(30, 41, 59, 0.8);
border: 2px solid rgba(255,255,255,0.1);
border-radius: 12px;
color: #cbd5e1;
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.opt-btn:hover {
border-color: #38bdf8;
color: #fff;
}
.opt-btn.selected {
background: #38bdf8;
color: #0f172a;
border-color: #38bdf8;
font-weight: 700;
}
.start-btn {
margin-top: 10px;
padding: 16px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: 2px solid #34d399;
border-radius: 16px;
color: white;
font-size: 22px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
transition: all 0.2s ease;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 25px rgba(16, 185, 129, 0.6);
background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
}
/* CONTROLS GUIDE */
.controls-hint {
font-size: 12px;
color: #64748b;
line-height: 1.5;
background: rgba(0,0,0,0.2);
padding: 10px;
border-radius: 8px;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<!-- UI LAYER -->
<div class="ui-layer">
<div class="top-bar">
<div class="teams-summary" id="teamsSummary">
<!-- Dynamic Team Cards -->
</div>
<div class="center-hud">
<div class="timer-box" id="turnTimer">45</div>
<div class="wind-gauge">
<span class="wind-text">WIND</span>
<span class="wind-arrow" id="windArrow">➔</span>
<span id="windValue" style="font-size: 14px; font-weight: 700;">0</span>
</div>
</div>
</div>
<div class="bottom-bar">
<button class="weapon-btn" id="weaponBtn">
<span id="currentWpnIcon">🚀</span>
<span id="currentWpnName">Bazooka</span>
</button>
<div class="status-msg" id="statusMsg">Team Red's Turn</div>
</div>
<!-- WEAPON SELECTOR MENU -->
<div class="weapon-menu" id="weaponMenu">
<!-- Dynamic Weapons -->
</div>
</div>
<!-- START / GAME OVER MENU -->
<div class="modal-overlay" id="menuModal">
<div class="menu-card">
<h1 id="menuTitle">WORM CHAOS</h1>
<div class="menu-option">
<label>Teams & Players</label>
<div class="btn-group" id="teamModeGroup">
<button class="opt-btn selected" data-val="2">2 Teams (1v1)</button>
<button class="opt-btn" data-val="3">3 Teams</button>
<button class="opt-btn" data-val="4">4 Teams</button>
</div>
</div>
<div class="menu-option">
<label>Opponents</label>
<div class="btn-group" id="aiModeGroup">
<button class="opt-btn selected" data-val="pvp">Human Hot-Seat</button>
<button class="opt-btn" data-val="cpu">VS CPU AI</button>
</div>
</div>
<div class="menu-option">
<label>Map Terrain Shape</label>
<div class="btn-group" id="mapShapeGroup">
<button class="opt-btn selected" data-val="single">Single Island</button>
<button class="opt-btn" data-val="double">Two Islands</button>
<button class="opt-btn" data-val="cave">Cavern</button>
</div>
</div>
<div class="controls-hint">
<strong>Controls:</strong> [A/D] Walk | [W] Aim Up / Jump | [S] Aim Down | [Space] Charge & Fire | [BackSpace] Backflip | [1-9 / Q] Quick Weapons
</div>
<button class="start-btn" id="startGameBtn">LET'S BATTLE!</button>
</div>
</div>
</div>
<script>
/* --- AUDIO SYNTHESIZER ENGINE --- */
class SoundEngine {
constructor() {
this.ctx = null;
this.muted = false;
}
init() {
if (!this.ctx) {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioCtx();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
playJump() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(150, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(400, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.15);
}
playExplosion(size = 1) {
if (!this.ctx) return;
const dur = 0.4 * size;
const bufferSize = this.ctx.sampleRate * dur;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800 * size, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + dur);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.8, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
}
playShoot() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(600, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, this.ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.2);
}
playTimerTick(isUrgent) {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
const freq = isUrgent ? 880 : 440;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.08);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.08);
}
playSplash() {
if (!this.ctx) return;
const dur = 0.3;
const bufferSize = this.ctx.sampleRate * dur;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(1200, this.ctx.currentTime);
filter.Q.setValueAtTime(3, this.ctx.currentTime);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
}
playSqueak(pitch = 1) {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(600 * pitch, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(1200 * pitch, this.ctx.currentTime + 0.08);
osc.frequency.linearRampToValueAtTime(800 * pitch, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.15);
}
playChoir() {
if (!this.ctx) return;
const freqs = [261.63, 329.63, 392.00, 523.25]; // C major chord
freqs.forEach(f => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(f, this.ctx.currentTime);
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 1.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 1.2);
});
}
playClick() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, this.ctx.currentTime);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.04);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.04);
}
}
const SFX = new SoundEngine();
/* --- WEAPONS DEFINITIONS --- */
const WEAPONS = [
{ id: 'bazooka', name: 'Bazooka', icon: '🚀', category: 'indirect', type: 'projectile', gravity: 0.25, windFactor: 1.0, bounce: 0, radius: 32, damage: 45, fuse: 0, desc: 'Classic wind-affected missile' },
{ id: 'grenade', name: 'Grenade', icon: '💣', category: 'indirect', type: 'projectile', gravity: 0.28, windFactor: 0.2, bounce: 0.6, radius: 36, damage: 50, fuse: 3, desc: 'Bouncy 3-second fuse explosive' },
{ id: 'holyhand', name: 'Holy Grenade', icon: '✝️', category: 'aoe', type: 'projectile', gravity: 0.22, windFactor: 0.1, bounce: 0.3, radius: 75, damage: 90, fuse: 3.5, choir: true, desc: 'Devastating high-AoE divine bomb' },
{ id: 'cluster', name: 'Cluster Bomb', icon: '💥', category: 'indirect', type: 'projectile', gravity: 0.28, windFactor: 0.3, bounce: 0.4, radius: 28, damage: 25, fuse: 2.5, clusters: 5, desc: 'Splits into multiple bomblets' },
{ id: 'shotgun', name: 'Shotgun', icon: '🔫', category: 'direct', type: 'instant', radius: 18, damage: 28, shots: 2, knockback: 8, desc: 'Direct hit, 2 shots per turn' },
{ id: 'sniper', name: 'Sniper Rifle', icon: '🎯', category: 'direct', type: 'instant', radius: 12, damage: 60, shots: 1, knockback: 14, desc: 'High accuracy direct laser beam' },
{ id: 'airstrike', name: 'Air Strike', icon: '✈️', category: 'aoe', type: 'target', radius: 30, damage: 35, bombs: 5, desc: 'Call in missiles from above' },
{ id: 'dynamite', name: 'Dynamite', icon: '🧨', category: 'aoe', type: 'drop', radius: 65, damage: 75, fuse: 4, desc: 'Heavy drop explosive with HUGE crater' },
{ id: 'jetpack', name: 'Jetpack', icon: '🎒', category: 'utility', type: 'utility', fuel: 100, desc: 'Fly freely through the air' },
{ id: 'teleport', name: 'Teleport', icon: '🌀', category: 'utility', type: 'utility', desc: 'Instantly move anywhere on map' },
{ id: 'girder', name: 'Girder Bridge', icon: '0️⃣', category: 'utility', type: 'utility', desc: 'Place a custom bridge platform' }
];
const TEAMS_CONFIG = [
{ id: 0, name: 'Red Rogues', color: '#ef4444', darkColor: '#991b1b' },
{ id: 1, name: 'Blue Bazookas', color: '#3b82f6', darkColor: '#1e40af' },
{ id: 2, name: 'Green Grenadiers', color: '#10b981', darkColor: '#065f46' },
{ id: 3, name: 'Yellow Yahoos', color: '#f59e0b', darkColor: '#92400e' }
];
/* --- GAME ENGINE & CONSTANTS --- */
const WORLD_WIDTH = 2200;
const WORLD_HEIGHT = 1200;
const WATER_LEVEL = 1060;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
// Offscreen canvas for destructible terrain
this.terrainCanvas = document.createElement('canvas');
this.terrainCanvas.width = WORLD_WIDTH;
this.terrainCanvas.height = WORLD_HEIGHT;
this.terrainCtx = this.terrainCanvas.getContext('2d');
// Game setup parameters
this.numTeams = 2;
this.isVsCPU = false;
this.mapShape = 'single'; // single, double, cave
// State
this.teams = [];
this.worms = [];
this.activeWormIdx = 0;
this.activeTeamIdx = 0;
this.projectiles = [];
this.particles = [];
this.debris = [];
this.floatingTexts = [];
this.turnTimer = 45;
this.timerInterval = null;
this.wind = 0; // -100 to 100
this.gameState = 'MENU'; // MENU, AIMING, FIRING, RETREAT, GAME_OVER
this.retreatTimer = 0;
// Camera
this.camera = { x: WORLD_WIDTH / 2, y: WORLD_HEIGHT / 2, zoom: 1, targetX: WORLD_WIDTH / 2, targetY: WORLD_HEIGHT / 2, targetZoom: 1 };
this.shakeAmount = 0;
this.freezeFrame = 0;
// Input
this.keys = {};
this.aimAngle = -Math.PI / 4;
this.aimPower = 0;
this.isCharging = false;
this.selectedWeapon = WEAPONS[0];
this.mouseWorld = { x: 0, y: 0 };
this.bindEvents();
this.resizeCanvas();
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
bindEvents() {
window.addEventListener('resize', () => this.resizeCanvas());
window.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
if (this.gameState === 'AIMING' && this.getCurrentWorm() && !this.getCurrentWorm().isCPU) {
// Quick weapon selection keys 1-9
if (e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key) - 1;
if (idx < WEAPONS.length) this.selectWeapon(WEAPONS[idx]);
}
if (e.code === 'KeyQ') {
this.toggleWeaponMenu();
}
if (e.code === 'Backspace') {
// Backflip jump
this.getCurrentWorm().jump(true);
}
}
});
window.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
if (e.code === 'Space' && this.isCharging && this.gameState === 'AIMING') {
this.fireWeapon();
}
});
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
const screenX = e.clientX - rect.left;
const screenY = e.clientY - rect.top;
// Convert screen to world coordinates
this.mouseWorld.x = (screenX - this.canvas.width / 2) / this.camera.zoom + this.camera.x;
this.mouseWorld.y = (screenY - this.canvas.height / 2) / this.camera.zoom + this.camera.y;
});
this.canvas.addEventListener('click', (e) => {
if (this.gameState === 'AIMING' && this.selectedWeapon.type === 'target') {
this.useTargetWeapon(this.mouseWorld.x, this.mouseWorld.y);
} else if (this.gameState === 'AIMING' && this.selectedWeapon.id === 'teleport') {
this.teleportActiveWorm(this.mouseWorld.x, this.mouseWorld.y);
} else if (this.gameState === 'AIMING' && this.selectedWeapon.id === 'girder') {
this.placeGirder(this.mouseWorld.x, this.mouseWorld.y);
}
});
// UI Event Handlers
document.getElementById('weaponBtn').onclick = () => this.toggleWeaponMenu();
// Menu selection buttons
this.setupMenuButtons('teamModeGroup', (val) => this.numTeams = parseInt(val));
this.setupMenuButtons('aiModeGroup', (val) => this.isVsCPU = (val === 'cpu'));
this.setupMenuButtons('mapShapeGroup', (val) => this.mapShape = val);
document.getElementById('startGameBtn').onclick = () => {
SFX.init();
SFX.playClick();
document.getElementById('menuModal').style.display = 'none';
this.startNewGame();
};
}
setupMenuButtons(groupId, callback) {
const group = document.getElementById(groupId);
const btns = group.querySelectorAll('.opt-btn');
btns.forEach(btn => {
btn.onclick = () => {
SFX.init();
SFX.playClick();
btns.forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
callback(btn.dataset.val);
};
});
}
toggleWeaponMenu() {
SFX.playClick();
const menu = document.getElementById('weaponMenu');
menu.classList.toggle('open');
}
selectWeapon(wpn) {
this.selectedWeapon = wpn;
document.getElementById('currentWpnIcon').textContent = wpn.icon;
document.getElementById('currentWpnName').textContent = wpn.name;
document.getElementById('weaponMenu').classList.remove('open');
this.renderWeaponMenu();
SFX.playClick();
}
renderWeaponMenu() {
const menu = document.getElementById('weaponMenu');
menu.innerHTML = '';
WEAPONS.forEach(w => {
const item = document.createElement('div');
item.className = `weapon-item ${w.id === this.selectedWeapon.id ? 'active' : ''}`;
item.innerHTML = `
<div class="weapon-icon">${w.icon}</div>
<div class="weapon-name">${w.name}</div>
<div class="weapon-cat cat-${w.category}">${w.category}</div>
`;
item.onclick = () => this.selectWeapon(w);
menu.appendChild(item);
});
}
/* --- MAP GENERATION --- */
generateTerrain() {
const ctx = this.terrainCtx;
ctx.clearRect(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
// Base terrain path
ctx.beginPath();
ctx.fillStyle = '#1e293b'; // Base rock background fill
if (this.mapShape === 'single') {
ctx.moveTo(100, WORLD_HEIGHT);
for (let x = 100; x <= WORLD_WIDTH - 100; x += 20) {
const mid = WORLD_WIDTH / 2;
const distFromCenter = Math.abs(x - mid) / mid;
const heightFactor = Math.cos(distFromCenter * Math.PI * 0.8);
const n = Math.sin(x * 0.005) * 80 + Math.sin(x * 0.02) * 30 + Math.cos(x * 0.05) * 15;
const y = 600 - heightFactor * 350 + n;
ctx.lineTo(x, y);
}
ctx.lineTo(WORLD_WIDTH - 100, WORLD_HEIGHT);
} else if (this.mapShape === 'double') {
ctx.moveTo(100, WORLD_HEIGHT);
for (let x = 100; x <= WORLD_WIDTH - 100; x += 20) {
const island1 = Math.exp(-Math.pow((x - 600) / 300, 2)) * 380;
const island2 = Math.exp(-Math.pow((x - 1600) / 300, 2)) * 380;
const n = Math.sin(x * 0.015) * 30 + Math.cos(x * 0.04) * 15;
const y = 750 - (island1 + island2) + n;
ctx.lineTo(x, y);
}
ctx.lineTo(WORLD_WIDTH - 100, WORLD_HEIGHT);
} else if (this.mapShape === 'cave') {
// Floor
ctx.moveTo(0, WORLD_HEIGHT);
for (let x = 0; x <= WORLD_WIDTH; x += 20) {
const n = Math.sin(x * 0.008) * 120 + Math.cos(x * 0.02) * 40;
ctx.lineTo(x, 700 + n);
}
ctx.lineTo(WORLD_WIDTH, WORLD_HEIGHT);
ctx.closePath();
ctx.fill();
// Ceiling
ctx.beginPath();
ctx.moveTo(0, 0);
for (let x = 0; x <= WORLD_WIDTH; x += 20) {
const n = Math.cos(x * 0.008) * 100 + Math.sin(x * 0.03) * 35;
ctx.lineTo(x, 350 + n);
}
ctx.lineTo(WORLD_WIDTH, 0);
}
ctx.closePath();
// Beautiful gradient for terrain body
const grad = ctx.createLinearGradient(0, 200, 0, WORLD_HEIGHT);
grad.addColorStop(0, '#475569');
grad.addColorStop(0.4, '#334155');
grad.addColorStop(1, '#0f172a');
ctx.fillStyle = grad;
ctx.fill();
// Top Grass/Surface Layer
this.addSurfaceGrass();
}
addSurfaceGrass() {
const ctx = this.terrainCtx;
const imgData = ctx.getImageData(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
const pixels = imgData.data;
// Create a secondary canvas layer for grass texturing
const tempCanvas = document.createElement('canvas');
tempCanvas.width = WORLD_WIDTH;
tempCanvas.height = WORLD_HEIGHT;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.strokeStyle = '#22c55e'; // Bright green grass line
tempCtx.lineWidth = 14;
tempCtx.lineCap = 'round';
ctx.save();
ctx.globalCompositeOperation = 'source-over';
// Find top boundary pixels to draw grass
for (let x = 0; x < WORLD_WIDTH; x += 6) {
for (let y = 10; y < WORLD_HEIGHT - 50; y += 4) {
const idx = (y * WORLD_WIDTH + x) * 4;
if (pixels[idx + 3] > 200) { // Solid pixel
ctx.fillStyle = '#22c55e';
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
// Dirt accent under grass
ctx.fillStyle = '#854d0e';
ctx.beginPath();
ctx.arc(x, y + 6, 5, 0, Math.PI * 2);
ctx.fill();
break;
}
}
}
ctx.restore();
}
/* --- GAME LIFECYCLE --- */
startNewGame() {
this.generateTerrain();
this.renderWeaponMenu();
// Setup Teams
this.teams = [];
this.worms = [];
for (let i = 0; i < this.numTeams; i++) {
const cfg = TEAMS_CONFIG[i];
const isCPU = (this.isVsCPU && i > 0);
const team = { id: i, name: cfg.name, color: cfg.color, darkColor: cfg.darkColor, isCPU: isCPU, worms: [] };
for (let w = 0; w < 4; w++) {
const name = `${cfg.name.split(' ')[0]} ${w + 1}`;
const worm = new Worm(this, w, team, name);
this.spawnWormSafely(worm);
team.worms.push(worm);
this.worms.push(worm);
}
this.teams.push(team);
}
this.activeTeamIdx = 0;
this.activeWormIdx = 0;
this.projectiles = [];
this.particles = [];
this.debris = [];
this.floatingTexts = [];
this.updateTeamsHUD();
this.startTurn();
this.gameState = 'AIMING';
requestAnimationFrame((t) => this.loop(t));
}
spawnWormSafely(worm) {
let attempts = 0;
while (attempts < 500) {
const x = 150 + Math.random() * (WORLD_WIDTH - 300);
for (let y = 50; y < WATER_LEVEL - 50; y += 10) {
if (this.isSolidPixel(x, y)) {
worm.x = x;
worm.y = y - 20;
worm.vx = 0;
worm.vy = 0;
return;
}
}
attempts++;
}
// Fallback position
worm.x = 400 + Math.random() * 800;
worm.y = 400;
}
startTurn() {
// Find next alive worm
let attempts = 0;
const totalWorms = this.worms.length;
do {
this.activeWormIdx = (this.activeWormIdx + 1) % totalWorms;
attempts++;
} while (this.worms[this.activeWormIdx].isDead && attempts < totalWorms * 2);
const worm = this.getCurrentWorm();
if (!worm || worm.isDead) {
this.checkWinCondition();
return;
}
this.activeTeamIdx = worm.team.id;
this.turnTimer = 45;
this.wind = Math.floor((Math.random() * 200) - 100);
this.updateWindUI();
this.gameState = 'AIMING';
this.isCharging = false;
this.aimPower = 0;
// Reset Jetpack fuel
worm.jetpackFuel = 100;
worm.isJetpacking = false;
// Focus Camera
this.camera.targetX = worm.x;
this.camera.targetY = worm.y;
this.camera.targetZoom = 1.2;
const msg = document.getElementById('statusMsg');
msg.textContent = `${worm.name}'s Turn!`;
msg.style.color = worm.team.color;
clearInterval(this.timerInterval);
this.timerInterval = setInterval(() => {
if (this.gameState === 'AIMING') {
this.turnTimer--;
const timerEl = document.getElementById('turnTimer');
timerEl.textContent = this.turnTimer;
if (this.turnTimer <= 10) {
timerEl.classList.add('urgent');
SFX.playTimerTick(true);
} else {
timerEl.classList.remove('urgent');
SFX.playTimerTick(false);
}
if (this.turnTimer <= 0) {
this.endTurn(500);
}
}
}, 1000);
if (worm.isCPU) {
this.handleCPUTurn(worm);
}
}
endTurn(delay = 2000) {
clearInterval(this.timerInterval);
this.gameState = 'RETREAT';
this.retreatTimer = delay;
setTimeout(() => {
this.checkWinCondition();
if (this.gameState !== 'GAME_OVER') {
this.startTurn();
}
}, delay);
}
checkWinCondition() {
const aliveTeams = this.teams.filter(t => t.worms.some(w => !w.isDead));
if (aliveTeams.length <= 1) {
this.gameState = 'GAME_OVER';
const winner = aliveTeams.length === 1 ? aliveTeams[0].name : 'Nobody';
const modal = document.getElementById('menuModal');
const title = document.getElementById('menuTitle');
title.textContent = aliveTeams.length === 1 ? `🎉 ${winner} WINS!` : `DRAW! GAME OVER`;
document.getElementById('startGameBtn').textContent = 'PLAY AGAIN';
modal.style.display = 'flex';
}
}
getCurrentWorm() {
return this.worms[this.activeWormIdx];
}
updateWindUI() {
const arrow = document.getElementById('windArrow');
const val = document.getElementById('windValue');
val.textContent = Math.abs(this.wind);
arrow.style.transform = `rotate(${this.wind >= 0 ? 0 : 180}deg)`;
}
updateTeamsHUD() {
const summary = document.getElementById('teamsSummary');
summary.innerHTML = '';
this.teams.forEach(team => {
const totalHp = team.worms.reduce((acc, w) => acc + (w.isDead ? 0 : w.hp), 0);
const maxHp = team.worms.length * 100;
const pct = Math.max(0, (totalHp / maxHp) * 100);
const card = document.createElement('div');
card.className = 'team-card';
card.innerHTML = `
<div class="team-name" style="color:${team.color}">
<span>${team.name}</span>
</div>
<div class="hp-bar-bg">
<div class="hp-bar-fill" style="width:${pct}%; background-color:${team.color}"></div>
</div>
`;
summary.appendChild(card);
});
}
/* --- COLLISION & TERRAIN LOGIC --- */
isSolidPixel(x, y) {
if (x < 0 || x >= WORLD_WIDTH || y < 0 || y >= WORLD_HEIGHT) return false;
const pixel = this.terrainCtx.getImageData(Math.floor(x), Math.floor(y), 1, 1).data;
return pixel[3] > 128; // Alpha threshold
}
carveExplosion(cx, cy, radius) {
const ctx = this.terrainCtx;
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Spawn Falling Debris Chunks
const numDebris = Math.floor(radius * 0.4);
for (let i = 0; i < numDebris; i++) {
const ang = Math.random() * Math.PI * 2;
const dist = Math.random() * radius * 0.8;
const px = cx + Math.cos(ang) * dist;
const py = cy + Math.sin(ang) * dist;
this.debris.push(new DebrisChunk(this, px, py));
}
// Explosion Smoke & Spark Particles
for (let i = 0; i < radius * 1.5; i++) {
this.particles.push(new Particle(cx, cy, 'explosion'));
}
// Damage Worms & Apply Blast Wave Knockback
this.worms.forEach(w => {
if (w.isDead) return;
const dx = w.x - cx;
const dy = w.y - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const damageRadius = radius * 1.4;
if (dist < damageRadius) {
const dmg = Math.floor((1 - dist / damageRadius) * this.selectedWeapon.damage);
if (dmg > 0) {
w.takeDamage(dmg);
// Blast impulse
const force = (1 - dist / damageRadius) * 16;
const angle = Math.atan2(dy, dx);
w.vx += Math.cos(angle) * force;
w.vy += Math.sin(angle) * force - 3; // UP shockwave
}
}
});
// Screen shake & Impact Freeze Frame
this.shakeAmount = Math.min(35, radius * 0.6);
if (radius > 50) this.freezeFrame = 4; // Pause frame logic briefly for visceral juice
SFX.playExplosion(radius / 40);
}
/* --- WEAPON ACTION & FIRING --- */
fireWeapon() {
if (this.gameState !== 'AIMING') return;
const worm = this.getCurrentWorm();
if (!worm) return;
const wpn = this.selectedWeapon;
const speed = this.aimPower * 0.24;
const vx = Math.cos(this.aimAngle) * speed + (worm.facingRight ? 2 : -2);
const vy = Math.sin(this.aimAngle) * speed;
if (wpn.type === 'projectile') {
this.projectiles.push(new Projectile(this, worm.x, worm.y - 10, vx, vy, wpn));
SFX.playShoot();
} else if (wpn.type === 'instant') {
this.fireInstantWeapon(worm, wpn);
} else if (wpn.type === 'drop') {
this.projectiles.push(new Projectile(this, worm.x, worm.y, 0, 1, wpn));
}
this.gameState = 'FIRING';
this.isCharging = false;
this.aimPower = 0;
// Follow projectile or worm
if (this.projectiles.length > 0) {
const p = this.projectiles[this.projectiles.length - 1];
this.camera.targetX = p.x;
this.camera.targetY = p.y;
}
}
fireInstantWeapon(worm, wpn) {
const dirX = Math.cos(this.aimAngle);
const dirY = Math.sin(this.aimAngle);
let rayX = worm.x;
let rayY = worm.y - 10;
SFX.playShoot();
// Raycast instant hit
for (let step = 0; step < 1200; step += 8) {
rayX += dirX * 8;
rayY += dirY * 8;
// Particle beam tracer
this.particles.push(new Particle(rayX, rayY, 'tracer'));
// Check hit worm
for (let target of this.worms) {
if (target !== worm && !target.isDead && Math.hypot(target.x - rayX, target.y - rayY) < 20) {
target.takeDamage(wpn.damage);
target.vx += dirX * wpn.knockback;
target.vy += dirY * wpn.knockback - 2;
this.carveExplosion(rayX, rayY, wpn.radius);
this.endTurn(1500);
return;
}
}
// Check hit terrain
if (this.isSolidPixel(rayX, rayY)) {
this.carveExplosion(rayX, rayY, wpn.radius);
this.endTurn(1500);
return;
}
}
this.endTurn(1000);
}
useTargetWeapon(tx, ty) {
const wpn = this.selectedWeapon;
if (wpn.id === 'airstrike') {
this.gameState = 'FIRING';
SFX.playSqueak(1.2);
// Drop multiple bombs in sequence
for (let i = 0; i < wpn.bombs; i++) {
setTimeout(() => {
const dropX = tx + (i - 2) * 35;
const dropY = 50;
const p = new Projectile(this, dropX, dropY, 0, 4, WEAPONS[0]);
this.projectiles.push(p);
}, i * 250);
}
setTimeout(() => this.endTurn(2500), wpn.bombs * 250 + 1000);
}
}
teleportActiveWorm(tx, ty) {
const worm = this.getCurrentWorm();
if (!worm || this.isSolidPixel(tx, ty)) return;
// Teleport effect
for (let i = 0; i < 25; i++) this.particles.push(new Particle(worm.x, worm.y, 'teleport'));
worm.x = tx;
worm.y = ty;
worm.vx = 0;
worm.vy = 0;
for (let i = 0; i < 25; i++) this.particles.push(new Particle(worm.x, worm.y, 'teleport'));
SFX.playSqueak(1.4);
this.endTurn(1000);
}
placeGirder(gx, gy) {
const ctx = this.terrainCtx;
ctx.save();
ctx.fillStyle = '#64748b'; // Slate bridge girder
ctx.translate(gx, gy);
ctx.rotate(this.aimAngle);
ctx.fillRect(-60, -8, 120, 16);
ctx.restore();
SFX.playClick();
this.endTurn(1000);
}
/* --- CPU AI CONTROLLER --- */
handleCPUTurn(worm) {
setTimeout(() => {
if (this.gameState !== 'AIMING' || worm.isDead) return;
// AI Risk & Target Evaluation
let bestTarget = null;
let minDistance = 99999;
this.worms.forEach(enemy => {
if (enemy.team.id !== worm.team.id && !enemy.isDead) {
const d = Math.hypot(enemy.x - worm.x, enemy.y - worm.y);
if (d < minDistance) {
minDistance = d;
bestTarget = enemy;
}
}
});
if (!bestTarget) return;
// Choose weapon based on range
if (minDistance < 200) {
this.selectWeapon(WEAPONS.find(w => w.id === 'shotgun') || WEAPONS[0]);
} else if (minDistance > 700 && Math.random() > 0.4) {
this.selectWeapon(WEAPONS.find(w => w.id === 'airstrike') || WEAPONS[0]);
} else {
this.selectWeapon(WEAPONS.find(w => w.id === 'bazooka') || WEAPONS[0]);
}
if (this.selectedWeapon.type === 'target') {
this.useTargetWeapon(bestTarget.x, bestTarget.y);
return;
}
// Ballistic Trajectory Calculation Simulation
const dx = bestTarget.x - worm.x;
const dy = bestTarget.y - worm.y;
let computedAngle = Math.atan2(dy, dx);
if (dx < 0) worm.facingRight = false;
else worm.facingRight = true;
// High arc trajectory tweak
computedAngle -= 0.35;
this.aimAngle = computedAngle;
this.aimPower = Math.min(100, Math.max(30, minDistance * 0.12));
// Aiming delay simulation
setTimeout(() => {
if (this.gameState === 'AIMING') {
this.fireWeapon();
}
}, 1200);
}, 1000);
}
/* --- UPDATE & RENDER LOOP --- */
loop(timestamp) {
if (this.freezeFrame > 0) {
this.freezeFrame--;
requestAnimationFrame((t) => this.loop(t));
return;
}
this.update();
this.render();
requestAnimationFrame((t) => this.loop(t));
}
update() {
const activeWorm = this.getCurrentWorm();
// Player Key Inputs
if (this.gameState === 'AIMING' && activeWorm && !activeWorm.isCPU) {
// Aim controls
if (this.keys['KeyW']) this.aimAngle -= 0.03;
if (this.keys['KeyS']) this.aimAngle += 0.03;
// Walk controls
if (this.keys['KeyA']) {
activeWorm.walk(-1);
} else if (this.keys['KeyD']) {
activeWorm.walk(1);
}
// Jump controls
if (this.keys['KeyW'] && activeWorm.isGrounded) {
activeWorm.jump(false);
}
// Jetpack control
if (this.selectedWeapon.id === 'jetpack' && this.keys['Space']) {
activeWorm.flyWithJetpack();
} else if (this.keys['Space'] && this.selectedWeapon.type === 'projectile') {
// Power Charge
this.isCharging = true;
this.aimPower = Math.min(100, this.aimPower + 1.8);
}
}
// Update Worms
this.worms.forEach(w => w.update());
// Update Projectiles
for (let i = this.projectiles.length - 1; i >= 0; i--) {
const p = this.projectiles[i];
p.update();
if (p.destroyed) this.projectiles.splice(i, 1);
}
// Update Particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const pt = this.particles[i];
pt.update();
if (pt.life <= 0) this.particles.splice(i, 1);
}
// Update Debris Chunks
for (let i = this.debris.length - 1; i >= 0; i--) {
const d = this.debris[i];
d.update();
if (d.destroyed) this.debris.splice(i, 1);
}
// Update Floating Text
for (let i = this.floatingTexts.length - 1; i >= 0; i--) {
const ft = this.floatingTexts[i];
ft.update();
if (ft.life <= 0) this.floatingTexts.splice(i, 1);
}
// Smooth Dynamic Camera
if (this.shakeAmount > 0) this.shakeAmount *= 0.88;
this.camera.x += (this.camera.targetX - this.camera.x) * 0.08;
this.camera.y += (this.camera.targetY - this.camera.y) * 0.08;
this.camera.zoom += (this.camera.targetZoom - this.camera.zoom) * 0.05;
}
render() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.save();
// Apply Camera Transformation & Screen Shake Juice
const shakeX = (Math.random() - 0.5) * this.shakeAmount;
const shakeY = (Math.random() - 0.5) * this.shakeAmount;
ctx.translate(this.canvas.width / 2 + shakeX, this.canvas.height / 2 + shakeY);
ctx.scale(this.camera.zoom, this.camera.zoom);
ctx.translate(-this.camera.x, -this.camera.y);
// Render Parallax Dynamic Sky Gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, WORLD_HEIGHT);
skyGrad.addColorStop(0, '#020617');
skyGrad.addColorStop(0.5, '#0f172a');
skyGrad.addColorStop(1, '#1e1b4b');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
// Draw Destructible Terrain
ctx.drawImage(this.terrainCanvas, 0, 0);
// Draw Debris
this.debris.forEach(d => d.draw(ctx));
// Draw Worms
this.worms.forEach(w => w.draw(ctx));
// Draw Projectiles
this.projectiles.forEach(p => p.draw(ctx));
// Draw Particles
this.particles.forEach(pt => pt.draw(ctx));
// Draw Predictive Aiming Line & Power Bar
this.renderAimingTrajectory(ctx);
// Draw Water Layer
this.renderWater(ctx);
// Draw Floating Damage Texts
this.floatingTexts.forEach(ft => ft.draw(ctx));
ctx.restore();
}
renderAimingTrajectory(ctx) {
const worm = this.getCurrentWorm();
if (!worm || this.gameState !== 'AIMING' || worm.isCPU) return;
const wpn = this.selectedWeapon;
if (wpn.type !== 'projectile') return;
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 2;
ctx.setLineDash([4, 6]);
ctx.beginPath();
let simX = worm.x;
let simY = worm.y - 10;
const simPower = (this.isCharging ? this.aimPower : 50) * 0.24;
let simVx = Math.cos(this.aimAngle) * simPower + (worm.facingRight ? 2 : -2);
let simVy = Math.sin(this.aimAngle) * simPower;
ctx.moveTo(simX, simY);
for (let t = 0; t < 35; t++) {
simVx += (this.wind * 0.002) * (wpn.windFactor || 1);
simVy += wpn.gravity || 0.25;
simX += simVx;
simY += simVy;
ctx.lineTo(simX, simY);
if (this.isSolidPixel(simX, simY)) break;
}
ctx.stroke();
ctx.restore();
// Draw Power Meter Bar when Charging
if (this.isCharging) {
ctx.save();
const barW = 60;
const barH = 8;
const bx = worm.x - barW / 2;
const by = worm.y - 45;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(bx, by, barW, barH);
const fillW = (this.aimPower / 100) * barW;
const grad = ctx.createLinearGradient(bx, 0, bx + barW, 0);
grad.addColorStop(0, '#f59e0b');
grad.addColorStop(1, '#ef4444');
ctx.fillStyle = grad;
ctx.fillRect(bx, by, fillW, barH);
ctx.strokeStyle = '#fff';
ctx.strokeRect(bx, by, barW, barH);
ctx.restore();
}
}
renderWater(ctx) {
ctx.save();
const time = Date.now() * 0.003;
ctx.fillStyle = 'rgba(14, 165, 233, 0.65)';
ctx.beginPath();
ctx.moveTo(0, WORLD_HEIGHT);
ctx.lineTo(0, WATER_LEVEL);
for (let x = 0; x <= WORLD_WIDTH; x += 30) {
const y = WATER_LEVEL + Math.sin(x * 0.02 + time) * 8 + Math.cos(x * 0.01 - time) * 4;
ctx.lineTo(x, y);
}
ctx.lineTo(WORLD_WIDTH, WORLD_HEIGHT);
ctx.closePath();
ctx.fill();
// Bright crest highlight
ctx.strokeStyle = 'rgba(186, 230, 253, 0.8)';
ctx.lineWidth = 3;
ctx.stroke();
ctx.restore();
}
}
/* --- WORM ENTITY CLASS --- */
class Worm {
constructor(game, id, team, name) {
this.game = game;
this.id = id;
this.team = team;
this.name = name;
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
this.hp = 100;
this.isDead = false;
this.facingRight = true;
this.isGrounded = false;
this.jetpackFuel = 100;
this.radius = 12;
}
walk(dir) {
this.facingRight = (dir > 0);
const stepX = this.x + dir * 2;
// Climb slopes or walk
if (!this.game.isSolidPixel(stepX, this.y)) {
this.x = stepX;
} else if (!this.game.isSolidPixel(stepX, this.y - 6)) {
this.x = stepX;
this.y -= 4;
}
}
jump(isBackflip = false) {
if (!this.isGrounded) return;
SFX.playJump();
this.isGrounded = false;
if (isBackflip) {
this.vy = -8.5;
this.vx = (this.facingRight ? -3.5 : 3.5);
} else {
this.vy = -6.5;
this.vx = (this.facingRight ? 3.5 : -3.5);
}
}
flyWithJetpack() {
if (this.jetpackFuel <= 0) return;
this.vy -= 0.6;
this.jetpackFuel -= 0.8;
this.game.particles.push(new Particle(this.x, this.y + 10, 'jetpack'));
}
takeDamage(amount) {
if (this.isDead) return;
this.hp = Math.max(0, this.hp - amount);
this.game.floatingTexts.push(new FloatingText(this.x, this.y - 20, `-${amount}`, '#ef4444'));
SFX.playSqueak(0.8);
this.game.updateTeamsHUD();
if (this.hp <= 0) {
this.die();
}
}
die() {
this.isDead = true;
this.hp = 0;
this.game.floatingTexts.push(new FloatingText(this.x, this.y - 30, 'RIP', '#94a3b8'));
}
update() {
if (this.isDead) return;
// Physics & Gravity
this.vy += 0.28; // Gravity
this.vx *= 0.92; // Friction
let nextX = this.x + this.vx;
let nextY = this.y + this.vy;
// Terrain Collision Check
if (this.game.isSolidPixel(nextX, nextY + this.radius)) {
// Fall Damage Check
if (this.vy > 11) {
const fallDmg = Math.floor((this.vy - 11) * 6);
this.takeDamage(fallDmg);
}
this.vy = 0;
this.isGrounded = true;
// Align to surface
while (this.game.isSolidPixel(this.x, this.y + this.radius - 1)) {
this.y--;
}
} else {
this.isGrounded = false;
this.y = nextY;
}
if (!this.game.isSolidPixel(nextX + (this.vx > 0 ? this.radius : -this.radius), this.y)) {
this.x = nextX;
} else {
this.vx = 0;
}
// Drowning Water Physics Check
if (this.y > WATER_LEVEL) {
this.game.particles.push(new Particle(this.x, WATER_LEVEL, 'splash'));
SFX.playSplash();
this.game.floatingTexts.push(new FloatingText(this.x, WATER_LEVEL - 20, 'DROWNED!', '#38bdf8'));
this.die();
}
}
draw(ctx) {
if (this.isDead) {
// Draw Tombstone
ctx.save();
ctx.fillStyle = '#64748b';
ctx.fillRect(this.x - 8, this.y - 16, 16, 18);
ctx.fillStyle = '#cbd5e1';
ctx.font = '10px Fredoka';
ctx.fillText('RIP', this.x - 7, this.y - 4);
ctx.restore();
return;
}
ctx.save();
ctx.translate(this.x, this.y);
// Worm Body
ctx.fillStyle = '#f43f5e'; // Pink cute body
ctx.beginPath();
ctx.arc(0, -6, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#9f1239';
ctx.lineWidth = 2;
ctx.stroke();
// Team Headband / Helmet
ctx.fillStyle = this.team.color;
ctx.fillRect(-this.radius, -18, this.radius * 2, 6);
// Eye
const eyeOffset = this.facingRight ? 4 : -4;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(eyeOffset, -8, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(eyeOffset + (this.facingRight ? 1 : -1), -8, 2, 0, Math.PI * 2);
ctx.fill();
// Active Worm Pointer & Name Tag
if (this === this.game.getCurrentWorm()) {
// Reticle Arrow
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.moveTo(0, -32);
ctx.lineTo(-6, -42);
ctx.lineTo(6, -42);
ctx.closePath();
ctx.fill();
}
// Health Bar
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(-16, -26, 32, 5);
ctx.fillStyle = this.hp > 30 ? '#22c55e' : '#ef4444';
ctx.fillRect(-16, -26, (this.hp / 100) * 32, 5);
ctx.restore();
}
}
/* --- PROJECTILE ENTITY CLASS --- */
class Projectile {
constructor(game, x, y, vx, vy, wpn) {
this.game = game;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.wpn = wpn;
this.fuse = wpn.fuse || 0;
this.bounces = 0;
this.destroyed = false;
this.spawnTime = Date.now();
}
update() {
if (this.destroyed) return;
// Physics
this.vx += (this.game.wind * 0.002) * (this.wpn.windFactor || 1);
this.vy += this.wpn.gravity || 0.25;
this.x += this.vx;
this.y += this.vy;
// Smoke Trail Particles
if (Math.random() > 0.3) {
this.game.particles.push(new Particle(this.x, this.y, 'smoke'));
}
// Fuse Countdown for Grenades
if (this.fuse > 0) {
this.fuse -= 0.016;
if (this.fuse <= 0) {
this.explode();
return;
}
}
// Collision with Terrain
if (this.game.isSolidPixel(this.x, this.y)) {
if (this.wpn.bounce > 0 && this.bounces < 3) {
this.vx *= -this.wpn.bounce;
this.vy *= -this.wpn.bounce;
this.bounces++;
SFX.playClick();
} else {
this.explode();
}
}
// Water Splash
if (this.y > WATER_LEVEL) {
SFX.playSplash();
this.game.particles.push(new Particle(this.x, WATER_LEVEL, 'splash'));
this.destroyed = true;
this.game.endTurn(1200);
}
}
explode() {
if (this.destroyed) return;
this.destroyed = true;
if (this.wpn.choir) SFX.playChoir();
// Cluster Bomb Splitting
if (this.wpn.clusters) {
for (let i = 0; i < this.wpn.clusters; i++) {
const subVx = (Math.random() - 0.5) * 8;
const subVy = -Math.random() * 6 - 2;
this.game.projectiles.push(new Projectile(this.game, this.x, this.y - 10, subVx, subVy, WEAPONS[0]));
}
}
this.game.carveExplosion(this.x, this.y, this.wpn.radius);
this.game.endTurn(1500);
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(Math.atan2(this.vy, this.vx));
ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.wpn.icon, 0, 0);
ctx.restore();
}
}
/* --- DEBRIS CHUNK ENTITY CLASS --- */
class DebrisChunk {
constructor(game, x, y) {
this.game = game;
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 7;
this.vy = -Math.random() * 6 - 1;
this.size = Math.random() * 5 + 3;
this.rotation = Math.random() * Math.PI * 2;
this.vRot = (Math.random() - 0.5) * 0.2;
this.destroyed = false;
}
update() {
this.vy += 0.3; // Gravity
this.x += this.vx;
this.y += this.vy;
this.rotation += this.vRot;
if (this.game.isSolidPixel(this.x, this.y) || this.y > WATER_LEVEL) {
this.destroyed = true;
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.fillStyle = '#475569';
ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
ctx.restore();
}
}
/* --- PARTICLE SYSTEM --- */
class Particle {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
this.life = 1.0;
this.decay = Math.random() * 0.03 + 0.015;
if (type === 'explosion') {
const ang = Math.random() * Math.PI * 2;
const spd = Math.random() * 8 + 2;
this.vx = Math.cos(ang) * spd;
this.vy = Math.sin(ang) * spd;
this.size = Math.random() * 12 + 6;
this.color = ['#f97316', '#ef4444', '#eab308', '#475569'][Math.floor(Math.random() * 4)];
} else if (type === 'smoke') {
this.vx = (Math.random() - 0.5) * 1;
this.vy = -Math.random() * 1.5;
this.size = Math.random() * 8 + 4;
this.color = 'rgba(148, 163, 184, 0.4)';
} else if (type === 'splash') {
this.vx = (Math.random() - 0.5) * 4;
this.vy = -Math.random() * 5 - 2;
this.size = Math.random() * 4 + 2;
this.color = '#38bdf8';
} else if (type === 'tracer') {
this.vx = 0;
this.vy = 0;
this.size = 3;
this.color = '#fef08a';
this.decay = 0.08;
} else if (type === 'teleport' || type === 'jetpack') {
this.vx = (Math.random() - 0.5) * 3;
this.vy = Math.random() * 3;
this.size = 4;
this.color = type === 'jetpack' ? '#f97316' : '#a855f7';
}
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= this.decay;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = Math.max(0, this.life);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
/* --- FLOATING COMBAT TEXT --- */
class FloatingText {
constructor(x, y, text, color) {
this.x = x;
this.y = y;
this.text = text;
this.color = color;
this.life = 1.0;
this.vy = -1.2;
}
update() {
this.y += this.vy;
this.life -= 0.02;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = Math.max(0, this.life);
ctx.font = 'bold 18px Fredoka';
ctx.fillStyle = this.color;
ctx.textAlign = 'center';
ctx.shadowColor = '#000';
ctx.shadowBlur = 4;
ctx.fillText(this.text, this.x, this.y);
ctx.restore();
}
}
// Initialize Game Instance on Page Load
window.onload = () => {
window.game = new Game();
};
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.6-flash","provider":"openrouter","persona":"basic","sessionId":"a027258f-236b-4c7c-bbe4-a4ea76b26aa7","tokensIn":544,"tokensOut":23625,"tokensTotal":24169,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-22T14:10:22.762Z"} -->