Worms
Qwen 3.6 27B Fable Fusion 711 Uncensored Heretic NM DAU NEO MAX MTP GGUF · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 100,263 bytes · SHA-256 dce881006588
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worms Wars - Artillery Game</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a1a;font-family:'Segoe UI',system-ui,sans-serif;color:#fff}
#game-container{position:relative;width:100%;height:100%}
#gameCanvas{display:block;position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain}
canvas:focus{outline:none}
/* HUD Overlay */
#hud{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}
/* Top Bar */
#top-bar{position:absolute;top:0;left:0;right:0;height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:linear-gradient(180deg,rgba(10,10,30,0.95) 0%,rgba(10,10,30,0.7) 100%);backdrop-filter:blur(8px);pointer-events:auto;z-index:10}
#wind-indicator{display:flex;align-items:center;gap:6px;font-size:13px;color:#8ec8ff}
#wind-arrow{font-size:18px;transition:transform 0.3s}
#wind-value{font-weight:700;color:#fff}
#turn-timer-container{position:relative;width:140px;height:20px;background:rgba(255,255,255,0.08);border-radius:10px;overflow:hidden;border:1px solid rgba(255,255,255,0.1)}
#turn-timer-fill{height:100%;background:linear-gradient(90deg,#4ecdc4,#ffe66d);transition:width 0.1s linear;border-radius:10px}
#turn-timer-fill.danger{background:linear-gradient(90deg,#ff6b6b,#ee5a24);animation:pulse-danger 0.3s ease infinite alternate}
@keyframes pulse-danger{from{opacity:1}to{opacity:0.7}}
#turn-timer-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:11px;font-weight:700;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.5)}
#team-info{display:flex;align-items:center;gap:8px;font-size:13px}
#team-color-dot{width:10px;height:10px;border-radius:50%;border:2px solid rgba(255,255,255,0.3)}
#team-name{font-weight:600}
#worm-name{color:#ffd700;font-weight:700}
/* Health Bars */
#team-health{position:absolute;top:48px;left:8px;display:flex;flex-direction:column;gap:2px;pointer-events:auto;z-index:10}
.team-health-row{display:flex;gap:4px}
.worm-health{width:52px;height:14px;background:rgba(0,0,0,0.6);border-radius:3px;position:relative;overflow:hidden;border:1px solid rgba(255,255,255,0.15);transition:all 0.3s}
.worm-health.active{border-color:#ffd700;box-shadow:0 0 8px rgba(255,215,0,0.4)}
.worm-health.dead{opacity:0.3}
.worm-health-bar{height:100%;border-radius:2px;transition:width 0.4s ease}
.worm-health-label{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.8)}
/* Bottom Control Panel */
#control-panel{position:absolute;bottom:0;left:0;right:0;height:100px;display:flex;align-items:flex-end;justify-content:center;gap:8px;padding:8px 12px;pointer-events:auto;z-index:10}
#weapon-panel{display:flex;gap:4px;background:rgba(10,10,30,0.9);backdrop-filter:blur(10px);border-radius:12px;padding:6px 8px;border:1px solid rgba(255,255,255,0.1)}
.weapon-slot{width:56px;height:56px;background:rgba(255,255,255,0.05);border-radius:8px;border:2px solid rgba(255,255,255,0.1);display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:all 0.2s;position:relative;user-select:none}
.weapon-slot:hover{background:rgba(255,255,255,0.12);transform:translateY(-2px)}
.weapon-slot.selected{border-color:#4ecdc4;background:rgba(78,205,196,0.15);box-shadow:0 0 12px rgba(78,205,196,0.3)}
.weapon-slot .icon{font-size:22px}
.weapon-slot .count{font-size:9px;color:#aaa;position:absolute;bottom:3px;right:5px}
.weapon-slot .key{position:absolute;top:2px;left:5px;font-size:8px;color:#666}
.weapon-slot.exhausted{opacity:0.4;pointer-events:none}
.weapon-slot .tooltip{display:none;position:absolute;bottom:62px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.95);color:#fff;font-size:11px;padding:4px 8px;border-radius:6px;white-space:nowrap;border:1px solid rgba(255,255,255,0.1);z-index:100}
.weapon-slot:hover .tooltip{display:block}
.weapon-slot .ammo-badge{position:absolute;top:-4px;right:-4px;background:#ff6b6b;color:#fff;font-size:8px;padding:1px 4px;border-radius:8px;font-weight:700}
#items-panel{display:flex;flex-direction:column;gap:4px;align-items:center}
.item-row{display:flex;gap:3px}
.item-slot{width:36px;height:36px;background:rgba(255,255,255,0.05);border-radius:6px;border:1px solid rgba(255,255,255,0.1);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:16px;transition:all 0.2s;position:relative}
.item-slot:hover{background:rgba(255,255,255,0.12);transform:scale(1.1)}
.item-slot.no-items{opacity:0.3;pointer-events:none}
.item-slot .item-count{position:absolute;top:-4px;right:-4px;font-size:8px;color:#ffd700;background:rgba(0,0,0,0.7);padding:0 3px;border-radius:4px}
/* End Turn Button */
#end-turn-btn{position:absolute;bottom:12px;right:16px;padding:8px 16px;background:linear-gradient(135deg,#e74c3c,#c0392b);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;transition:all 0.2s;box-shadow:0 4px 15px rgba(231,76,60,0.4);pointer-events:auto;z-index:10}
#end-turn-btn:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(231,76,60,0.6)}
/* Menu Screen */
#menu-screen,#game-over-screen{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a1a 0%,#1a1a3e 50%,#0d2b45 100%);z-index:100}
#game-over-screen{display:none}
.game-title{font-size:64px;font-weight:900;background:linear-gradient(135deg,#ffd700,#ff6b6b,#4ecdc4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px;text-shadow:none;letter-spacing:-1px}
.game-subtitle{font-size:16px;color:#8ec8ff;margin-bottom:32px;letter-spacing:2px;text-transform:uppercase}
.menu-section{margin-bottom:16px;text-align:center}
.menu-label{font-size:12px;color:#8ec8ff;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
.menu-buttons{display:flex;gap:8px;justify-content:center}
.menu-btn{padding:10px 20px;background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.15);border-radius:8px;font-size:14px;cursor:pointer;transition:all 0.2s;font-weight:600}
.menu-btn:hover{background:rgba(255,255,255,0.16);transform:translateY(-1px)}
.menu-btn.active{background:rgba(78,205,196,0.2);border-color:#4ecdc4;color:#4ecdc4}
.start-btn{margin-top:24px;padding:14px 48px;background:linear-gradient(135deg,#4ecdc4,#44a08d);color:#fff;border:none;border-radius:10px;font-size:18px;font-weight:800;cursor:pointer;transition:all 0.2s;box-shadow:0 6px 25px rgba(78,205,196,0.4);letter-spacing:1px;text-transform:uppercase}
.start-btn:hover{transform:translateY(-3px) scale(1.02);box-shadow:0 10px 35px rgba(78,205,196,0.6)}
/* Game Over */
.game-over-title{font-size:48px;font-weight:900;color:#ffd700;margin-bottom:16px}
.winner-info{font-size:24px;color:#fff;margin-bottom:32px}
.game-over-buttons{display:flex;gap:12px}
/* Turn Announcement */
#turn-announcement{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:32px;font-weight:900;color:#ffd700;text-shadow:0 4px 20px rgba(0,0,0,0.8),0 0 40px rgba(255,215,0,0.3);opacity:0;transition:opacity 0.3s;pointer-events:none;z-index:50;text-align:center}
#turn-announcement.show{opacity:1}
#turn-announcement .team-color{font-size:40px}
#turn-announcement .worm-name-display{font-size:28px;margin-top:4px;color:#fff}
/* Crosshair overlay */
#crosshair-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:13px;color:rgba(255,255,255,0.6);pointer-events:none;z-index:5;text-align:center}
/* Water indicator */
#water-level-line{position:absolute;left:0;right:0;height:2px;background:rgba(78,205,196,0.3);pointer-events:none;z-index:5}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="hud" style="display:none">
<div id="top-bar">
<div id="wind-indicator">
<span style="color:#666;font-size:10px">WIND</span>
<span id="wind-arrow">→</span>
<span id="wind-value">0.0</span>
</div>
<div id="turn-timer-container">
<div id="turn-timer-fill"></div>
<span id="turn-timer-text">30</span>
</div>
<div id="team-info">
<div id="team-color-dot"></div>
<span id="worm-name"></span>
<span style="color:#666">of</span>
<span id="team-name"></span>
</div>
</div>
<div id="team-health"></div>
<div id="control-panel">
<div id="weapon-panel"></div>
<div id="items-panel">
<div class="item-row" id="items-row"></div>
</div>
</div>
<button id="end-turn-btn">⏩ End Turn</button>
<div id="turn-announcement"></div>
<div id="crosshair-hint">Left Click to fire • Drag mouse to aim</div>
</div>
<div id="menu-screen">
<div class="game-title">WORMS WARS</div>
<div class="game-subtitle">Artillery Tactics</div>
<div class="menu-section">
<div class="menu-label">Teams</div>
<div class="menu-buttons">
<button class="menu-btn active" data-teams="2">2 Teams</button>
<button class="menu-btn" data-teams="3">3 Teams</button>
<button class="menu-btn" data-teams="4">4 Teams</button>
</div>
</div>
<div class="menu-section">
<div class="menu-label">CPU Difficulty</div>
<div class="menu-buttons">
<button class="menu-btn active" data-difficulty="medium">Medium</button>
<button class="menu-btn" data-difficulty="easy">Easy</button>
<button class="menu-btn" data-difficulty="hard">Hard</button>
</div>
</div>
<div class="menu-section">
<div class="menu-label">Terrain</div>
<div class="menu-buttons">
<button class="menu-btn active" data-terrain="island">Island</button>
<button class="menu-btn" data-terrain="cave">Cave</button>
<button class="menu-btn" data-terrain="dual">Dual Islands</button>
</div>
</div>
<div class="menu-section">
<div class="menu-label">CPU Players</div>
<div class="menu-buttons">
<button class="menu-btn" data-cpu="0">None</button>
<button class="menu-btn" data-cpu="1">1</button>
<button class="menu-btn" data-cpu="2" style="background:rgba(78,205,196,0.2);border-color:#4ecdc4;color:#4ecdc4">2</button>
<button class="menu-btn" data-cpu="3">3</button>
</div>
</div>
<button class="start-btn" id="start-btn">▶ START BATTLE</button>
</div>
<div id="game-over-screen">
<div class="game-over-title">🎉 VICTORY</div>
<div class="winner-info" id="winner-info"></div>
<div class="game-over-buttons">
<button class="start-btn" id="restart-btn">Play Again</button>
<button class="menu-btn" id="menu-btn">Main Menu</button>
</div>
</div>
</div>
<script>
// ============================================================
// WORMS WARS - Complete Game Implementation
// ============================================================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ==================== CONSTANTS ====================
const GAME_WIDTH = 2000;
const GAME_HEIGHT = 800;
const WATER_LEVEL = 680;
const GRAVITY = 0.35;
const UNIT_MAX_SPEED = 3;
const UNIT_JUMP_FORCE = -8;
const TURN_TIME = 30;
const FALL_DAMAGE_THRESHOLD = 14;
const WORM_RADIUS = 12;
// Set canvas to game dimensions, CSS will scale it
canvas.width = GAME_WIDTH;
canvas.height = GAME_HEIGHT;
// ==================== AUDIO ENGINE ====================
const AudioEngine = {
ctx: null,
masterGain: null,
init() {
if (this.ctx) return;
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
},
play(type, params = {}) {
if (!this.ctx) return;
const t = this.ctx.currentTime;
switch(type) {
case 'explosion-small': this._explosion(t, 150, 0.3, params); break;
case 'explosion-medium': this._explosion(t, 100, 0.5, params); break;
case 'explosion-large': this._explosion(t, 60, 0.8, params); break;
case 'explosion-nuke': this._explosion(t, 40, 1.5, params); break;
case 'shot': this._shot(t); break;
case 'hit': this._hit(t); break;
case 'jump': this._jump(t); break;
case 'click': this._click(t); break;
case 'menu-hover': this._click(t, 0.8); break;
case 'menu-select': this._click(t, 1.2); break;
case 'timer-tick': this._timerTick(t); break;
case 'timer-urgent': this._timerTick(t, 800); break;
case 'drown': this._drown(t); break;
case 'splash': this._splash(t); break;
case 'powerup': this._powerup(t); break;
case 'banana': this._banana(t); break;
case 'laser': this._laser(t); break;
case 'freeze': this._freeze(t); break;
case 'cheer': this._cheer(t); break;
}
},
_explosion(t, freq, duration, params) {
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.8, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
gain.connect(this.masterGain);
const noise = this.ctx.createBufferSource();
const bufSize = this.ctx.sampleRate * duration;
const buf = this.ctx.createBuffer(1, bufSize, this.ctx.sampleRate);
const data = buf.getChannelData(0);
for(let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.2));
noise.buffer = buf;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = freq + (params.extraLow ? -50 : 0);
noise.connect(filter);
filter.connect(gain);
noise.start(t);
const osc = this.ctx.createOscillator();
osc.frequency.setValueAtTime(freq, t);
osc.frequency.exponentialRampToValueAtTime(30, t + duration);
osc.connect(gain);
osc.start(t);
osc.stop(t + duration);
},
_shot(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(600, t);
osc.frequency.exponentialRampToValueAtTime(200, t + 0.15);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.15);
},
_hit(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.frequency.value = 300;
gain.gain.setValueAtTime(0.4, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.08);
},
_jump(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(300, t);
osc.frequency.linearRampToValueAtTime(500, t + 0.1);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.12);
},
_click(t, vol = 1) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'square';
osc.frequency.value = 800;
gain.gain.setValueAtTime(0.15 * vol, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.05);
},
_timerTick(t, freq = 1200) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.06);
},
_drown(t) {
for(let i = 0; i < 3; i++) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(400 - i*100, t + i*0.1);
gain.gain.setValueAtTime(0.2, t + i*0.1);
gain.gain.exponentialRampToValueAtTime(0.001, t + i*0.1 + 0.2);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t + i*0.1);
osc.stop(t + i*0.1 + 0.2);
}
},
_splash(t) {
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.4, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
gain.connect(this.masterGain);
const noise = this.ctx.createBufferSource();
const buf = this.ctx.createBuffer(1, this.ctx.sampleRate * 0.3, this.ctx.sampleRate);
const data = buf.getChannelData(0);
for(let i = 0; i < buf.length; i++) data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (buf.length * 0.3));
noise.buffer = buf;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 800;
noise.connect(filter);
filter.connect(gain);
noise.start(t);
},
_powerup(t) {
[400, 500, 600, 800].forEach((f, i) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = f;
gain.gain.setValueAtTime(0, t + i*0.05);
gain.gain.linearRampToValueAtTime(0.2, t + i*0.05 + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, t + i*0.05 + 0.1);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t + i*0.05);
osc.stop(t + i*0.05 + 0.1);
});
},
_banana(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(600, t);
osc.frequency.linearRampToValueAtTime(200, t + 0.2);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.2);
},
_laser(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(2000, t);
osc.frequency.exponentialRampToValueAtTime(400, t + 0.4);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.4);
},
_freeze(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.linearRampToValueAtTime(2000, t + 0.3);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.3);
},
_cheer(t) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(400, t);
osc.frequency.linearRampToValueAtTime(600, t + 0.1);
osc.frequency.linearRampToValueAtTime(800, t + 0.2);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.5);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.5);
}
};
const TEAM_COLORS = [
{name:'Red Team', color:'#e74c3c', light:'#ff6b6b', dark:'#c0392b'},
{name:'Blue Team', color:'#3498db', light:'#5dade2', dark:'#2980b9'},
{name:'Green Team', color:'#2ecc71', light:'#58d68d', dark:'#27ae60'},
{name:'Purple Team', color:'#9b59b6', light:'#bb8fce', dark:'#8e44ad'}
];
const WORM_NAMES = [
'Sgt. Stinky', 'Lt. Loopy', 'Cpl. Wiggles', 'Pvt. Gloop',
'Capt. Crunch', 'Maj. Fluffy', 'Gen. Ooze', 'Brig. Squish',
'Sgt. Noodle', 'Lt. Puddle', 'Cpl. Bouncy', 'Pvt. Splat',
'Capt. Wobble', 'Maj. Gooey', 'Gen. Fartington', 'Brig. Sludge'
];
// ==================== WEAPONS DEFINITION ====================
const WEAPONS = {
bazooka: {
name:'Bazooka', icon:'🚀', type:'indirect', damage:25, radius:50,
speed:7, gravity:0.15, fuse:40, color:'#ff9800', sound:'explosion-small',
ammo:5, desc:'Reliable rocket launcher'
},
grenade: {
name:'Grenade', icon:'💣', type:'indirect', damage:20, radius:40,
speed:9, gravity:0.25, fuse:50, color:'#4caf50', sound:'explosion-small',
ammo:8, desc:'Hand-thrown explosive'
},
shotgun: {
name:'Shotgun', icon:'🔫', type:'direct', damage:35, radius:15,
speed:15, spread:0.3, pellets:5, color:'#9e9e9e', sound:'shot', instant:true,
ammo:3, desc:'Close-range devastation'
},
cluster: {
name:'Cluster Bomb', icon:'🎯', type:'indirect', damage:15, radius:80,
speed:6, gravity:0.12, fuse:60, color:'#ff5722', sound:'explosion-medium',
submunitions:8, subRadius:25, subDamage:10, ammo:2, desc:'Area denial weapon'
},
super: {
name:'Super-explosive', icon:'💥', type:'aoe', damage:45, radius:100,
speed:5, gravity:0.1, fuse:50, color:'#f44336', sound:'explosion-large',
ammo:1, desc:'Massive destruction'
},
laser: {
name:'Laser', icon:'⚡', type:'direct', damage:30, radius:5,
speed:25, color:'#e91e63', sound:'laser', instant:true, ammo:4,
desc:'Instant hit energy beam'
},
uzi: {
name:'Uzi', icon:'🔫', type:'direct', damage:4, radius:10,
speed:12, gravity:0.05, ammo:50, burst:15, color:'#2196f3', sound:'shot',
desc:'Rapid fire submachine gun'
},
bananabomb: {
name:'Banana Bomb', icon:'🍌', type:'indirect', damage:20, radius:45,
speed:8, gravity:0.2, fuse:120, color:'#ffeb3b', sound:'banana', ammo:3,
desc:'Tactical fruit weapon'
},
nuke: {
name:'Nuclear Bomb', icon:'☢️', type:'aoe', damage:80, radius:200,
speed:4, gravity:0.08, fuse:80, color:'#76ff03', sound:'explosion-nuke',
ammo:1, desc:'MAP WIPING'
},
dynamite: {
name:'Dynamite', icon:'🧨', type:'indirect', damage:30, radius:55,
speed:7, gravity:0.2, fuse:90, color:'#ff9800', sound:'explosion-medium',
ammo:4, desc:'Pick up and place explosive'
}
};
const ITEMS = {
medkit: {name:'Medkit', icon:'❤️', action:'heal', amount:50},
parachute: {name:'Parachute', icon:'🪂', action:'parachute'},
speed: {name:'Speed Boost', icon:'⚡', action:'speed', duration:200},
freeze: {name:'Ice Bomb', icon:'❄️', action:'freeze_enemy', duration:180, radius:100},
extraTime: {name:'Extra Time', icon:'⏰', action:'extra_time', amount:15}
};
// ==================== GAME STATE ====================
const game = {
state: 'menu',
width: GAME_WIDTH,
height: GAME_HEIGHT,
teams: [],
worms: [],
currentTeamIndex: 0,
currentWormIndex: 0,
turnTimer: TURN_TIME,
lastTick: 0,
wind: 0,
projectiles: [],
particles: [],
floatingTexts: [],
debris: [],
terrain: null,
terrainData: null,
waterData: null,
selectedWeapon: 0,
isAiming: false,
aimStart: null,
aimEnd: null,
camera: {x:0, y:0, zoom:1, targetX:0, targetY:0, targetZoom:1},
shake: {x:0, y:0, intensity:0, decay:0.9},
freezeTimer: 0,
config: {
numTeams: 2,
cpuCount: 2,
difficulty: 'medium',
terrain: 'island'
},
terrainOffsetY: 150,
waterLine: WATER_LEVEL
};
// ==================== TERRAIN GENERATION ====================
const TerrainGen = {
generate(type) {
const w = GAME_WIDTH;
const h = GAME_HEIGHT;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
// Generate height map
const heights = new Float32Array(w);
switch(type) {
case 'island': this._generateIsland(heights, w); break;
case 'cave': this._generateCave(heights, w, imageData); break;
case 'dual': this._generateDualIslands(heights, w); break;
}
// Fill terrain from heights
if(type !== 'cave') {
const skyGradient = this._generateSky(data, w, h);
for(let x = 0; x < w; x++) {
const groundY = heights[x];
for(let y = 0; y < h; y++) {
const idx = (y * w + x) * 4;
if(y < groundY) {
// Sky - use gradient
// Already set
} else if(y === groundY) {
// Grass top
const v = 0.35 + Math.random() * 0.15;
data[idx] = Math.floor(34 * v);
data[idx+1] = Math.floor(139 * v);
data[idx+2] = Math.floor(34 * v);
data[idx+3] = 255;
} else if(y < groundY + 25) {
// Dirt
const v = 0.4 + Math.random() * 0.2;
data[idx] = Math.floor(120 * v);
data[idx+1] = Math.floor(80 * v);
data[idx+2] = Math.floor(50 * v);
data[idx+3] = 255;
} else {
// Rock
const v = 0.3 + Math.random() * 0.15;
data[idx] = Math.floor(80 * v);
data[idx+1] = Math.floor(80 * v);
data[idx+2] = Math.floor(85 * v);
data[idx+3] = 255;
}
}
}
}
// Water
this._generateWater(data, w, h);
return {imageData, heights};
},
_noise(x, seed) {
const n = Math.sin(x * 12.9898 + seed * 78.233) * 43758.5453;
return n - Math.floor(n);
},
_smoothNoise(x, seed) {
const i = Math.floor(x);
const f = x - i;
const a = this._noise(i, seed);
const b = this._noise(i + 1, seed);
return a + (b - a) * (f * f * (3 - 2 * f));
},
_fbm(x, seed, octaves = 4) {
let val = 0, amp = 1, freq = 1, max = 0;
for(let i = 0; i < octaves; i++) {
val += this._smoothNoise(x * freq, seed + i * 10) * amp;
max += amp;
amp *= 0.5;
freq *= 2;
}
return val / max;
},
_generateIsland(heights, w) {
const baseHeight = GAME_HEIGHT * 0.55;
for(let x = 0; x < w; x++) {
const nx = x / w;
// Island shape - higher in middle, slopes to edges
const islandShape = 1 - Math.pow(Math.abs(nx - 0.5) * 2, 1.5);
const noise = this._fbm(nx * 8, 42);
const height = baseHeight - islandShape * 180 + noise * 60;
heights[x] = Math.min(Math.max(height, 100), GAME_HEIGHT - 80);
}
},
_generateDualIslands(heights, w) {
const baseHeight = GAME_HEIGHT * 0.55;
for(let x = 0; x < w; x++) {
const nx = x / w;
// Two peaks
const d1 = Math.abs(nx - 0.28);
const d2 = Math.abs(nx - 0.72);
const island = Math.max(
1 - Math.pow(d1 * 4, 1.2),
1 - Math.pow(d2 * 4, 1.2)
);
const noise = this._fbm(nx * 10, 99);
const height = baseHeight - island * 200 + noise * 50;
heights[x] = Math.min(Math.max(height, 100), GAME_HEIGHT - 80);
}
},
_generateCave(heights, w, imageData) {
const data = imageData.data;
const ceilingY = GAME_HEIGHT * 0.25;
const floorBase = GAME_HEIGHT * 0.55;
// Generate floor with noise
for(let x = 0; x < w; x++) {
const nx = x / w;
const noise = this._fbm(nx * 6, 77);
heights[x] = floorBase + noise * 40;
}
// Fill cave - dark background
for(let i = 0; i < data.length; i += 4) {
data[i] = 30; data[i+1] = 25; data[i+2] = 35; data[i+3] = 255;
}
// Cave ceiling
const ceilingHeights = new Float32Array(w);
for(let x = 0; x < w; x++) {
const nx = x / w;
const noise = this._fbm(nx * 7, 55);
ceilingHeights[x] = ceilingY + noise * 50;
}
for(let x = 0; x < w; x++) {
const cy = ceilingHeights[x];
const fy = heights[x];
for(let y = 0; y < GAME_HEIGHT; y++) {
const idx = (y * w + x) * 4;
if(y >= cy && y < fy) {
// Air in cave - dark
const depth = (y - cy) / (fy - cy);
const v = 0.15 - depth * 0.08;
data[idx] = Math.floor(40 * v);
data[idx+1] = Math.floor(35 * v);
data[idx+2] = Math.floor(55 * v);
}
}
}
// Ceiling rock
for(let x = 0; x < w; x++) {
for(let y = 0; y < ceilingHeights[x]; y++) {
const idx = (y * w + x) * 4;
const v = 0.3 + Math.random() * 0.15;
data[idx] = Math.floor(70 * v);
data[idx+1] = Math.floor(65 * v);
data[idx+2] = Math.floor(75 * v);
}
}
// Floor terrain
for(let x = 0; x < w; x++) {
const fy = heights[x];
for(let y = fy; y < GAME_HEIGHT; y++) {
const idx = (y * w + x) * 4;
const v = 0.3 + Math.random() * 0.15;
if(y < fy + 20) {
data[idx] = Math.floor(100 * v);
data[idx+1] = Math.floor(90 * v);
data[idx+2] = Math.floor(100 * v);
} else {
data[idx] = Math.floor(60 * v);
data[idx+1] = Math.floor(55 * v);
data[idx+2] = Math.floor(65 * v);
}
}
}
// Water in cave (at bottom)
this._generateWater(data, w, GAME_HEIGHT);
},
_generateSky(data, w, h) {
for(let y = 0; y < GAME_HEIGHT; y++) {
const t = Math.min(y / 400, 1);
const r = 15 + t * 40;
const g = 20 + t * 50;
const b = 60 + t * 80;
for(let x = 0; x < w; x++) {
const idx = (y * w + x) * 4;
data[idx] = r;
data[idx+1] = g;
data[idx+2] = b;
data[idx+3] = 255;
}
}
},
_generateWater(data, w, h) {
const waterY = WATER_LEVEL;
for(let y = waterY; y < h; y++) {
const depth = (y - waterY) / (h - waterY);
const r = Math.floor(30 - depth * 20);
const g = Math.floor(80 + depth * 20);
const b = Math.floor(140 + depth * 40);
for(let x = 0; x < w; x++) {
const idx = (y * w + x) * 4;
// Only color if this is water area (not terrain)
if(data[idx+3] === 255 && (depth > 0.1 || Math.random() > 0.3)) {
data[idx] = r;
data[idx+1] = g;
data[idx+2] = b;
data[idx+3] = Math.floor(200 + depth * 55);
}
}
}
},
_isTerrain(x, y) {
if(x < 0 || x >= GAME_WIDTH || y < 0 || y >= GAME_HEIGHT) return true;
const idx = (Math.floor(y) * GAME_WIDTH + Math.floor(x)) * 4;
// If pixel is opaque and not sky/water colored
const a = game.terrainData.data[idx + 3];
if(a < 128) return false;
const r = game.terrainData.data[idx];
const g = game.terrainData.data[idx + 1];
const b = game.terrainData.data[idx + 2];
// Sky: dark blue-purple; Water: teal-blue
if(r < 40 && g < 40 && b < 100 && y < WATER_LEVEL - 50) return false;
if(r > 25 && g > 70 && b > 130 && y > WATER_LEVEL - 20) return false;
return true;
},
_isAir(x, y) {
if(x < 0 || x >= GAME_WIDTH || y < 0 || y >= GAME_HEIGHT) return false;
const idx = (Math.floor(y) * GAME_WIDTH + Math.floor(x)) * 4;
const a = game.terrainData.data[idx + 3];
const r = game.terrainData.data[idx];
const g = game.terrainData.data[idx + 1];
const b = game.terrainData.data[idx + 2];
// Sky area
if(r < 60 && g < 70 && b < 140 && y < WATER_LEVEL - 50) return true;
// Cave air
if(r < 30 && g < 30 && b < 50 && y < WATER_LEVEL - 50) return true;
// Water
if(y >= WATER_LEVEL - 10 && r >= 25 && g >= 70 && b >= 130 && a > 180) return true;
return false;
},
_isWater(x, y) {
if(y < WATER_LEVEL - 5) return false;
if(x < 0 || x >= GAME_WIDTH || y < 0 || y >= GAME_HEIGHT) return false;
const idx = (Math.floor(y) * GAME_WIDTH + Math.floor(x)) * 4;
const r = game.terrainData.data[idx];
const g = game.terrainData.data[idx + 1];
const b = game.terrainData.data[idx + 2];
return r >= 25 && g >= 70 && b >= 130 && game.terrainData.data[idx + 3] > 180;
},
_setAir(x, y) {
if(x < 0 || x >= GAME_WIDTH || y < 0 || y >= GAME_HEIGHT) return;
const idx = (Math.floor(y) * GAME_WIDTH + Math.floor(x)) * 4;
game.terrainData.data[idx] = 15;
game.terrainData.data[idx+1] = 20;
game.terrainData.data[idx+2] = 60;
game.terrainData.data[idx+3] = 255;
},
_sampleHeight(x) {
const xi = Math.floor(x);
if(xi < 0 || xi >= GAME_WIDTH) return GAME_HEIGHT;
for(let y = 0; y < GAME_HEIGHT; y++) {
if(this._isTerrain(xi, y)) return y;
}
return GAME_HEIGHT;
}
};
// ==================== PARTICLE SYSTEM ====================
const Particles = {
create(x, y, type, count, color, params) {
for(let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (params?.speedMin || 1) + Math.random() * ((params?.speedMax || 4) - (params?.speedMin || 1));
const life = (params?.lifeMin || 20) + Math.random() * ((params?.lifeMax || 40) - (params?.lifeMin || 20));
game.particles.push({
x, y,
vx: Math.cos(angle) * speed + (params?.vx || 0),
vy: Math.sin(angle) * speed + (params?.vy || 0) - (params?.lift || 0),
life, maxLife: life,
size: (params?.sizeMin || 2) + Math.random() * ((params?.sizeMax || 5) - (params?.sizeMin || 2)),
color: color || `hsl(${30 + Math.random()*30}, 100%, ${50 + Math.random()*30}%)`,
type: type || 'default',
gravity: params?.gravity || 0.05,
friction: params?.friction || 0.98,
shape: params?.shape || 'circle',
trail: params?.trail || false,
glow: params?.glow || false,
rotation: Math.random() * Math.PI * 2,
rotSpeed: (Math.random() - 0.5) * 0.2,
colorEnd: params?.colorEnd || null,
sizeEnd: params?.sizeEnd || null
});
}
},
update() {
for(let i = game.particles.length - 1; i >= 0; i--) {
const p = game.particles[i];
p.life--;
p.vx *= p.friction;
p.vy *= p.friction;
p.vy += p.gravity;
p.x += p.vx;
p.y += p.vy;
p.rotation += p.rotSpeed;
if(p.type === 'spark') {
p.size *= 0.96;
}
if(p.type === 'smoke') {
p.size *= 1.02;
}
if(p.type === 'debris') {
if(TerrainGen._isTerrain(p.x, p.y + p.size)) {
p.vy *= -0.3;
p.vx *= 0.7;
p.y -= p.size;
}
}
if(p.life <= 0 || p.x < -50 || p.x > GAME_WIDTH + 50 || p.y > GAME_HEIGHT + 50) {
game.particles.splice(i, 1);
}
}
},
draw(ctx) {
for(const p of game.particles) {
const alpha = p.life / p.maxLife;
let color = p.color;
if(p.colorEnd && alpha < 1) {
color = this._lerpColor(p.color, p.colorEnd, 1 - alpha);
}
const size = p.sizeEnd ? p.size * (alpha * 0.5 + 0.5) : p.size * (alpha * 0.7 + 0.3);
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
if(p.glow) {
ctx.shadowBlur = 10;
ctx.shadowColor = color;
}
if(p.shape === 'circle') {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, size, 0, Math.PI * 2);
ctx.fill();
} else if(p.shape === 'square') {
ctx.fillStyle = color;
ctx.fillRect(-size/2, -size/2, size, size);
} else if(p.shape === 'star') {
ctx.fillStyle = color;
this._drawStar(ctx, 0, 0, 5, size, size * 0.4);
} else if(p.shape === 'line') {
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-size, 0);
ctx.lineTo(size, 0);
ctx.stroke();
}
ctx.restore();
}
},
_drawStar(ctx, cx, cy, spikes, outerR, innerR) {
let rot = Math.PI / 2 * 3;
const step = Math.PI / spikes;
ctx.beginPath();
ctx.moveTo(cx, cy - outerR);
for(let i = 0; i < spikes; i++) {
ctx.lineTo(cx + Math.cos(rot) * outerR, cy + Math.sin(rot) * outerR);
rot += step;
ctx.lineTo(cx + Math.cos(rot) * innerR, cy + Math.sin(rot) * innerR);
rot += step;
}
ctx.closePath();
ctx.fill();
},
_lerpColor(c1, c2, t) {
if(!c1 || !c2) return c1 || c2;
if(c1.startsWith('hsl')) return c1;
const r1 = parseInt(c1.slice(1,3),16), g1 = parseInt(c1.slice(3,5),16), b1 = parseInt(c1.slice(5,7),16);
const r2 = parseInt(c2.slice(1,3),16), g2 = parseInt(c2.slice(3,5),16), b2 = parseInt(c2.slice(5,7),16);
const r = Math.round(r1 + (r2-r1)*t);
const g = Math.round(g1 + (g2-g1)*t);
const b = Math.round(b1 + (b2-b1)*t);
return `rgb(${r},${g},${b})`;
},
explosion(x, y, radius, color) {
// Fire
this.create(x, y, 'fire', 30, null, {
speedMin: 2, speedMax: 8, lifeMin: 15, lifeMax: 35,
sizeMin: 3, sizeMax: 10, gravity: -0.1, lift: 2, shape:'circle',
glow: true, colorEnd: '#333333'
});
// Sparks
this.create(x, y, 'spark', 20, '#ffd700', {
speedMin: 4, speedMax: 12, lifeMin: 20, lifeMax: 50,
sizeMin: 1, sizeMax: 3, gravity: 0.15, shape:'circle'
});
// Smoke
this.create(x, y, 'smoke', 15, null, {
speedMin: 1, speedMax: 4, lifeMin: 40, lifeMax: 80,
sizeMin: 5, sizeMax: 15, gravity: -0.08, lift: 1, shape:'circle',
colorEnd: '#555555'
});
},
nuke(x, y) {
// Massive explosion
this.explosion(x, y, 200, null);
this.create(x, y, 'fire', 80, null, {
speedMin: 5, speedMax: 18, lifeMin: 30, lifeMax: 80,
sizeMin: 8, sizeMax: 25, gravity: -0.05, lift: 3, shape:'circle', glow:true
});
// Shockwave ring particles
this.create(x, y, 'shockwave', 40, '#ff4444', {
speedMin: 12, speedMax: 16, lifeMin: 20, lifeMax: 40,
sizeMin: 3, sizeMax: 6, gravity: 0, friction: 0.96, shape:'circle', glow:true
});
},
debris(x, y, count) {
const colors = ['#8B4513','#654321','#A0522D','#556B2F','#696969'];
this.create(x, y, 'debris', count, null, {
speedMin: 1, speedMax: 5, lifeMin: 60, lifeMax: 150,
sizeMin: 3, sizeMax: 8, gravity: 0.2, friction: 0.99,
shape:'square'
});
},
splash(x, y) {
this.create(x, y, 'splash', 20, '#4fc3f7', {
speedMin: 2, speedMax: 6, lifeMin: 15, lifeMax: 30,
sizeMin: 2, sizeMax: 5, gravity: 0.15, vy: -3, shape:'circle'
});
},
muzzleFlash(x, y, dir) {
this.create(x, y, 'spark', 8, '#ffff00', {
speedMin: 3, speedMax: 8, lifeMin: 5, lifeMax: 12,
sizeMin: 2, sizeMax: 5, vx: Math.cos(dir)*5, vy: Math.sin(dir)*5,
shape:'circle', glow:true
});
},
trail(x, y) {
this.create(x, y, 'smoke', 1, '#aaaaaa', {
speedMin: 0, speedMax: 1, lifeMin: 15, lifeMax: 25,
sizeMin: 2, sizeMax: 4, gravity: -0.05, shape:'circle'
});
},
landing(x, y) {
this.create(x, y, 'dust', 12, '#d4a76a', {
speedMin: 1, speedMax: 3, lifeMin: 15, lifeMax: 30,
sizeMin: 2, sizeMax: 6, gravity: -0.05, vy:-1, shape:'circle'
});
},
freezeEffect(x, y, radius) {
this.create(x, y, 'spark', 30, '#00bcd4', {
speedMin: 2, speedMax: 8, lifeMin: 30, lifeMax: 60,
sizeMin: 2, sizeMax: 8, gravity: -0.05, shape:'star', glow:true
});
}
};
// ==================== FLOATING TEXT ====================
const FloatingText = {
add(x, y, text, color, size = 16) {
game.floatingTexts.push({
x, y, text, color, size,
life: 60, maxLife: 60,
vy: -1.5, scale: 1.2
});
},
update() {
for(let i = game.floatingTexts.length - 1; i >= 0; i--) {
const ft = game.floatingTexts[i];
ft.life--;
ft.y += ft.vy;
ft.vy *= 0.95;
ft.scale = 1 + (ft.life / ft.maxLife) * 0.3;
if(ft.life <= 0) game.floatingTexts.splice(i, 1);
}
},
draw(ctx) {
ctx.textAlign = 'center';
for(const ft of game.floatingTexts) {
const alpha = ft.life / ft.maxLife;
ctx.globalAlpha = alpha;
ctx.font = `bold ${ft.size * ft.scale}px "Segoe UI", sans-serif`;
ctx.fillStyle = '#000';
ctx.fillText(ft.text, ft.x + 1, ft.y + 1);
ctx.fillStyle = ft.color;
ctx.fillText(ft.text, ft.x, ft.y);
}
ctx.globalAlpha = 1;
}
};
// ==================== WORM CLASS ====================
class Worm {
constructor(team, name, color) {
this.team = team;
this.name = name;
this.color = color;
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
this.hp = 100;
this.maxHp = 100;
this.alive = true;
this.radius = WORM_RADIUS;
this.onGround = false;
this.facing = 1;
this.moveSpeed = UNIT_MAX_SPEED;
this.parachute = false;
this.frozen = 0;
this.invincible = 0;
this.drowning = 0;
this.lastFallVY = 0;
this.turnStartTime = 0;
this.weaponAmmo = {};
for(const key in WEAPONS) this.weaponAmmo[key] = WEAPONS[key].ammo;
this.items = [];
this.emoteTimer = 0;
}
startTurn() {
this.turnStartTime = Date.now();
this.invincible = 30;
}
update() {
if(!this.alive) return;
this.emoteTimer--;
if(this.frozen > 0) {
this.frozen--;
if(this.frozen <= 0) {
Particles.freezeEffect(this.x, this.y, 30);
}
return;
}
if(this.invincible > 0) this.invincible--;
// Apply wind to projectile-like movement
if(this.parachute) {
this.vx += game.wind * 0.02;
}
// Gravity
if(!this.onGround || this.parachute) {
this.vy += GRAVITY;
}
// Check water
const inWater = TerrainGen._isWater(this.x, this.y + this.radius);
if(inWater && !this.parachute) {
this.drowning++;
this.vy *= 0.85;
this.vx *= 0.85;
if(this.drowning > 180) {
this.hp -= 1;
if(this.hp <= 0) {
this.die();
return;
}
if(this.drowning % 30 === 0) {
Particles.splash(this.x, this.y);
AudioEngine.play('drown');
}
}
} else {
this.drowning = Math.max(0, this.drowning - 5);
}
// Parachute physics
if(this.parachute) {
this.vy = Math.min(this.vy, 1.5);
}
// Move
this.x += this.vx;
this.y += this.vy;
// Terrain collision
this._collideTerrain();
// Fall damage tracking
this.lastFallVY = this.vy;
// Boundaries
if(this.x < this.radius) { this.x = this.radius; this.vx *= -0.3; }
if(this.x > GAME_WIDTH - this.radius) { this.x = GAME_WIDTH - this.radius; this.vx *= -0.3; }
// Check if fallen out of map
if(this.y > GAME_HEIGHT + 50) {
this.hp -= 40;
if(this.hp <= 0) this.die();
else {
this.hp -= 40;
FloatingText.add(this.x, GAME_HEIGHT - 50, 'FELL OUT!', '#ff4444', 18);
this.respawn();
}
}
}
_collideTerrain() {
const r = this.radius;
let groundY = GAME_HEIGHT;
let onGround = false;
// Check ground
const gx = Math.floor(this.x);
for(let y = Math.floor(this.y + r); y >= 0; y--) {
if(TerrainGen._isTerrain(gx, y)) {
groundY = y;
break;
}
}
if(this.y + r >= groundY && !TerrainGen._isWater(this.x, this.y + r)) {
this.y = groundY - r;
this.vy = 0;
onGround = true;
// Fall damage
if(this.lastFallVY > FALL_DAMAGE_THRESHOLD) {
const dmg = Math.floor((this.lastFallVY - FALL_DAMAGE_THRESHOLD) * 3);
this.takeDamage(dmg);
FloatingText.add(this.x, this.y - 20, `-${dmg} FALL`, '#ffaa00', 14);
Particles.landing(this.x, groundY);
}
}
// Side collision
const leftX = Math.floor(this.x - r);
const rightX = Math.floor(this.x + r);
const wormY = Math.floor(this.y);
if(onGround && this.vx !== 0) {
for(let yOff = -r + 2; yOff <= r - 2; yOff++) {
const checkY = Math.floor(this.y + yOff);
if(this.vx > 0 && TerrainGen._isTerrain(rightX, checkY) && !TerrainGen._isAir(rightX, checkY)) {
this.x = rightX - r;
this.vx = 0;
}
if(this.vx < 0 && TerrainGen._isTerrain(leftX, checkY) && !TerrainGen._isAir(leftX, checkY)) {
this.x = leftX + r;
this.vx = 0;
}
}
}
this.onGround = onGround;
}
jump() {
if(this.onGround && !this.frozen && this.alive) {
this.vy = UNIT_JUMP_FORCE;
this.onGround = false;
AudioEngine.play('jump');
Particles.landing(this.x, this.y + this.radius);
this.emoteTimer = 10;
}
}
takeDamage(amount) {
if(this.invincible > 0 || !this.alive) return;
this.hp -= amount;
if(this.hp <= 0) {
this.die();
} else {
this.hp = Math.max(0, this.hp);
}
}
die() {
if(!this.alive) return;
this.alive = false;
Particles.explosion(this.x, this.y, 30, this.color);
Particles.create(this.x, this.y, 'debris', 10, '#8B4513', {
speedMin: 2, speedMax: 6, lifeMin: 40, lifeMax: 100,
sizeMin: 4, sizeMax: 10, gravity: 0.2, friction: 0.99, shape:'square'
});
FloatingText.add(this.x, this.y - 20, '💀 DEAD!', '#ff0000', 20);
AudioEngine.play('explosion-medium');
game.shake.intensity = 5;
}
respawn() {
// Find nearest friendly landing spot
const teamWorms = game.worms.filter(w => w.team === this.team && w.alive);
let bestX = GAME_WIDTH / 2;
let bestY = 100;
for(const w of teamWorms) {
const dist = Math.abs(w.x - this.x);
if(dist < Math.abs(bestX - this.x)) {
bestX = w.x + (Math.random() > 0.5 ? 80 : -80);
}
}
bestX = Math.max(100, Math.min(GAME_WIDTH - 100, bestX));
for(let y = 0; y < GAME_HEIGHT; y++) {
if(TerrainGen._isTerrain(Math.floor(bestX), y) && y < WATER_LEVEL - 20) {
bestY = y - 30;
break;
}
}
this.x = bestX;
this.y = bestY;
this.vx = 0;
this.vy = 0;
this.parachute = true;
this.hp = 50;
AudioEngine.play('powerup');
FloatingText.add(this.x, this.y - 20, 'RESPAWN!', '#4ecdc4', 16);
Particles.create(this.x, this.y, 'spark', 20, '#4ecdc4', {
speedMin: 2, speedMax: 6, lifeMin: 20, lifeMax: 40,
sizeMin: 2, sizeMax: 5, shape:'circle', glow:true
});
}
draw(ctx) {
if(!this.alive) return;
ctx.save();
ctx.translate(this.x, this.y);
// Invincibility flash
if(this.invincible > 0 && Math.floor(this.invincible / 4) % 2 === 0) {
ctx.globalAlpha = 0.5;
}
// Frozen effect
if(this.frozen > 0) {
ctx.fillStyle = 'rgba(0,188,212,0.3)';
ctx.beginPath();
ctx.arc(0, 0, this.radius + 4, 0, Math.PI * 2);
ctx.fill();
}
// Parachute
if(this.parachute) {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-8, -30);
ctx.lineTo(0, -15);
ctx.moveTo(8, -30);
ctx.lineTo(0, -15);
ctx.stroke();
ctx.fillStyle = '#ff5722';
ctx.beginPath();
ctx.arc(-8, -32, 6, Math.PI, 0);
ctx.arc(8, -32, 6, Math.PI, 0);
ctx.fill();
}
// Drowning indicator
if(this.drowning > 60) {
const bubbleY = -this.radius - 5 - (Math.sin(Date.now()/200) * 3);
ctx.fillStyle = 'rgba(100,200,255,0.7)';
ctx.beginPath();
ctx.arc(8 + Math.sin(Date.now()/300)*5, bubbleY - this.drowning % 30 * 0.5, 3, 0, Math.PI*2);
ctx.fill();
}
// Worm body - segmented
const segments = 4;
const segWidth = this.radius * 1.5;
const time = Date.now() / 100;
for(let i = segments - 1; i >= 0; i--) {
const segX = (i - (segments-1)/2) * (segWidth * 0.5);
const wave = Math.sin(time + i * 0.8) * 2;
const segRadius = this.radius * (1 - Math.abs(i - (segments-1)/2) * 0.15);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.ellipse(segX, wave, segRadius * 1.3, segRadius, 0, 0, Math.PI * 2);
ctx.fill();
// Highlight
ctx.fillStyle = this.color.light || this.color;
ctx.globalAlpha = ctx.globalAlpha || 1;
const highlightAlpha = 0.3 * ctx.globalAlpha;
ctx.fillStyle = `rgba(255,255,255,${highlightAlpha})`;
ctx.beginPath();
ctx.ellipse(segX - 2, wave - 2, segRadius * 0.5, segRadius * 0.3, -0.3, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Face (on front segment)
const faceX = this.facing * segWidth * 0.35;
const faceWave = Math.sin(time + (segments-1) * 0.8) * 2;
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(faceX - 3 * this.facing, faceWave - 3, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(faceX - 2 * this.facing, faceWave - 3, 2, 0, Math.PI * 2);
ctx.fill();
// Mouth
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.5;
ctx.beginPath();
if(this.emoteTimer > 0) {
// Happy/open mouth when jumping
ctx.arc(faceX, faceWave + 3, 3, 0, Math.PI);
} else {
// Neutral smile
ctx.arc(faceX, faceWave + 1, 2.5, 0.2, Math.PI - 0.2);
}
ctx.stroke();
// Drowning sad face
if(this.drowning > 120) {
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(faceX, faceWave + 5, 2.5, Math.PI + 0.2, -0.2);
ctx.stroke();
}
// Health bar
const barWidth = 24;
const barHeight = 3;
const barX = -barWidth / 2;
const barY = -this.radius - 10;
const hpRatio = this.hp / this.maxHp;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(barX, barY, barWidth, barHeight);
ctx.fillStyle = hpRatio > 0.5 ? '#4caf50' : hpRatio > 0.25 ? '#ff9800' : '#f44336';
ctx.fillRect(barX, barY, barWidth * hpRatio, barHeight);
ctx.restore();
}
}
// ==================== PROJECTILE CLASS ====================
class Projectile {
constructor(x, y, vx, vy, weapon, owner) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.weapon = weapon;
this.owner = owner;
this.life = weapon.fuse || 300;
this.active = true;
this.radius = 4;
this.trailTimer = 0;
this.isDynamite = weapon.name === 'Dynamite';
this.pickedUp = false;
this.carrier = null;
}
update() {
if(!this.active) return;
// Wind effect
this.vx += game.wind * this.weapon.gravity * 0.3;
// Gravity
this.vy += this.weapon.gravity || 0;
if(this.isDynamite && this.pickedUp) {
this.x = this.carrier.x;
this.y = this.carrier.y - 20;
return;
}
this.x += this.vx;
this.y += this.vy;
this.trailTimer++;
if(this.trailTimer % 3 === 0 && !this.weapon.instant) {
Particles.trail(this.x, this.y);
}
// Terrain collision
if(TerrainGen._isTerrain(this.x, this.y) && !TerrainGen._isAir(this.x, this.y)) {
this.explode();
return;
}
// Water collision
if(TerrainGen._isWater(this.x, this.y)) {
Particles.splash(this.x, this.y);
AudioEngine.play('splash');
this.explode();
return;
}
// Worm collision
for(const worm of game.worms) {
if(!worm.alive || worm.team === this.owner.team) continue;
const dx = this.x - worm.x;
const dy = this.y - worm.y;
if(Math.sqrt(dx*dx + dy*dy) < worm.radius + this.radius) {
this.explode();
return;
}
}
// Fuse
this.life--;
if(this.life <= 0) {
this.explode();
}
// Out of bounds
if(this.x < -50 || this.x > GAME_WIDTH + 50 || this.y > GAME_HEIGHT + 50) {
this.active = false;
}
}
explode() {
this.active = false;
const w = this.weapon;
let explosionRadius = w.radius;
let damage = w.damage;
// Sound based on weapon type
AudioEngine.play(w.sound);
// Visual effects
if(w.name === 'Nuclear Bomb') {
Particles.nuke(this.x, this.y);
FloatingText.add(this.x, this.y - 30, '☢️ NUKED!', '#76ff03', 24);
game.freezeTimer = 25;
game.shake.intensity = 20;
} else if(w.name === 'Super-explosive') {
Particles.explosion(this.x, this.y, explosionRadius);
Particles.debris(this.x, this.y, 15);
game.freezeTimer = 15;
game.shake.intensity = 12;
} else {
Particles.explosion(this.x, this.y, explosionRadius);
Particles.debris(this.x, this.y, 8);
game.shake.intensity = Math.max(3, explosionRadius / 10);
}
// Terrain destruction
this._destroyTerrain();
// Damage worms
for(const worm of game.worms) {
if(!worm.alive) continue;
const dx = this.x - worm.x;
const dy = this.y - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist < explosionRadius + worm.radius) {
const falloff = 1 - (dist / (explosionRadius + worm.radius));
const actualDamage = Math.max(1, Math.floor(damage * falloff));
worm.takeDamage(actualDamage);
FloatingText.add(worm.x, worm.y - 15, `-${actualDamage}`, '#ff4444', 18);
worm.vx += (dx / dist) * explosionRadius * 0.15;
worm.vy += (dy / dist) * explosionRadius * 0.15 - 3;
}
}
// Cluster bomb submunitions
if(w.submunitions) {
for(let i = 0; i < w.submunitions; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 3 + Math.random() * 4;
const proj = new Projectile(this.x, this.y,
Math.cos(angle) * speed + game.wind * 0.5,
Math.sin(angle) * speed - 2,
{damage: w.subDamage, radius: w.subRadius, fuse: 40, sound: 'explosion-small', name:'Sub'},
this.owner
);
game.projectiles.push(proj);
}
}
}
_destroyTerrain() {
const w = this.weapon;
const r = w.radius;
const sx = Math.max(0, Math.floor(this.x - r));
const ex = Math.min(GAME_WIDTH, Math.ceil(this.x + r));
const sy = Math.max(0, Math.floor(this.y - r));
const ey = Math.min(GAME_HEIGHT, Math.ceil(this.y + r));
const data = game.terrainData.data;
for(let y = sy; y < ey; y++) {
for(let x = sx; x < ex; x++) {
const dx = x - this.x;
const dy = y - this.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist < r && TerrainGen._isTerrain(x, y)) {
TerrainGen._setAir(x, y);
}
}
}
ctx.putImageData(game.terrainData, 0, 0);
}
draw(ctx) {
if(!this.active || (this.isDynamite && this.pickedUp)) return;
ctx.save();
ctx.translate(this.x, this.y);
const w = this.weapon;
if(w.name === 'Laser') {
ctx.strokeStyle = '#e91e63';
ctx.lineWidth = 3;
ctx.shadowBlur = 8;
ctx.shadowColor = '#e91e63';
ctx.beginPath();
ctx.moveTo(-8, 0);
ctx.lineTo(8, 0);
ctx.stroke();
} else if(w.name === 'Banana Bomb') {
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f57f17';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
} else if(w.name === 'Nuclear Bomb') {
// Pulsing glow
const pulse = 1 + Math.sin(Date.now() / 100) * 0.3;
ctx.fillStyle = '#76ff03';
ctx.shadowBlur = 15;
ctx.shadowColor = '#76ff03';
ctx.beginPath();
ctx.arc(0, 0, 6 * pulse, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#33691e';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('☢', 0, 4);
} else if(w.name === 'Dynamite') {
ctx.fillStyle = '#8d6e63';
ctx.fillRect(-3, -6, 6, 12);
ctx.fillStyle = '#ff9800';
ctx.fillRect(-2, -8, 4, 3);
// Fuse spark
if(this.life % 4 < 2) {
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(0, -10, 2, 0, Math.PI * 2);
ctx.fill();
}
} else {
// Default projectile
ctx.fillStyle = w.color || '#ff9800';
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.fill();
// Trail glow
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.beginPath();
ctx.arc(-1, -1, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
// ==================== AI ====================
const AI = {
decideAction(worm) {
if(!worm.alive || worm.frozen > 0) return {action:'wait'};
const enemies = game.worms.filter(w => w.alive && w.team !== worm.team);
if(enemies.length === 0) return {action:'wait'};
const difficultyMod = game.config.difficulty === 'hard' ? 0.85 :
game.config.difficulty === 'easy' ? 0.6 : 0.75;
// Risk assessment - avoid dangerous positions
const risk = this._assessRisk(worm);
if(risk.high) {
return {action:'move_to_safety', x: risk.safeX};
}
// Choose target
const target = this._chooseTarget(worm, enemies);
if(!target) return {action:'wait'};
// Choose weapon
const weaponKey = this._chooseWeapon(worm, target);
if(!weaponKey) return {action:'wait'};
const weapon = WEAPONS[weaponKey];
if(!weapon) return {action:'wait'};
// Calculate aim
const aim = this._calculateAim(worm, target, weapon, difficultyMod);
if(!aim) return {action:'wait'};
return {
action:'fire',
weapon: weaponKey,
aimX: aim.x,
aimY: aim.y
};
},
_assessRisk(worm) {
// Check if near edge
const heightAtPos = TerrainGen._sampleHeight(worm.x);
const leftHeight = TerrainGen._sampleHeight(worm.x - 30);
const rightHeight = TerrainGen._sampleHeight(worm.x + 30);
if(leftHeight > heightAtPos + 40) return {high:true, safeX: worm.x - 40};
if(rightHeight > heightAtPos + 40) return {high:true, safeX: worm.x + 40};
// Check if near water
if(TerrainGen._isWater(worm.x, worm.y + worm.radius + 5)) {
return {high:true, safeX: worm.x - 50};
}
return {high:false};
},
_chooseTarget(worm, enemies) {
let bestTarget = null;
let bestScore = -Infinity;
for(const enemy of enemies) {
const dx = enemy.x - worm.x;
const dy = enemy.y - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy);
let score = 0;
// Lower HP enemies are easier targets
score += (enemy.hp / enemy.maxHp) * -100;
// Closer enemies are better
score += Math.max(0, 300 - dist);
// Enemies exposed (not in tunnels) are better
if(!this._isInTunnel(enemy)) score += 50;
if(score > bestScore) {
bestScore = score;
bestTarget = enemy;
}
}
return bestTarget;
},
_isInTunnel(worm) {
// Check if surrounded by terrain above
let terrainAbove = 0;
for(let dx = -10; dx <= 10; dx += 2) {
const x = Math.floor(worm.x + dx);
const y = Math.floor(worm.y - 15);
if(TerrainGen._isTerrain(x, y)) terrainAbove++;
}
return terrainAbove > 6;
},
_chooseWeapon(worm, target) {
const dx = target.x - worm.x;
const dy = target.y - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy);
const availableWeapons = Object.entries(WEAPONS)
.filter(([k, w]) => worm.weaponAmmo[k] > 0);
if(availableWeapons.length === 0) return null;
let bestWeapon = null;
let bestScore = -Infinity;
for(const [key, weapon] of availableWeapons) {
let score = 0;
if(weapon.type === 'direct' && weapon.instant) {
if(dist < 400 && this._hasLineOfSight(worm, target)) {
score += 80;
} else {
score -= 50;
}
} else if(weapon.type === 'indirect') {
score += 30;
// Prefer weapons with good range
const maxRange = weapon.speed * weapon.fuse;
if(maxRange > dist * 0.8) score += 20;
} else if(weapon.type === 'aoe') {
// Only use big weapons when multiple enemies nearby
const nearbyEnemies = game.worms.filter(e => {
if(!e.alive || e.team === worm.team) return false;
return Math.sqrt((e.x-worm.x)**2 + (e.y-worm.y)**2) < weapon.radius * 2;
}).length;
score += nearbyEnemies * 40;
if(dist > 200) score += 20;
}
// Weapon effectiveness bonus
if(weapon.damage > 30) score += 10;
if(weapon.radius > 60) score += 5;
if(score > bestScore) {
bestScore = score;
bestWeapon = key;
}
}
return bestWeapon;
},
_hasLineOfSight(from, to) {
const dx = to.x - from.x;
const dy = to.y - from.y;
const dist = Math.sqrt(dx*dx + dy*dy);
const steps = Math.floor(dist / 10);
for(let i = 1; i < steps; i++) {
const t = i / steps;
const x = from.x + dx * t;
const y = from.y + dy * t;
if(TerrainGen._isTerrain(x, y) && !TerrainGen._isAir(x, y)) return false;
}
return true;
},
_calculateAim(worm, target, weapon, accuracy) {
const dx = target.x - worm.x;
const dy = target.y - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(weapon.instant) {
return {x: target.x, y: target.y};
}
// Add inaccuracy based on difficulty
const inaccuracy = (1 - accuracy) * 80;
const aimX = target.x + (Math.random() - 0.5) * inaccuracy;
const aimY = target.y + (Math.random() - 0.5) * inaccuracy;
// Calculate angle using physics
const g = weapon.gravity || 0.1;
const speed = weapon.speed;
// For indirect fire, aim for a point above target
const targetYAbove = aimY - 50 - Math.random() * 100;
// Solve trajectory equation
const angle = this._calculateAngle(worm.x, worm.y, aimX, targetYAbove, speed, g);
if(angle === null) return null;
return {x: aimX, y: aimY};
},
_calculateAngle(x1, y1, x2, y2, speed, g) {
const dx = x2 - x1;
const dy = y2 - y1;
// Adjust for wind
const windAdj = game.wind * 2;
// Calculate required angle
const discriminant = g * g * dx * dx + (2 * g * dy - speed * speed * speed * speed) * dx * dx + speed * speed * speed * speed * speed * speed;
// Simplified approach
const baseAngle = Math.atan2(dy, dx);
// Add arc for indirect weapons
const arcAngle = baseAngle - 0.4;
return arcAngle;
}
};
// ==================== GAME LOGIC ====================
function initGame(config) {
AudioEngine.init();
game.config = config;
game.teams = [];
game.worms = [];
game.projectiles = [];
game.particles = [];
game.floatingTexts = [];
game.debris = [];
game.selectedWeapon = 0;
game.isAiming = false;
game.freezeTimer = 0;
game.shake.intensity = 0;
game.wind = (Math.random() - 0.5) * 4;
// Generate terrain
const terrainResult = TerrainGen.generate(config.terrain);
game.terrainData = terrainResult.imageData;
ctx.putImageData(game.terrainData, 0, 0);
// Create teams and worms
const numTeams = config.numTeams;
const names = [...WORM_NAMES].sort(() => Math.random() - 0.5);
let nameIdx = 0;
for(let t = 0; t < numTeams; t++) {
const teamColor = TEAM_COLORS[t % TEAM_COLORS.length];
game.teams.push({
id: t,
name: teamColor.name,
color: teamColor.color,
light: teamColor.light,
dark: teamColor.dark,
worms: [],
cpu: t >= numTeams - config.cpuCount
});
}
// Place worms on terrain
for(let t = 0; t < numTeams; t++) {
const team = game.teams[t];
const baseX = (t + 0.5) * (GAME_WIDTH / numTeams);
for(let w = 0; w < 4; w++) {
const worm = new Worm(t, names[nameIdx++], team.color);
// Find landing position
let placeX = baseX + (w - 1.5) * 40;
placeX = Math.max(80, Math.min(GAME_WIDTH - 80, placeX));
let placeY = 100;
for(let y = 0; y < GAME_HEIGHT - 50; y++) {
if(TerrainGen._isTerrain(Math.floor(placeX), y) && y < WATER_LEVEL - 30) {
placeY = y - 25;
break;
}
}
worm.x = placeX;
worm.y = placeY;
worm.team = t;
game.worms.push(worm);
team.worms.push(worm);
}
}
// Start first turn
game.currentTeamIndex = 0;
game.currentWormIndex = 0;
startTurn();
}
function startTurn() {
// Find next alive worm
let attempts = 0;
while(attempts < 100) {
const team = game.teams[game.currentTeamIndex];
const aliveWorms = team.worms.filter(w => w.alive);
if(aliveWorms.length === 0) {
// Team eliminated
game.currentTeamIndex = (game.currentTeamIndex + 1) % game.teams.length;
attempts++;
continue;
}
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(!worm.alive) {
game.currentWormIndex = (game.currentWormIndex + 1) % aliveWorms.length;
attempts++;
continue;
}
worm.startTurn();
game.turnTimer = TURN_TIME;
game.lastTick = Date.now();
game.isAiming = false;
// Camera to worm
game.camera.targetX = worm.x;
game.camera.targetY = worm.y;
// Announce turn
showTurnAnnouncement(team, worm);
// Wind change
if(Math.random() < 0.4) {
game.wind = (Math.random() - 0.5) * 4;
FloatingText.add(GAME_WIDTH/2, 100, 'WIND CHANGE!', '#8ec8ff', 14);
}
updateHUD();
// CPU turn
if(team.cpu) {
game.state = 'playing';
setTimeout(() => executeAITurn(worm), 500);
} else {
// Human turn - enable controls
game.state = 'playing';
}
}
}
function showTurnAnnouncement(team, worm) {
const el = document.getElementById('turn-announcement');
el.innerHTML = `<div class="team-color" style="color:${team.color}">${team.name}</div><div class="worm-name-display">${worm.name}</div>`;
el.classList.add('show');
AudioEngine.play('powerup');
setTimeout(() => el.classList.remove('show'), 1500);
}
function updateHUD() {
const team = game.teams[game.currentTeamIndex];
const aliveWorms = team.worms.filter(w => w.alive);
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(!worm) return;
// Team info
document.getElementById('team-color-dot').style.backgroundColor = team.color;
document.getElementById('worm-name').textContent = worm.name;
document.getElementById('team-name').textContent = team.name;
// Health bars
const healthContainer = document.getElementById('team-health');
healthContainer.innerHTML = '';
for(const t of game.teams) {
if(t.worms.length === 0) continue;
const row = document.createElement('div');
row.className = 'team-health-row';
for(const w of t.worms) {
const div = document.createElement('div');
div.className = 'worm-health' + (!w.alive ? ' dead' : '') +
(w === worm ? ' active' : '');
const hpRatio = Math.max(0, w.hp / w.maxHp);
const hpColor = hpRatio > 0.5 ? '#4caf50' : hpRatio > 0.25 ? '#ff9800' : '#f44336';
div.innerHTML = `
<div class="worm-health-bar" style="width:${hpRatio*100}%;background:${hpColor}"></div>
<div class="worm-health-label">${w.alive ? w.hp : '✕'}</div>
`;
row.appendChild(div);
}
healthContainer.appendChild(row);
}
// Weapon panel
const weaponPanel = document.getElementById('weapon-panel');
weaponPanel.innerHTML = '';
const weaponKeys = Object.keys(WEAPONS);
const keys = '1234567890';
weaponKeys.forEach((key, i) => {
const w = WEAPONS[key];
const hasAmmo = worm.weaponAmmo[key] > 0;
const slot = document.createElement('div');
slot.className = 'weapon-slot' + (i === game.selectedWeapon ? ' selected' : '') +
(!hasAmmo ? ' exhausted' : '');
slot.innerHTML = `
<span class="key">${keys[i] || ''}</span>
<span class="icon">${w.icon}</span>
<span class="count">${w.name}</span>
<span class="ammo-badge">${worm.weaponAmmo[key]}</span>
<div class="tooltip">${w.name} (${w.type})<br>${w.desc}</div>
`;
if(hasAmmo) {
slot.onclick = () => {
game.selectedWeapon = i;
AudioEngine.play('menu-hover');
updateHUD();
};
}
weaponPanel.appendChild(slot);
});
// Items panel
const itemsRow = document.getElementById('items-row');
itemsRow.innerHTML = '';
for(let i = 0; i < 4; i++) {
const itemKey = Object.keys(ITEMS)[i];
const item = ITEMS[itemKey];
const hasItem = worm.items.includes(itemKey);
const slot = document.createElement('div');
slot.className = 'item-slot' + (!hasItem ? ' no-items' : '');
slot.innerHTML = `<span>${item.icon}</span>`;
if(hasItem) {
slot.onclick = () => useItem(worm, itemKey);
}
itemsRow.appendChild(slot);
}
}
function useItem(worm, itemKey) {
const item = ITEMS[itemKey];
if(!worm.items.includes(itemKey)) return;
worm.items = worm.items.filter(i => i !== itemKey);
AudioEngine.play('powerup');
switch(item.action) {
case 'heal':
worm.hp = Math.min(worm.maxHp, worm.hp + item.amount);
FloatingText.add(worm.x, worm.y - 20, `+${item.amount} HP`, '#4caf50', 18);
Particles.create(worm.x, worm.y, 'spark', 15, '#4caf50', {
speedMin: 1, speedMax: 4, lifeMin: 15, lifeMax: 30,
sizeMin: 2, sizeMax: 5, gravity: -0.1, shape:'circle'
});
break;
case 'parachute':
worm.parachute = true;
FloatingText.add(worm.x, worm.y - 20, '🪂!', '#ffffff', 16);
break;
case 'speed':
worm.moveSpeed = UNIT_MAX_SPEED * 2;
setTimeout(() => worm.moveSpeed = UNIT_MAX_SPEED, (item.duration || 200) * 16.67);
FloatingText.add(worm.x, worm.y - 20, '⚡ SPEED!', '#ffeb3b', 16);
break;
case 'freeze_enemy':
for(const other of game.worms) {
if(!other.alive || other.team === worm.team) continue;
const dist = Math.sqrt((other.x-worm.x)**2 + (other.y-worm.y)**2);
if(dist < item.radius) {
other.frozen = item.duration;
FloatingText.add(other.x, other.y - 20, '❄️ FROZEN!', '#00bcd4', 18);
}
}
Particles.freezeEffect(worm.x, worm.y, item.radius);
AudioEngine.play('freeze');
break;
case 'extra_time':
game.turnTimer = Math.min(TURN_TIME, game.turnTimer + item.amount);
FloatingText.add(GAME_WIDTH/2, GAME_HEIGHT/2 - 50, `+${item.amount}s`, '#4ecdc4', 20);
break;
}
updateHUD();
}
function executeAITurn(worm) {
if(!worm.alive) { endTurn(); return; }
const action = AI.decideAction(worm);
if(action.action === 'wait' || action.action === 'move_to_safety') {
endTurn();
return;
}
if(action.action === 'fire') {
const weaponKey = action.weapon;
if(worm.weaponAmmo[weaponKey] > 0) {
const weapon = WEAPONS[weaponKey];
const dx = action.aimX - worm.x;
const dy = action.aimY - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
const angle = Math.atan2(dy, dx);
// Adjust angle for indirect weapons
let finalAngle = angle;
if(weapon.type === 'indirect' || weapon.type === 'aoe') {
finalAngle = Math.min(angle - 0.3, -0.2);
}
fireWeapon(worm, weaponKey, finalAngle);
}
}
setTimeout(() => endTurn(), 1500);
}
function fireWeapon(worm, weaponKey, angle) {
const weapon = WEAPONS[weaponKey];
if(!weapon || worm.weaponAmmo[weaponKey] <= 0) return;
worm.weaponAmmo[weaponKey]--;
let proj;
if(weapon.instant) {
// Direct hit laser/shotgun - instant damage
const dx = Math.cos(angle) * 500;
const dy = Math.sin(angle) * 500;
if(weapon.name === 'Laser') {
AudioEngine.play('laser');
// Check line for damage
for(const target of game.worms) {
if(!target.alive || target.team === worm.team) continue;
const tdx = target.x - worm.x;
const tdy = target.y - worm.y;
const tAngle = Math.atan2(tdy, tdx);
if(Math.abs(tAngle - angle) < 0.08) {
target.takeDamage(weapon.damage);
FloatingText.add(target.x, target.y - 15, `-${weapon.damage}`, '#ff4444', 20);
Particles.create(target.x, target.y, 'spark', 15, '#e91e63', {
speedMin: 2, speedMax: 6, lifeMin: 10, lifeMax: 25,
sizeMin: 2, sizeMax: 5, shape:'circle', glow:true
});
}
}
// Terrain damage along line
const steps = 50;
for(let i = 0; i < steps; i++) {
const t = i / steps;
const px = worm.x + dx * t;
const py = worm.y + dy * t;
if(TerrainGen._isTerrain(px, py)) {
const r = 3;
for(let ox = -r; ox <= r; ox++) {
for(let oy = -r; oy <= r; oy++) {
if(ox*ox + oy*oy < r*r && TerrainGen._isTerrain(px+ox, py+oy)) {
TerrainGen._setAir(px+ox, py+oy);
}
}
}
i = steps; // stop at first hit
}
}
ctx.putImageData(game.terrainData, 0, 0);
Particles.create(worm.x + dx * 0.5, worm.y + dy * 0.5, 'spark', 10, '#e91e63', {
speedMin: 1, speedMax: 4, lifeMin: 10, lifeMax: 20, glow:true
});
} else if(weapon.name === 'Shotgun') {
AudioEngine.play('shot');
Particles.muzzleFlash(
worm.x + Math.cos(angle) * worm.radius,
worm.y + Math.sin(angle) * worm.radius,
angle
);
for(let p = 0; p < weapon.pellets; p++) {
const pelletAngle = angle + (Math.random() - 0.5) * weapon.spread;
const target = findWormInLine(worm.x, worm.y, pelletAngle, 500);
if(target) {
target.takeDamage(Math.floor(weapon.damage / weapon.pellets));
FloatingText.add(target.x, target.y - 15, `-${Math.floor(weapon.damage/weapon.pellets)}`, '#ff4444', 14);
}
}
}
updateHUD();
return;
}
if(weapon.name === 'Uzi') {
for(let b = 0; b < weapon.burst && worm.weaponAmmo[weaponKey] >= 0; b++) {
setTimeout(() => {
if(worm.weaponAmmo[weaponKey] < 0) return;
const spreadAngle = angle + (Math.random() - 0.5) * 0.3;
const p = new Projectile(
worm.x + Math.cos(spreadAngle) * worm.radius,
worm.y + Math.sin(spreadAngle) * worm.radius,
Math.cos(spreadAngle) * weapon.speed + game.wind * 0.5,
Math.sin(spreadAngle) * weapon.speed,
{damage: weapon.damage, radius: weapon.radius, fuse:60, sound:'shot', name:'Uzi'},
worm.team
);
game.projectiles.push(p);
AudioEngine.play('shot');
}, b * 50);
}
updateHUD();
return;
}
const vx = Math.cos(angle) * weapon.speed;
const vy = Math.sin(angle) * weapon.speed;
proj = new Projectile(
worm.x + Math.cos(angle) * (worm.radius + 5),
worm.y + Math.sin(angle) * (worm.radius + 5),
vx, vy, weapon, worm.team
);
game.projectiles.push(proj);
AudioEngine.play('shot');
Particles.muzzleFlash(
worm.x + Math.cos(angle) * worm.radius,
worm.y + Math.sin(angle) * worm.radius,
angle
);
updateHUD();
}
function findWormInLine(x, y, angle, maxDist) {
let closest = null;
let closestDist = maxDist;
for(const worm of game.worms) {
if(!worm.alive || worm.team === 0) continue;
const dx = worm.x - x;
const dy = worm.y - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist > maxDist) continue;
const wormAngle = Math.atan2(dy, dx);
if(Math.abs(wormAngle - angle) < 0.05 && dist < closestDist) {
closestDist = dist;
closest = worm;
}
}
return closest;
}
function endTurn() {
const team = game.teams[game.currentTeamIndex];
const aliveWorms = team.worms.filter(w => w.alive);
game.currentWormIndex++;
if(game.currentWormIndex >= aliveWorms.length || !team.worms.some(w => w.alive)) {
game.currentWormIndex = 0;
game.currentTeamIndex = (game.currentTeamIndex + 1) % game.teams.length;
// Award random item
const nextTeam = game.teams[game.currentTeamIndex];
const nextAlive = nextTeam.worms.filter(w => w.alive);
if(nextAlive.length > 0) {
const itemKeys = Object.keys(ITEMS);
const randomItem = itemKeys[Math.floor(Math.random() * itemKeys.length)];
const worm = nextAlive[Math.floor(Math.random() * nextAlive.length)];
if(worm.items.length < 4) {
worm.items.push(randomItem);
FloatingText.add(worm.x, worm.y - 30, `${ITEMS[randomItem].icon} ${ITEMS[randomItem].name}!`, '#ffd700', 16);
AudioEngine.play('powerup');
}
}
}
checkGameEnd();
startTurn();
}
function checkGameEnd() {
const aliveTeams = game.teams.filter(t => t.worms.some(w => w.alive));
if(aliveTeams.length === 1) {
game.state = 'gameover';
const winner = aliveTeams[0];
document.getElementById('winner-info').textContent = `${winner.name} Wins!`;
document.getElementById('winner-info').style.color = winner.color;
document.getElementById('game-over-screen').style.display = 'flex';
AudioEngine.play('cheer');
}
}
// ==================== INPUT HANDLING ====================
let mouseX = 0, mouseY = 0;
let screenMouseX = 0, screenMouseY = 0;
let keysDown = {};
let aimMode = false;
canvas.addEventListener('mousedown', (e) => {
if(game.state !== 'playing') return;
if(game.teams[game.currentTeamIndex].cpu) return;
const rect = canvas.getBoundingClientRect();
screenMouseX = e.clientX - rect.left;
screenMouseY = e.clientY - rect.top;
mouseX = screenMouseX / game.camera.zoom + game.camera.x;
mouseY = screenMouseY / game.camera.zoom + game.camera.y;
const team = game.teams[game.currentTeamIndex];
const aliveWorms = team.worms.filter(w => w.alive);
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(!worm || worm.frozen > 0) return;
// Check dynamite pickup
for(const proj of game.projectiles) {
if(proj.weapon.name === 'Dynamite' && proj.life > 0 && !proj.pickedUp) {
const dx = mouseX - proj.x;
const dy = mouseY - proj.y;
if(Math.sqrt(dx*dx + dy*dy) < 30) {
proj.pickedUp = true;
proj.carrier = worm;
proj.life = 150; // Reset fuse on pickup
AudioEngine.play('powerup');
FloatingText.add(proj.x, proj.y - 20, 'PICKED UP!', '#ff9800', 14);
return;
}
}
}
aimMode = true;
game.aimStart = {x: mouseX, y: mouseY};
game.aimEnd = {x: mouseX, y: mouseY};
AudioEngine.play('click');
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
screenMouseX = e.clientX - rect.left;
screenMouseY = e.clientY - rect.top;
mouseX = screenMouseX / game.camera.zoom + game.camera.x;
mouseY = screenMouseY / game.camera.zoom + game.camera.y;
if(aimMode && game.aimStart) {
game.aimEnd = {x: mouseX, y: mouseY};
}
});
canvas.addEventListener('mouseup', (e) => {
if(!aimMode || game.state !== 'playing') {
aimMode = false;
return;
}
const team = game.teams[game.currentTeamIndex];
if(team.cpu) { aimMode = false; return; }
const aliveWorms = team.worms.filter(w => w.alive);
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(!worm || worm.frozen > 0) { aimMode = false; return; }
// Check if dropped dynamite
for(const proj of game.projectiles) {
if(proj.pickedUp && proj.carrier === worm) {
proj.x = mouseX;
proj.y = mouseY;
proj.pickedUp = false;
proj.carrier = null;
proj.vx = 0;
proj.vy = -2;
AudioEngine.play('hit');
FloatingText.add(proj.x, proj.y - 20, 'DROPPED!', '#ff9800', 14);
aimMode = false;
return;
}
}
// Fire weapon
const weaponKey = Object.keys(WEAPONS)[game.selectedWeapon];
const weapon = WEAPONS[weaponKey];
if(weapon && worm.weaponAmmo[weaponKey] > 0) {
const dx = game.aimEnd.x - worm.x;
const dy = game.aimEnd.y - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist > 20) {
let angle = Math.atan2(dy, dx);
// Force arc for indirect weapons if aim is too direct
if((weapon.type === 'indirect' || weapon.type === 'aoe') && angle > -0.15) {
angle = -0.6 + (Math.random() - 0.5) * 0.1;
}
fireWeapon(worm, weaponKey, angle);
}
}
aimMode = false;
});
document.addEventListener('keydown', (e) => {
keysDown[e.key] = true;
if(game.state !== 'playing') return;
const team = game.teams[game.currentTeamIndex];
if(team.cpu) return;
const aliveWorms = team.worms.filter(w => w.alive);
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(!worm || !worm.alive || worm.frozen > 0) return;
// Number keys for weapon selection
const num = parseInt(e.key);
if(num >= 1 && num <= Object.keys(WEAPONS).length) {
if(worm.weaponAmmo[Object.keys(WEAPONS)[num-1]] > 0) {
game.selectedWeapon = num - 1;
AudioEngine.play('menu-hover');
updateHUD();
}
}
// Jump
if(e.key === ' ' || e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
worm.jump();
}
// Zoom
if(e.key === '+' || e.key === '=') {
game.camera.targetZoom = Math.min(2, game.camera.zoom + 0.1);
}
if(e.key === '-' || e.key === '_') {
game.camera.targetZoom = Math.max(0.5, game.camera.zoom - 0.1);
}
});
document.addEventListener('keyup', (e) => {
keysDown[e.key] = false;
});
document.getElementById('end-turn-btn').addEventListener('click', () => {
AudioEngine.play('click');
endTurn();
});
// Menu handlers
document.querySelectorAll('[data-teams]').forEach(btn => {
btn.addEventListener('click', () => {
AudioEngine.init();
AudioEngine.play('menu-hover');
document.querySelectorAll('[data-teams]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
document.querySelectorAll('[data-difficulty]').forEach(btn => {
btn.addEventListener('click', () => {
AudioEngine.init();
AudioEngine.play('menu-hover');
document.querySelectorAll('[data-difficulty]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
document.querySelectorAll('[data-terrain]').forEach(btn => {
btn.addEventListener('click', () => {
AudioEngine.init();
AudioEngine.play('menu-hover');
document.querySelectorAll('[data-terrain]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
document.querySelectorAll('[data-cpu]').forEach(btn => {
btn.addEventListener('click', () => {
AudioEngine.init();
AudioEngine.play('menu-hover');
document.querySelectorAll('[data-cpu]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
document.getElementById('start-btn').addEventListener('click', () => {
AudioEngine.play('menu-select');
const teamsBtn = document.querySelector('[data-teams].active');
const diffBtn = document.querySelector('[data-difficulty].active');
const terrainBtn = document.querySelector('[data-terrain].active');
const cpuBtn = document.querySelector('[data-cpu].active');
const config = {
numTeams: parseInt(teamsBtn.dataset.teams),
cpuCount: parseInt(cpuBtn.dataset.cpu),
difficulty: diffBtn.dataset.difficulty,
terrain: terrainBtn.dataset.terrain
};
document.getElementById('menu-screen').style.display = 'none';
document.getElementById('hud').style.display = 'block';
initGame(config);
});
document.getElementById('restart-btn').addEventListener('click', () => {
AudioEngine.play('menu-select');
document.getElementById('game-over-screen').style.display = 'none';
document.getElementById('menu-screen').style.display = 'flex';
game.state = 'menu';
});
document.getElementById('menu-btn').addEventListener('click', () => {
AudioEngine.play('menu-select');
document.getElementById('game-over-screen').style.display = 'none';
document.getElementById('menu-screen').style.display = 'flex';
game.state = 'menu';
});
// ==================== RENDERING ====================
function render() {
const w = canvas.width;
const h = canvas.height;
// Clear
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, w, h);
// Camera update
const targetX = game.camera.targetX;
const targetY = game.camera.targetY;
const targetZoom = game.camera.targetZoom;
// Smooth camera
game.camera.x += (targetX - game.camera.x) * 0.05;
game.camera.y += (targetY - game.camera.y) * 0.05;
game.camera.zoom += (targetZoom - game.camera.zoom) * 0.05;
// Screen shake
if(game.shake.intensity > 0.5) {
game.shake.x = (Math.random() - 0.5) * game.shake.intensity;
game.shake.y = (Math.random() - 0.5) * game.shake.intensity;
game.shake.intensity *= game.shake.decay;
} else {
game.shake.x = 0;
game.shake.y = 0;
game.shake.intensity = 0;
}
ctx.save();
// Apply camera transform
const centerX = w / 2;
const centerY = h / 2;
ctx.translate(centerX + game.shake.x, centerY + game.shake.y);
ctx.scale(game.camera.zoom, game.camera.zoom);
ctx.translate(-game.camera.x, -game.camera.y);
// Draw terrain
if(game.terrainData) {
ctx.putImageData(game.terrainData, 0, 0);
} else {
// Show loading screen
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 36px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.fillText('WORMS WARS', GAME_WIDTH/2, GAME_HEIGHT/2 - 20);
ctx.fillStyle = '#8ec8ff';
ctx.font = '18px "Segoe UI", sans-serif';
ctx.fillText('Configure your game on the menu screen', GAME_WIDTH/2, GAME_HEIGHT/2 + 20);
}
// Draw water line effect
ctx.strokeStyle = 'rgba(78,205,196,0.3)';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
ctx.moveTo(0, WATER_LEVEL);
ctx.lineTo(GAME_WIDTH, WATER_LEVEL);
ctx.stroke();
ctx.setLineDash([]);
// Freeze effect
if(game.freezeTimer > 0) {
game.freezeTimer--;
ctx.fillStyle = `rgba(200,240,255,${game.freezeTimer / 25 * 0.3})`;
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
}
// Draw worms
for(const worm of game.worms) {
worm.draw(ctx);
}
// Draw projectiles
for(const proj of game.projectiles) {
proj.draw(ctx);
}
// Draw particles
Particles.draw(ctx);
// Draw floating texts
FloatingText.draw(ctx);
// Draw aiming trajectory
if(aimMode && game.aimStart && game.state === 'playing') {
drawAimingTrajectory();
}
ctx.restore();
// Wind indicator update
const windArrow = document.getElementById('wind-arrow');
const windValue = document.getElementById('wind-value');
windArrow.style.transform = `scaleX(${game.wind >= 0 ? 1 : -1})`;
windValue.textContent = Math.abs(game.wind).toFixed(1);
}
function drawAimingTrajectory() {
const team = game.teams[game.currentTeamIndex];
const aliveWorms = team.worms.filter(w => w.alive);
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(!worm) return;
const weaponKey = Object.keys(WEAPONS)[game.selectedWeapon];
const weapon = WEAPONS[weaponKey];
if(!weapon || weapon.instant) return;
const dx = game.aimEnd.x - worm.x;
const dy = game.aimEnd.y - worm.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist < 20) return;
let angle = Math.atan2(dy, dx);
if((weapon.type === 'indirect' || weapon.type === 'aoe') && angle > -0.15) {
angle = -0.6;
}
const speed = weapon.speed;
const g = weapon.gravity || 0.1;
const vx = Math.cos(angle) * speed;
const vy = Math.sin(angle) * speed;
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
ctx.beginPath();
let px = worm.x;
let py = worm.y;
let pvx = vx;
let pvy = vy;
ctx.moveTo(px, py);
for(let i = 0; i < 60; i++) {
pvx += game.wind * g * 0.3;
pvy += g;
px += pvx;
py += pvy;
if(py > GAME_HEIGHT || px < 0 || px > GAME_WIDTH || TerrainGen._isTerrain(px, py)) {
break;
}
ctx.lineTo(px, py);
}
ctx.stroke();
ctx.setLineDash([]);
// Draw explosion preview
const finalDist = Math.sqrt((px-worm.x)**2 + (py-worm.y)**2);
ctx.strokeStyle = weapon.color || '#ff9800';
ctx.lineWidth = 1;
ctx.globalAlpha = 0.4;
ctx.beginPath();
ctx.arc(px, py, weapon.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
}
// ==================== GAME LOOP ====================
let lastTime = 0;
function gameLoop(timestamp) {
const dt = timestamp - lastTime;
lastTime = timestamp;
if(game.state === 'playing' || game.state === 'gameover') {
update(dt);
}
render();
requestAnimationFrame(gameLoop);
}
function update(dt) {
// Timer
if(game.state === 'playing' && !game.teams[game.currentTeamIndex].cpu) {
const now = Date.now();
const elapsed = (now - game.lastTick) / 1000;
game.turnTimer -= elapsed;
game.lastTick = now;
// Timer warning sounds
if(game.turnTimer <= 5 && game.turnTimer > 4 && Math.floor(now/500)%2===0) {
AudioEngine.play('timer-tick');
}
if(game.turnTimer <= 2 && game.turnTimer > 1) {
AudioEngine.play('timer-urgent');
}
if(game.turnTimer <= 0) {
endTurn();
return;
}
}
// Update timer UI
const timerFill = document.getElementById('turn-timer-fill');
const timerText = document.getElementById('turn-timer-text');
const timerPct = Math.max(0, game.turnTimer / TURN_TIME);
timerFill.style.width = (timerPct * 100) + '%';
timerText.textContent = Math.ceil(game.turnTimer);
if(timerPct < 0.2) {
timerFill.classList.add('danger');
} else {
timerFill.classList.remove('danger');
}
// Worm movement
const team = game.teams[game.currentTeamIndex];
if(team && !team.cpu) {
const aliveWorms = team.worms.filter(w => w.alive);
const worm = aliveWorms[game.currentWormIndex % aliveWorms.length];
if(worm && worm.alive) {
if(keysDown['ArrowLeft'] || keysDown['a'] || keysDown['A']) {
worm.vx = -worm.moveSpeed;
worm.facing = -1;
} else if(keysDown['ArrowRight'] || keysDown['d'] || keysDown['D']) {
worm.vx = worm.moveSpeed;
worm.facing = 1;
} else if(worm.onGround) {
worm.vx *= 0.7;
}
}
}
// Update worms
for(const worm of game.worms) {
worm.update();
}
// Update projectiles
for(let i = game.projectiles.length - 1; i >= 0; i--) {
game.projectiles[i].update();
if(!game.projectiles[i].active && !game.projectiles[i].isDynamite) {
game.projectiles.splice(i, 1);
}
}
// Update particles
Particles.update();
// Update floating texts
FloatingText.update();
// Check game end
if(game.state === 'playing') {
checkGameEnd();
}
}
// ==================== RESIZE ====================
// Remove resize - canvas keeps fixed internal resolution, CSS scales it
window.addEventListener('resize', () => {});
// Start game loop
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF","provider":"brigid","persona":"full","sessionId":"6cd5413f-485c-4374-81b1-e390e53703b1","tokensIn":3300288,"tokensOut":39111,"tokensTotal":3339399,"cost":0,"turns":39,"toolCalls":38,"failedToolCalls":0,"timestamp":"2026-08-06T03:33:12.149Z"} -->