Worms
Gemini 3.6 Flash · 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 62,721 bytes · SHA-256 60f158205b30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>WORMAGE: Tactical Artillery Mayhem</title>
<style>
:root {
--bg-dark: #0f172a;
--panel-bg: rgba(15, 23, 42, 0.85);
--panel-border: rgba(255, 255, 255, 0.15);
--accent-gold: #fbbf24;
--accent-red: #ef4444;
--accent-blue: #3b82f6;
--accent-green: #10b981;
--accent-purple: #8b5cf6;
--font-main: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
* {
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: #000;
font-family: var(--font-main);
color: #fff;
}
#game-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(circle at center, #1e293b 0%, #090d16 100%);
}
canvas#gameCanvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
background: transparent;
}
/* HUD Overlay */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 16px;
z-index: 10;
}
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.team-bars {
display: flex;
flex-direction: column;
gap: 8px;
background: var(--panel-bg);
padding: 12px 16px;
border-radius: 12px;
border: 1px solid var(--panel-border);
backdrop-filter: blur(8px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
min-width: 220px;
}
.team-status-row {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-weight: 700;
}
.team-color-badge {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 8px currentColor;
}
.team-health-bar-bg {
flex: 1;
height: 10px;
background: rgba(255,255,255,0.1);
border-radius: 5px;
overflow: hidden;
position: relative;
}
.team-health-bar-fill {
height: 100%;
width: 100%;
transition: width 0.3s ease;
border-radius: 5px;
}
.timer-box {
background: var(--panel-bg);
border: 2px solid var(--panel-border);
border-radius: 16px;
padding: 10px 24px;
text-align: center;
backdrop-filter: blur(8px);
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.timer-box.urgent {
border-color: var(--accent-red);
animation: timerPulse 0.5s infinite alternate;
}
@keyframes timerPulse {
from { transform: scale(1); box-shadow: 0 0 15px rgba(239, 68, 68, 0.4); }
to { transform: scale(1.08); box-shadow: 0 0 30px rgba(239, 68, 68, 0.8); }
}
.timer-val {
font-size: 36px;
font-weight: 900;
line-height: 1;
font-family: monospace;
text-shadow: 0 0 10px rgba(255,255,255,0.3);
}
.timer-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.5px;
opacity: 0.7;
margin-top: 2px;
}
.wind-box {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 12px;
padding: 10px 16px;
backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
align-items: center;
min-width: 180px;
}
.wind-title {
font-size: 11px;
letter-spacing: 1px;
text-transform: uppercase;
opacity: 0.8;
margin-bottom: 6px;
}
.wind-gauge {
position: relative;
width: 140px;
height: 12px;
background: rgba(0,0,0,0.4);
border-radius: 6px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.1);
}
.wind-center-mark {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: rgba(255,255,255,0.5);
z-index: 2;
}
.wind-bar {
position: absolute;
top: 0;
bottom: 0;
background: linear-gradient(90deg, #38bdf8, #818cf8);
transition: all 0.3s ease;
}
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
}
.weapon-indicator {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 16px;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 16px;
pointer-events: auto;
cursor: pointer;
backdrop-filter: blur(8px);
transition: transform 0.2s ease, background 0.2s ease;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.weapon-indicator:hover {
transform: translateY(-2px);
background: rgba(30, 41, 59, 0.95);
}
.weapon-icon-box {
width: 48px;
height: 48px;
border-radius: 10px;
background: rgba(255,255,255,0.08);
display: flex;
justify-content: center;
align-items: center;
font-size: 26px;
border: 1px solid rgba(255,255,255,0.15);
}
.weapon-details {
display: flex;
flex-direction: column;
}
.weapon-name {
font-size: 16px;
font-weight: 800;
color: var(--accent-gold);
}
.weapon-ammo {
font-size: 12px;
opacity: 0.7;
}
.controls-hint {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 12px;
padding: 10px 16px;
font-size: 11px;
line-height: 1.5;
opacity: 0.85;
backdrop-filter: blur(8px);
}
/* Weapon Selector Menu Modal */
#weapon-menu {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
background: rgba(15, 23, 42, 0.96);
border: 2px solid var(--panel-border);
border-radius: 20px;
padding: 24px;
z-index: 100;
width: 90%;
max-width: 680px;
backdrop-filter: blur(16px);
box-shadow: 0 20px 50px rgba(0,0,0,0.8);
display: none;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: auto;
}
#weapon-menu.active {
display: block;
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.menu-header {
font-size: 20px;
font-weight: 800;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--accent-gold);
display: flex;
justify-content: space-between;
align-items: center;
}
.weapon-category-title {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: #94a3b8;
margin: 12px 0 8px 0;
}
.weapon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.weapon-btn {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 10px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
color: #fff;
}
.weapon-btn:hover {
background: rgba(255,255,255,0.15);
border-color: var(--accent-gold);
transform: translateY(-2px);
}
.weapon-btn.selected {
border-color: var(--accent-gold);
background: rgba(251, 191, 36, 0.15);
box-shadow: 0 0 12px rgba(251, 191, 36, 0.3);
}
.weapon-btn-icon {
font-size: 22px;
}
.weapon-btn-info {
display: flex;
flex-direction: column;
}
.weapon-btn-title {
font-size: 13px;
font-weight: 700;
}
.weapon-btn-count {
font-size: 10px;
opacity: 0.6;
}
/* Banner overlay for Turn announcements / Notifications */
#announcer-banner {
position: absolute;
top: 25%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
font-size: 42px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 20px rgba(0,0,0,0.8), 0 4px 10px rgba(0,0,0,0.9);
pointer-events: none;
opacity: 0;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 50;
white-space: nowrap;
padding: 12px 36px;
border-radius: 20px;
background: rgba(15,23,42,0.85);
border: 2px solid var(--accent-gold);
backdrop-filter: blur(12px);
}
#announcer-banner.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
/* Fullscreens / Modals (Setup & Game Over) */
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(10, 15, 26, 0.92);
backdrop-filter: blur(16px);
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal-card {
background: #1e293b;
border: 2px solid var(--panel-border);
border-radius: 24px;
padding: 36px;
width: 100%;
max-width: 580px;
box-shadow: 0 25px 60px rgba(0,0,0,0.8);
display: flex;
flex-direction: column;
gap: 20px;
}
.modal-title {
font-size: 32px;
font-weight: 900;
text-align: center;
color: var(--accent-gold);
letter-spacing: 1px;
text-shadow: 0 0 15px rgba(251, 191, 36, 0.3);
}
.modal-subtitle {
font-size: 14px;
color: #94a3b8;
text-align: center;
margin-top: -12px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-label {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: #cbd5e1;
}
.option-selector {
display: flex;
gap: 10px;
}
.opt-btn {
flex: 1;
padding: 12px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
color: #fff;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.opt-btn:hover {
background: rgba(255,255,255,0.15);
}
.opt-btn.selected {
background: var(--accent-blue);
border-color: #60a5fa;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
}
.teams-setup-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.team-setup-row {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(0,0,0,0.2);
padding: 10px 14px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.05);
}
.team-setup-name {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
}
.btn-primary {
background: linear-gradient(135deg, #f59e0b, #d97706);
border: none;
border-radius: 14px;
padding: 16px;
font-size: 18px;
font-weight: 900;
color: #fff;
cursor: pointer;
box-shadow: 0 8px 20px rgba(217, 119, 6, 0.4);
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 10px;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(217, 119, 6, 0.6);
background: linear-gradient(135deg, #fbbf24, #f59e0b);
}
/* Audio enable banner overlay for web audio autoplay rules */
#audio-prompt {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
border: 1px solid var(--accent-gold);
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
pointer-events: none;
z-index: 300;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<!-- HUD -->
<div id="hud">
<div class="hud-top">
<div class="team-bars" id="team-bars-container">
<!-- Team bars injected dynamically -->
</div>
<div class="timer-box" id="timer-box">
<div class="timer-val" id="timer-text">45</div>
<div class="timer-label">Turn Time</div>
</div>
<div class="wind-box">
<div class="wind-title">Wind Vector</div>
<div class="wind-gauge">
<div class="wind-center-mark"></div>
<div class="wind-bar" id="wind-bar"></div>
</div>
</div>
</div>
<div class="hud-bottom">
<div class="weapon-indicator" id="weapon-indicator-btn" onclick="toggleWeaponMenu()">
<div class="weapon-icon-box" id="hud-weapon-icon">🚀</div>
<div class="weapon-details">
<div class="weapon-name" id="hud-weapon-name">Bazooka</div>
<div class="weapon-ammo" id="hud-weapon-ammo">Ammo: Infinite</div>
</div>
</div>
<div class="controls-hint">
[A/D or ◄/►] Move | [W/S or ▲/▼] Aim | [SPACE] Fire (Hold Power) <br>
[E or UP] Jump | [Q] Backflip | [TAB] Weapons | [R] Rope/Retract
</div>
</div>
</div>
<!-- Weapon Selection Grid -->
<div id="weapon-menu">
<div class="menu-header">
<span>Arsenal Selection</span>
<span style="font-size: 14px; color: #94a3b8; cursor: pointer;" onclick="toggleWeaponMenu()">✕ Close</span>
</div>
<div id="weapon-grid-container">
<!-- Weapon categories & buttons injected dynamically -->
</div>
</div>
<!-- Announcement Banner -->
<div id="announcer-banner">TEAM RED'S TURN</div>
<!-- Lobby / Game Setup Modal -->
<div class="modal-overlay" id="setup-modal">
<div class="modal-card">
<div class="modal-title">💣 WORMAGE 💣</div>
<div class="modal-subtitle">Tactical Artillery & Terrain Destruction</div>
<div class="form-group">
<div class="form-label">Map Terrain Shape</div>
<div class="option-selector" id="terrain-selector">
<div class="opt-btn selected" data-val="island">Single Island</div>
<div class="opt-btn" data-val="two_islands">Two Islands</div>
<div class="opt-btn" data-val="cave">Deep Cavern</div>
</div>
</div>
<div class="form-group">
<div class="form-label">Teams Configuration</div>
<div class="teams-setup-list" id="teams-setup-list">
<!-- Dynamically populated -->
</div>
</div>
<button class="btn-primary" onclick="startGameFromLobby()">Commence Warfare!</button>
</div>
</div>
<!-- Game Over Modal -->
<div class="modal-overlay" id="gameover-modal" style="display: none;">
<div class="modal-card">
<div class="modal-title" id="winner-title">VICTORY!</div>
<div class="modal-subtitle" id="winner-subtitle">Team Alpha dominated the battlefield</div>
<div class="form-group" id="match-stats" style="line-height: 1.8; font-size: 14px; color: #cbd5e1;">
<!-- Stats summary -->
</div>
<button class="btn-primary" onclick="returnToLobby()">Back to Menu</button>
</div>
</div>
</div>
<script>
/**
* WORMAGE - Complete Artillery Engine
* Includes: WebAudio Synth, Procedural Terrain, Pixel Destruction, Physics with Gravity/Wind/Water,
* Custom Debris Chunks, Predictive Trajectory Lines, Risk-Assessment AI, Dynamic Camera with Zoom,
* Particle Systems, and Full Weapon Arsenal.
*/
// Global Audio Engine (Web Audio API Synthesizer)
class SoundEngine {
constructor() {
this.ctx = null;
this.enabled = false;
}
init() {
if (this.ctx) return;
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (AudioCtx) {
this.ctx = new AudioCtx();
this.enabled = true;
}
}
playTone(freq, type, duration, startVol = 0.3, endVol = 0) {
if (!this.enabled || !this.ctx) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(startVol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, endVol), this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
} catch (e) {}
}
playExplosion(size = 1.0) {
if (!this.enabled || !this.ctx) return;
try {
const bufferSize = this.ctx.sampleRate * (0.3 * size);
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(400, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + (0.3 * size));
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.6 * size, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + (0.3 * size));
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
// Low boom oscillator
this.playTone(80, 'sine', 0.4 * size, 0.5 * size, 0.01);
} catch (e) {}
}
playJump() {
if (!this.enabled || !this.ctx) return;
try {
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.linearRampToValueAtTime(0, this.ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.15);
} catch(e){}
}
playLaunch() {
if (!this.enabled || !this.ctx) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(300, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, this.ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.2);
} catch(e){}
}
playSplash() {
if (!this.enabled || !this.ctx) return;
try {
const bufferSize = this.ctx.sampleRate * 0.25;
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(1000, 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 + 0.25);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
} catch(e){}
}
playClick() {
this.playTone(800, 'sine', 0.04, 0.1, 0.01);
}
playTimerTick(urgent = false) {
this.playTone(urgent ? 900 : 600, 'square', 0.05, urgent ? 0.25 : 0.1, 0.01);
}
playHoly() {
if (!this.enabled || !this.ctx) return;
[440, 554.37, 659.25, 880].forEach(f => {
this.playTone(f, 'sine', 1.2, 0.25, 0.01);
});
}
}
const sound = new SoundEngine();
document.addEventListener('pointerdown', () => sound.init(), { once: true });
// Game Constants & Configuration
const WORLD_WIDTH = 1600;
const WORLD_HEIGHT = 900;
const GRAVITY = 380; // px/s^2
const WATER_LEVEL = 820;
const TEAMS_DEF = [
{ id: 0, name: 'Red Rockets', color: '#ef4444', accent: '#fca5a5' },
{ id: 1, name: 'Blue Bombers', color: '#3b82f6', accent: '#93c5fd' },
{ id: 2, name: 'Green Grenadiers', color: '#10b981', accent: '#6ee7b7' },
{ id: 3, name: 'Yellow Yahoos', color: '#f59e0b', accent: '#fde047' }
];
const WEAPONS = [
// Indirect
{ id: 'bazooka', name: 'Bazooka', icon: '🚀', category: 'Indirect', ammo: -1, damage: 45, radius: 42, speed: 650, windAffect: 0.8, fuse: 0 },
{ id: 'grenade', name: 'Grenade', icon: '💣', category: 'Indirect', ammo: -1, damage: 50, radius: 45, speed: 550, windAffect: 0.2, fuse: 3.5, bounce: true },
{ id: 'cluster', name: 'Cluster Bomb', icon: '💥', category: 'Indirect', ammo: 3, damage: 30, radius: 35, speed: 520, windAffect: 0.3, fuse: 3, clusters: 5 },
{ id: 'mortar', name: 'Mortar', icon: '🔻', category: 'Indirect', ammo: 2, damage: 55, radius: 50, speed: 700, windAffect: 0.9, fuse: 0 },
{ id: 'holy', name: 'Holy Grenade', icon: '✨', category: 'Indirect', ammo: 1, damage: 90, radius: 85, speed: 480, windAffect: 0.1, fuse: 4.0, holy: true },
// Direct
{ id: 'shotgun', name: 'Shotgun', icon: '🔫', category: 'Direct', ammo: -1, damage: 25, radius: 18, speed: 1200, windAffect: 0, direct: true, shots: 2 },
{ id: 'sniper', name: 'Sniper Rifle', icon: '🎯', category: 'Direct', ammo: 2, damage: 60, radius: 12, speed: 2000, windAffect: 0, direct: true },
{ id: 'firepunch', name: 'Fire Punch', icon: '🥊', category: 'Direct', ammo: -1, damage: 40, radius: 25, melee: true },
// High-AoE / Map-altering
{ id: 'airstrike', name: 'Air Strike', icon: '✈️', category: 'AoE / Special', ammo: 1, damage: 40, radius: 40, strike: true },
{ id: 'dynamite', name: 'Dynamite', icon: '🧨', category: 'AoE / Special', ammo: 2, damage: 75, radius: 70, drop: true, fuse: 5 },
{ id: 'drill', name: 'Drill Bomb', icon: '🌀', category: 'AoE / Special', ammo: 2, damage: 50, radius: 45, speed: 550, drill: true },
// Utilities
{ id: 'rope', name: 'Ninja Rope', icon: '🪢', category: 'Utility', ammo: -1, utility: 'rope' },
{ id: 'jetpack', name: 'Jetpack', icon: '🎒', category: 'Utility', ammo: 2, utility: 'jetpack' },
{ id: 'teleport', name: 'Teleporter', icon: '🌀', category: 'Utility', ammo: 1, utility: 'teleport' },
{ id: 'girder', name: 'Girder Bridge', icon: '🧱', category: 'Utility', ammo: 2, utility: 'girder' },
{ id: 'blowtorch', name: 'Blowtorch', icon: '🔥', category: 'Utility', ammo: 3, utility: 'torch' }
];
// Canvas & Terrain Setup
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = WORLD_WIDTH;
canvas.height = WORLD_HEIGHT;
const terrainCanvas = document.createElement('canvas');
terrainCanvas.width = WORLD_WIDTH;
terrainCanvas.height = WORLD_HEIGHT;
const terrainCtx = terrainCanvas.getContext('2d');
let terrainImageData = null;
// Game State
let gameState = 'LOBBY'; // LOBBY, PLAYING, GAMEOVER
let selectedTerrainType = 'island';
let teams = [];
let activeTeamIdx = 0;
let activeWormIdx = 0;
let turnTimer = 45;
let turnTimerInterval = null;
let currentWind = 0; // -100 to 100
let projectiles = [];
let particles = [];
let debrisChunks = [];
let combatTexts = [];
let camera = { x: WORLD_WIDTH / 2, y: WORLD_HEIGHT / 2, zoom: 1, targetX: WORLD_WIDTH / 2, targetY: WORLD_HEIGHT / 2, shake: 0 };
let impactFreezeFrames = 0;
let selectedWeapon = WEAPONS[0];
let isCharging = false;
let chargePower = 0;
let aimAngle = -Math.PI / 4; // -45 deg
let matchStats = { totalDamage: {}, totalKills: {} };
// Controls Input Tracker
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'KeyTab') {
e.preventDefault();
toggleWeaponMenu();
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
// Mouse interaction for aiming / targeting
let mousePos = { x: 0, y: 0, worldX: 0, worldY: 0 };
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
mousePos.x = (e.clientX - rect.left) * scaleX;
mousePos.y = (e.clientY - rect.top) * scaleY;
// Convert to world space considering camera
mousePos.worldX = (mousePos.x - canvas.width/2) / camera.zoom + camera.x;
mousePos.worldY = (mousePos.y - canvas.height/2) / camera.zoom + camera.y;
const currentWorm = getActiveWorm();
if (currentWorm && gameState === 'PLAYING' && !currentWorm.isCPU) {
const dx = mousePos.worldX - currentWorm.x;
const dy = mousePos.worldY - (currentWorm.y - 12);
aimAngle = Math.atan2(dy, dx);
currentWorm.facing = dx >= 0 ? 1 : -1;
}
});
canvas.addEventListener('mousedown', e => {
sound.init();
const currentWorm = getActiveWorm();
if (!currentWorm || gameState !== 'PLAYING' || currentWorm.isCPU || turnTimer <= 0) return;
if (selectedWeapon.utility === 'teleport') {
// Perform Teleport
currentWorm.x = mousePos.worldX;
currentWorm.y = mousePos.worldY;
currentWorm.vx = 0;
currentWorm.vy = 0;
spawnParticles(currentWorm.x, currentWorm.y, '#8b5cf6', 20);
sound.playTone(600, 'sine', 0.2, 0.4, 0);
consumeWeaponAmmo();
endTurnDelayed(1.5);
} else if (selectedWeapon.utility === 'girder') {
// Build Girder
placeGirder(mousePos.worldX, mousePos.worldY);
consumeWeaponAmmo();
} else if (selectedWeapon.strike) {
// Air strike target
triggerAirStrike(mousePos.worldX);
consumeWeaponAmmo();
endTurnDelayed(3.0);
} else if (!selectedWeapon.utility && !selectedWeapon.melee) {
isCharging = true;
chargePower = 0;
}
});
canvas.addEventListener('mouseup', e => {
if (isCharging) {
fireWeapon(chargePower);
isCharging = false;
chargePower = 0;
}
});
// UI Setup Events
document.querySelectorAll('#terrain-selector .opt-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#terrain-selector .opt-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
selectedTerrainType = btn.getAttribute('data-val');
sound.playClick();
});
});
function initLobbyUI() {
const container = document.getElementById('teams-setup-list');
container.innerHTML = '';
TEAMS_DEF.forEach((t, idx) => {
const row = document.createElement('div');
row.className = 'team-setup-row';
row.innerHTML = `
<div class="team-setup-name">
<span class="team-color-badge" style="background: ${t.color}"></span>
<span>${t.name}</span>
</div>
<div class="option-selector">
<button class="opt-btn ${idx < 2 ? 'selected' : ''}" onclick="toggleTeamActive(${idx}, 'human', this)">Human</button>
<button class="opt-btn" onclick="toggleTeamActive(${idx}, 'cpu', this)">CPU</button>
<button class="opt-btn ${idx >= 2 ? 'selected' : ''}" onclick="toggleTeamActive(${idx}, 'off', this)">Off</button>
</div>
`;
container.appendChild(row);
});
}
const teamConfig = [
{ type: 'human' },
{ type: 'human' },
{ type: 'off' },
{ type: 'off' }
];
function toggleTeamActive(idx, type, btn) {
sound.playClick();
teamConfig[idx].type = type;
const parent = btn.parentElement;
parent.querySelectorAll('.opt-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
}
initLobbyUI();
// Terrain Procedural Generation System
function generateTerrain(shape) {
terrainCtx.clearRect(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
terrainCtx.fillStyle = '#1e3a8a';
terrainCtx.beginPath();
if (shape === 'island') {
// Single organic island in middle
terrainCtx.moveTo(150, WORLD_HEIGHT);
for (let x = 150; x <= WORLD_WIDTH - 150; x += 10) {
let norm = (x - 150) / (WORLD_WIDTH - 300);
let heightVal = Math.sin(norm * Math.PI) * 420;
heightVal += Math.sin(x * 0.02) * 45 + Math.cos(x * 0.05) * 20;
let y = WORLD_HEIGHT - 120 - heightVal;
terrainCtx.lineTo(x, y);
}
terrainCtx.lineTo(WORLD_WIDTH - 150, WORLD_HEIGHT);
} else if (shape === 'two_islands') {
// Two islands split by water channel
terrainCtx.moveTo(100, WORLD_HEIGHT);
for (let x = 100; x <= 650; x += 10) {
let norm = (x - 100) / 550;
let y = WORLD_HEIGHT - 120 - Math.sin(norm * Math.PI) * 380 + Math.sin(x * 0.03) * 30;
terrainCtx.lineTo(x, y);
}
terrainCtx.lineTo(650, WORLD_HEIGHT);
terrainCtx.moveTo(950, WORLD_HEIGHT);
for (let x = 950; x <= WORLD_WIDTH - 100; x += 10) {
let norm = (x - 950) / 550;
let y = WORLD_HEIGHT - 120 - Math.sin(norm * Math.PI) * 380 + Math.cos(x * 0.03) * 30;
terrainCtx.lineTo(x, y);
}
terrainCtx.lineTo(WORLD_WIDTH - 100, WORLD_HEIGHT);
} else if (shape === 'cave') {
// Deep Cavern with ceiling and ground
// Floor
terrainCtx.moveTo(50, WORLD_HEIGHT);
for (let x = 50; x <= WORLD_WIDTH - 50; x += 10) {
let y = WORLD_HEIGHT - 150 - Math.sin(x * 0.008) * 120 + Math.cos(x * 0.025) * 40;
terrainCtx.lineTo(x, y);
}
terrainCtx.lineTo(WORLD_WIDTH - 50, WORLD_HEIGHT);
// Ceiling
terrainCtx.moveTo(50, 0);
for (let x = 50; x <= WORLD_WIDTH - 50; x += 10) {
let y = 160 + Math.sin(x * 0.01) * 100 + Math.cos(x * 0.03) * 35;
terrainCtx.lineTo(x, y);
}
terrainCtx.lineTo(WORLD_WIDTH - 50, 0);
}
terrainCtx.closePath();
// Create gradient styling for terrain
const grad = terrainCtx.createLinearGradient(0, 0, 0, WORLD_HEIGHT);
grad.addColorStop(0, '#22c55e');
grad.addColorStop(0.15, '#15803d');
grad.addColorStop(0.3, '#78350f');
grad.addColorStop(1, '#451a03');
terrainCtx.fillStyle = grad;
terrainCtx.fill();
// Add dirt texture details / stones
terrainCtx.fillStyle = 'rgba(0,0,0,0.15)';
for (let i = 0; i < 300; i++) {
let rx = Math.random() * WORLD_WIDTH;
let ry = Math.random() * WORLD_HEIGHT;
terrainCtx.beginPath();
terrainCtx.arc(rx, ry, Math.random() * 8 + 2, 0, Math.PI * 2);
terrainCtx.fill();
}
// Cache pixel data
terrainImageData = terrainCtx.getImageData(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
}
function isTerrainSolid(x, y) {
x = Math.floor(x);
y = Math.floor(y);
if (x < 0 || x >= WORLD_WIDTH || y < 0 || y >= WORLD_HEIGHT) return false;
const idx = (y * WORLD_WIDTH + x) * 4;
return terrainImageData.data[idx + 3] > 128;
}
function carveTerrain(cx, cy, radius) {
terrainCtx.save();
terrainCtx.globalCompositeOperation = 'destination-out';
terrainCtx.beginPath();
terrainCtx.arc(cx, cy, radius, 0, Math.PI * 2);
terrainCtx.fill();
terrainCtx.restore();
// Update cached imagedata
terrainImageData = terrainCtx.getImageData(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
// Spawn visual debris particles & falling debris chunks!
const chunkCount = Math.floor(radius / 4);
for (let i = 0; i < chunkCount; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * radius * 0.8;
debrisChunks.push({
x: cx + Math.cos(angle) * dist,
y: cy + Math.sin(angle) * dist,
vx: (Math.random() - 0.5) * 300,
vy: (Math.random() - 0.8) * 350,
radius: Math.random() * 4 + 3,
color: '#78350f'
});
}
}
function placeGirder(gx, gy) {
terrainCtx.save();
terrainCtx.fillStyle = '#94a3b8';
terrainCtx.strokeStyle = '#475569';
terrainCtx.lineWidth = 3;
terrainCtx.beginPath();
terrainCtx.rect(gx - 60, gy - 10, 120, 20);
terrainCtx.fill();
terrainCtx.stroke();
terrainCtx.restore();
terrainImageData = terrainCtx.getImageData(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
spawnParticles(gx, gy, '#cbd5e1', 15);
sound.playClick();
}
// Unit / Worm Class
class Worm {
constructor(teamId, id, name, x, y) {
this.teamId = teamId;
this.id = id;
this.name = name;
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.radius = 12;
this.hp = 100;
this.maxHp = 100;
this.facing = 1; // 1 = right, -1 = left
this.isGrounded = false;
this.isDead = false;
this.isDrowning = false;
this.parachute = false;
this.jetpackFuel = 0;
this.rope = null; // { anchorX, anchorY, length }
this.isCPU = false;
}
update(dt) {
if (this.isDead) return;
// Water Drowning Check
if (this.y + this.radius >= WATER_LEVEL) {
if (!this.isDrowning) {
this.isDrowning = true;
sound.playSplash();
spawnParticles(this.x, WATER_LEVEL, '#38bdf8', 25);
addCombatText(this.x, this.y - 20, 'DROWNING!', '#38bdf8');
}
this.hp -= dt * 40;
this.vy = 40; // drag slow sink
if (this.hp <= 0) {
this.die();
}
return;
}
// Jetpack handling
if (this.jetpackFuel > 0 && keys['KeyW']) {
this.vy -= 800 * dt;
this.jetpackFuel -= dt * 25;
spawnParticles(this.x, this.y + 10, '#f97316', 2);
}
// Apply Physics
this.vy += GRAVITY * dt;
// Apply Wind if in air or using parachute
if (!this.isGrounded) {
this.vx += currentWind * 2.5 * dt;
if (this.parachute) {
this.vy = Math.min(this.vy, 60);
}
}
// Ninja Rope swing physics
if (this.rope) {
const dx = this.x - this.rope.anchorX;
const dy = this.y - this.rope.anchorY;
const dist = Math.hypot(dx, dy);
if (dist > this.rope.length) {
const angle = Math.atan2(dy, dx);
this.x = this.rope.anchorX + Math.cos(angle) * this.rope.length;
this.y = this.rope.anchorY + Math.sin(angle) * this.rope.length;
// Dampen radial velocity
const dot = this.vx * Math.cos(angle) + this.vy * Math.sin(angle);
this.vx -= dot * Math.cos(angle);
this.vy -= dot * Math.sin(angle);
}
}
// Horizontal Movement
let nextX = this.x + this.vx * dt;
let nextY = this.y + this.vy * dt;
// Collision Detection against Terrain
if (isTerrainSolid(nextX, nextY + this.radius)) {
// Check fall damage
if (this.vy > 400 && !this.parachute) {
const fallDmg = Math.floor((this.vy - 380) * 0.15);
if (fallDmg > 5) {
this.takeDamage(fallDmg);
sound.playExplosion(0.3);
}
}
// Step up slope check
let stepUp = 0;
while (stepUp < 12 && isTerrainSolid(nextX, nextY + this.radius - stepUp)) {
stepUp++;
}
if (stepUp < 12) {
nextY -= stepUp;
this.vy = 0;
this.isGrounded = true;
this.parachute = false;
} else {
this.vx = -this.vx * 0.3; // bounce off wall
nextX = this.x;
}
} else {
this.isGrounded = false;
}
this.x = nextX;
this.y = nextY;
// Friction
if (this.isGrounded) {
this.vx *= 0.8;
} else {
this.vx *= 0.98;
}
}
jump(powerY = -220, powerX = 0) {
if (this.isGrounded) {
this.vy = powerY;
this.vx = powerX;
this.isGrounded = false;
sound.playJump();
}
}
takeDamage(amount) {
if (this.isDead) return;
this.hp -= amount;
addCombatText(this.x, this.y - 25, `-${Math.floor(amount)}`, '#ef4444');
camera.shake = Math.max(camera.shake, amount * 0.2);
if (this.hp <= 0) {
this.die();
}
}
die() {
this.hp = 0;
this.isDead = true;
spawnParticles(this.x, this.y, '#ef4444', 30);
addCombatText(this.x, this.y - 30, 'RIP', '#94a3b8');
}
draw(ctx) {
if (this.isDead) {
// Tombstone graphic
ctx.fillStyle = '#64748b';
ctx.fillRect(this.x - 8, this.y - 14, 16, 18);
ctx.fillStyle = '#cbd5e1';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('RIP', this.x, this.y - 4);
return;
}
const team = TEAMS_DEF[this.teamId];
// Rope Line
if (this.rope) {
ctx.strokeStyle = '#d97706';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x, this.y - 8);
ctx.lineTo(this.rope.anchorX, this.rope.anchorY);
ctx.stroke();
}
// Parachute Graphic
if (this.parachute) {
ctx.fillStyle = team.color;
ctx.beginPath();
ctx.arc(this.x, this.y - 28, 22, Math.PI, 0);
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(this.x - 20, this.y - 28);
ctx.lineTo(this.x, this.y - 8);
ctx.moveTo(this.x + 20, this.y - 28);
ctx.lineTo(this.x, this.y - 8);
ctx.stroke();
}
// Worm Body (cute pill/capsule)
ctx.save();
ctx.translate(this.x, this.y - 10);
// Body base
ctx.fillStyle = '#f472b6'; // Cute pink worm skin
ctx.beginPath();
ctx.ellipse(0, 0, 10, 12, 0, 0, Math.PI * 2);
ctx.fill();
// Head Bandana / Helmet with team color
ctx.fillStyle = team.color;
ctx.fillRect(-10, -10, 20, 6);
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(this.facing * 4, -3, 3.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(this.facing * 5 + Math.cos(aimAngle)*1.5, -3 + Math.sin(aimAngle)*1.5, 1.5, 0, Math.PI * 2);
ctx.fill();
// Active Worm Indicator Arrow
if (this === getActiveWorm() && gameState === 'PLAYING') {
ctx.fillStyle = '#fbbf24';
ctx.beginPath();
ctx.moveTo(0, -26);
ctx.lineTo(-6, -36);
ctx.lineTo(6, -36);
ctx.closePath();
ctx.fill();
}
ctx.restore();
// Aim Indicator Line & Charge Bar
if (this === getActiveWorm() && gameState === 'PLAYING') {
ctx.strokeStyle = 'rgba(251, 191, 36, 0.8)';
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(this.x, this.y - 10);
ctx.lineTo(this.x + Math.cos(aimAngle) * 40, this.y - 10 + Math.sin(aimAngle) * 40);
ctx.stroke();
ctx.setLineDash([]);
// Draw Predictive Arc Trajectory Line
drawTrajectoryPreview(this.x, this.y - 10, aimAngle, isCharging ? chargePower : 500);
// Power Bar if charging
if (isCharging) {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(this.x - 20, this.y + 12, 40, 6);
ctx.fillStyle = '#ef4444';
ctx.fillRect(this.x - 20, this.y + 12, (chargePower / 800) * 40, 6);
}
}
// Name & Health Bar
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(this.name, this.x, this.y - 24);
// HP Bar
const hpPct = Math.max(0, this.hp / this.maxHp);
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(this.x - 15, this.y - 20, 30, 4);
ctx.fillStyle = hpPct > 0.5 ? '#10b981' : hpPct > 0.25 ? '#f59e0b' : '#ef4444';
ctx.fillRect(this.x - 15, this.y - 20, 30 * hpPct, 4);
}
}
// Draw Predictive Trajectory Preview Dot Line
function drawTrajectoryPreview(startX, startY, angle, speed) {
if (selectedWeapon.utility || selectedWeapon.strike || selectedWeapon.melee) return;
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
let px = startX;
let py = startY;
let vx = Math.cos(angle) * speed;
let vy = Math.sin(angle) * speed;
const dt = 0.04;
const windFactor = (selectedWeapon.windAffect || 0) * currentWind * 2.5;
for (let i = 0; i < 25; i++) {
vx += windFactor * dt;
vy += GRAVITY * dt;
px += vx * dt;
py += vy * dt;
if (isTerrainSolid(px, py) || py >= WATER_LEVEL) break;
ctx.beginPath();
ctx.arc(px, py, 2, 0, Math.PI * 2);
ctx.fill();
}
}
// Projectile System
class Projectile {
constructor(type, x, y, vx, vy, config = {}) {
this.type = type; // weapon id
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = config.radius || 4;
this.damage = config.damage || 40;
this.blastRadius = config.blastRadius || 45;
this.fuse = config.fuse || 0;
this.timer = 0;
this.windAffect = config.windAffect || 0;
this.bounce = config.bounce || false;
this.clusters = config.clusters || 0;
this.holy = config.holy || false;
this.drill = config.drill || false;
this.isDead = false;
}
update(dt) {
if (this.isDead) return;
this.timer += dt;
// Wind affect
this.vx += currentWind * this.windAffect * 2.5 * dt;
if (!this.drill) {
this.vy += GRAVITY * dt;
}
let nextX = this.x + this.vx * dt;
let nextY = this.y + this.vy * dt;
// Trail particles
if (Math.random() < 0.6) {
particles.push({
x: this.x,
y: this.y,
vx: (Math.random() - 0.5) * 20,
vy: (Math.random() - 0.5) * 20,
radius: Math.random() * 3 + 1,
color: 'rgba(200,200,200,0.6)',
life: 0.3,
maxLife: 0.3
});
}
// Fuse detonation check
if (this.fuse > 0 && this.timer >= this.fuse) {
if (this.holy) sound.playHoly();
this.explode();
return;
}
// Terrain or Water Collision
if (nextY >= WATER_LEVEL) {
sound.playSplash();
spawnParticles(this.x, WATER_LEVEL, '#38bdf8', 15);
this.isDead = true;
return;
}
if (isTerrainSolid(nextX, nextY)) {
if (this.drill) {
// Drill burrows through terrain
carveTerrain(nextX, nextY, 14);
if (this.timer >= 2.0) this.explode();
} else if (this.bounce) {
this.vx *= -0.6;
this.vy *= -0.6;
sound.playTone(300, 'sine', 0.05, 0.2, 0);
} else {
this.explode();
}
return;
}
// Direct Hit on Worm check
for (let team of teams) {
for (let worm of team.worms) {
if (!worm.isDead && Math.hypot(nextX - worm.x, nextY - worm.y) < worm.radius + this.radius) {
this.explode();
return;
}
}
}
this.x = nextX;
this.y = nextY;
}
explode() {
this.isDead = true;
carveTerrain(this.x, this.y, this.blastRadius);
sound.playExplosion(this.blastRadius / 45);
impactFreezeFrames = 3; // Punchy frame-freeze
// Particles & Screen Shake
spawnParticles(this.x, this.y, '#f97316', 35);
camera.shake = Math.max(camera.shake, this.blastRadius * 0.4);
// Cluster submunitions spawn
if (this.clusters > 0) {
for (let i = 0; i < this.clusters; i++) {
let angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.5;
let spd = Math.random() * 250 + 150;
projectiles.push(new Projectile('cluster_sub', this.x, this.y, Math.cos(angle)*spd, Math.sin(angle)*spd, {
damage: 20, blastRadius: 22, fuse: 1.5
}));
}
}
// Apply Damage & Impulse to Worms in radius
teams.forEach(team => {
team.worms.forEach(worm => {
if (worm.isDead) return;
let dist = Math.hypot(this.x - worm.x, this.y - worm.y);
if (dist < this.blastRadius + 20) {
let force = (1 - dist / (this.blastRadius + 20));
let dmg = Math.floor(this.damage * force);
let angle = Math.atan2(worm.y - this.y, worm.x - this.x);
worm.vx += Math.cos(angle) * force * 450;
worm.vy += Math.sin(angle) * force * 450 - 50;
worm.takeDamage(dmg);
}
});
});
}
draw(ctx) {
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
}
// Particle System
function spawnParticles(x, y, color, count = 15) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 200 + 50;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
radius: Math.random() * 4 + 2,
color: color,
life: Math.random() * 0.5 + 0.3,
maxLife: 0.8
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += GRAVITY * 0.3 * dt;
if (p.life <= 0) particles.splice(i, 1);
}
}
function drawParticles(ctx) {
particles.forEach(p => {
ctx.fillStyle = p.color;
ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1.0;
}
// Floating Combat Text
function addCombatText(x, y, text, color = '#fff') {
combatTexts.push({ x, y, text, color, life: 1.2, vy: -40 });
}
function updateCombatTexts(dt) {
for (let i = combatTexts.length - 1; i >= 0; i--) {
let ct = combatTexts[i];
ct.life -= dt;
ct.y += ct.vy * dt;
if (ct.life <= 0) combatTexts.splice(i, 1);
}
}
function drawCombatTexts(ctx) {
combatTexts.forEach(ct => {
ctx.fillStyle = ct.color;
ctx.font = '900 16px sans-serif';
ctx.textAlign = 'center';
ctx.globalAlpha = Math.max(0, ct.life);
ctx.fillText(ct.text, ct.x, ct.y);
});
ctx.globalAlpha = 1.0;
}
// Physical Debris Chunks System
function updateDebrisChunks(dt) {
for (let i = debrisChunks.length - 1; i >= 0; i--) {
let d = debrisChunks[i];
d.vy += GRAVITY * dt;
d.x += d.vx * dt;
d.y += d.vy * dt;
if (d.y >= WATER_LEVEL) {
debrisChunks.splice(i, 1);
continue;
}
if (isTerrainSolid(d.x, d.y)) {
debrisChunks.splice(i, 1); // Merge into ground
}
}
}
function drawDebrisChunks(ctx) {
debrisChunks.forEach(d => {
ctx.fillStyle = d.color;
ctx.beginPath();
ctx.arc(d.x, d.y, d.radius, 0, Math.PI * 2);
ctx.fill();
});
}
// Weapon Actions Execution
function fireWeapon(power) {
const currentWorm = getActiveWorm();
if (!currentWorm) return;
const vx = Math.cos(aimAngle) * power;
const vy = Math.sin(aimAngle) * power;
const spawnX = currentWorm.x + Math.cos(aimAngle) * 20;
const spawnY = currentWorm.y - 10 + Math.sin(aimAngle) * 20;
sound.playLaunch();
if (selectedWeapon.id === 'bazooka') {
projectiles.push(new Projectile('bazooka', spawnX, spawnY, vx, vy, {
damage: selectedWeapon.damage, blastRadius: selectedWeapon.radius, windAffect: selectedWeapon.windAffect
}));
} else if (selectedWeapon.id === 'grenade') {
projectiles.push(new Projectile('grenade', spawnX, spawnY, vx, vy, {
damage: selectedWeapon.damage, blastRadius: selectedWeapon.radius, fuse: selectedWeapon.fuse, bounce: true
}));
} else if (selectedWeapon.id === 'cluster') {
projectiles.push(new Projectile('cluster', spawnX, spawnY, vx, vy, {
damage: selectedWeapon.damage, blastRadius: selectedWeapon.radius, fuse: selectedWeapon.fuse, bounce: true, clusters: 5
}));
} else if (selectedWeapon.id === 'holy') {
projectiles.push(new Projectile('holy', spawnX, spawnY, vx, vy, {
damage: selectedWeapon.damage, blastRadius: selectedWeapon.radius, fuse: selectedWeapon.fuse, bounce: true, holy: true
}));
} else if (selectedWeapon.id === 'shotgun' || selectedWeapon.id === 'sniper') {
// Direct Raycast Shot
sound.playExplosion(0.5);
let rx = spawnX;
let ry = spawnY;
let rvx = Math.cos(aimAngle) * selectedWeapon.speed;
let rvy = Math.sin(aimAngle) * selectedWeapon.speed;
const dt = 0.02;
for (let step = 0; step < 100; step++) {
rx += rvx * dt;
ry += rvy * dt;
particles.push({ x: rx, y: ry, vx: 0, vy: 0, radius: 2, color: '#fef08a', life: 0.2, maxLife: 0.2 });
if (isTerrainSolid(rx, ry)) {
carveTerrain(rx, ry, selectedWeapon.radius);
break;
}
// Check worm hit
let hit = false;
teams.forEach(team => {
team.worms.forEach(worm => {
if (!worm.isDead && Math.hypot(rx - worm.x, ry - worm.y) < worm.radius + 6) {
worm.takeDamage(selectedWeapon.damage);
worm.vx += Math.cos(aimAngle) * 300;
worm.vy += Math.sin(aimAngle) * 300 - 50;
hit = true;
}
});
});
if (hit) break;
}
} else if (selectedWeapon.id === 'dynamite') {
projectiles.push(new Projectile('dynamite', spawnX, spawnY, vx * 0.2, vy * 0.2, {
damage: selectedWeapon.damage, blastRadius: selectedWeapon.radius, fuse: selectedWeapon.fuse, bounce: true
}));
} else if (selectedWeapon.id === 'drill') {
projectiles.push(new Projectile('drill', spawnX, spawnY, vx, vy, {
damage: selectedWeapon.damage, blastRadius: selectedWeapon.radius, drill: true, fuse: 3
}));
}
consumeWeaponAmmo();
endTurnDelayed(3.0);
}
function triggerAirStrike(targetX) {
sound.playLaunch();
addCombatText(targetX, 100, 'INCOMING AIR STRIKE!', '#ef4444');
for (let i = 0; i < 5; i++) {
setTimeout(() => {
let offsetX = (i - 2) * 35;
projectiles.push(new Projectile('airstrike', targetX + offsetX, 50, 0, 450, {
damage: 40, blastRadius: 40
}));
}, i * 200);
}
}
function consumeWeaponAmmo() {
if (selectedWeapon.ammo > 0) {
selectedWeapon.ammo--;
}
updateHUD();
}
// Turn Manager & Game Loop Control
function getActiveWorm() {
if (!teams[activeTeamIdx]) return null;
return teams[activeTeamIdx].worms[activeWormIdx];
}
function startNextTurn() {
// Advance team & worm
activeTeamIdx = (activeTeamIdx + 1) % teams.length;
let attempts = 0;
while (teams[activeTeamIdx].worms.every(w => w.isDead) && attempts < teams.length) {
activeTeamIdx = (activeTeamIdx + 1) % teams.length;
attempts++;
}
// Check Game Over
const aliveTeams = teams.filter(t => t.worms.some(w => !w.isDead));
if (aliveTeams.length <= 1) {
endGame(aliveTeams[0]);
return;
}
// Find next alive worm in team
const team = teams[activeTeamIdx];
do {
activeWormIdx = (activeWormIdx + 1) % team.worms.length;
} while (team.worms[activeWormIdx].isDead);
const activeWorm = getActiveWorm();
// Reset Turn Variables
turnTimer = 45;
currentWind = (Math.random() - 0.5) * 200; // New wind value
document.getElementById('wind-bar').style.width = `${Math.abs(currentWind) / 2}%`;
document.getElementById('wind-bar').style.left = currentWind >= 0 ? '50%' : `${50 - Math.abs(currentWind) / 2}%`;
// Focus Camera
camera.targetX = activeWorm.x;
camera.targetY = activeWorm.y;
// Banner Announcement
showBanner(`${team.name}'s Turn`);
// CPU Turn Execution
if (activeWorm.isCPU) {
executeCPUTurn(activeWorm);
}
updateHUD();
}
function endTurnDelayed(delaySec) {
setTimeout(() => {
// Wait until all projectiles settled
const checkInterval = setInterval(() => {
if (projectiles.length === 0 && debrisChunks.length === 0) {
clearInterval(checkInterval);
startNextTurn();
}
}, 200);
}, delaySec * 1000);
}
function showBanner(text) {
const banner = document.getElementById('announcer-banner');
banner.innerText = text;
banner.classList.add('show');
setTimeout(() => banner.classList.remove('show'), 2000);
}
// Risk-Assessment CPU AI Controller
function executeCPUTurn(cpuWorm) {
setTimeout(() => {
if (gameState !== 'PLAYING' || cpuWorm.isDead) return;
// 1. Find nearest enemy worm
let targetWorm = null;
let minDist = Infinity;
teams.forEach((t, idx) => {
if (idx !== activeTeamIdx) {
t.worms.forEach(w => {
if (!w.isDead) {
let d = Math.hypot(w.x - cpuWorm.x, w.y - cpuWorm.y);
if (d < minDist) {
minDist = d;
targetWorm = w;
}
}
});
}
});
if (!targetWorm) return;
// 2. Risk Assessment: Avoid falling in water or off cliffs
if (cpuWorm.x < 150 || cpuWorm.x > WORLD_WIDTH - 150) {
// Walk away from water edge
cpuWorm.vx = cpuWorm.x < 150 ? 80 : -80;
}
// 3. Aim Calculation with Wind & Gravity Iterative Simulation
const dx = targetWorm.x - cpuWorm.x;
const dy = targetWorm.y - cpuWorm.y;
let testAngle = Math.atan2(dy, dx);
let testPower = Math.min(750, Math.max(350, minDist * 0.9));
aimAngle = testAngle;
cpuWorm.facing = dx >= 0 ? 1 : -1;
// 4. Select Weapon
if (minDist < 120) {
selectedWeapon = WEAPONS.find(w => w.id === 'shotgun') || WEAPONS[0];
} else if (Math.random() < 0.3) {
selectedWeapon = WEAPONS.find(w => w.id === 'airstrike') || WEAPONS[0];
} else {
selectedWeapon = WEAPONS.find(w => w.id === 'bazooka') || WEAPONS[0];
}
updateHUD();
// 5. Fire after realistic delay
setTimeout(() => {
if (selectedWeapon.strike) {
triggerAirStrike(targetWorm.x);
consumeWeaponAmmo();
endTurnDelayed(3.0);
} else {
fireWeapon(testPower);
}
}, 1500);
}, 1000);
}
// HUD & Menu Management
function updateHUD() {
// Update Team Health Bars
const teamBarsContainer = document.getElementById('team-bars-container');
teamBarsContainer.innerHTML = '';
teams.forEach(t => {
const totalHP = t.worms.reduce((sum, w) => sum + Math.max(0, w.hp), 0);
const maxHP = t.worms.length * 100;
const pct = (totalHP / maxHP) * 100;
const row = document.createElement('div');
row.className = 'team-status-row';
row.innerHTML = `
<span class="team-color-badge" style="background: ${t.color}; color: ${t.color}"></span>
<span style="width: 100px;">${t.name}</span>
<div class="team-health-bar-bg">
<div class="team-health-bar-fill" style="width: ${pct}%; background: ${t.color}"></div>
</div>
<span style="font-size: 11px; font-family: monospace;">${Math.floor(totalHP)}</span>
`;
teamBarsContainer.appendChild(row);
});
// Weapon Indicator
document.getElementById('hud-weapon-icon').innerText = selectedWeapon.icon;
document.getElementById('hud-weapon-name').innerText = selectedWeapon.name;
document.getElementById('hud-weapon-ammo').innerText = selectedWeapon.ammo < 0 ? 'Ammo: Infinite' : `Ammo: ${selectedWeapon.ammo}`;
}
function buildWeaponGrid() {
const container = document.getElementById('weapon-grid-container');
container.innerHTML = '';
const categories = ['Indirect', 'Direct', 'AoE / Special', 'Utility'];
categories.forEach(cat => {
const catTitle = document.createElement('div');
catTitle.className = 'weapon-category-title';
catTitle.innerText = cat;
container.appendChild(catTitle);
const grid = document.createElement('div');
grid.className = 'weapon-grid';
WEAPONS.filter(w => w.category === cat).forEach(w => {
const btn = document.createElement('div');
btn.className = `weapon-btn ${w.id === selectedWeapon.id ? 'selected' : ''}`;
btn.onclick = () => {
selectedWeapon = w;
sound.playClick();
toggleWeaponMenu();
updateHUD();
};
btn.innerHTML = `
<div class="weapon-btn-icon">${w.icon}</div>
<div class="weapon-btn-info">
<div class="weapon-btn-title">${w.name}</div>
<div class="weapon-btn-count">${w.ammo < 0 ? 'Infinite' : `x${w.ammo}`}</div>
</div>
`;
grid.appendChild(btn);
});
container.appendChild(grid);
});
}
function toggleWeaponMenu() {
const menu = document.getElementById('weapon-menu');
if (menu.classList.contains('active')) {
menu.classList.remove('active');
} else {
buildWeaponGrid();
menu.classList.add('active');
}
}
// Game Setup & Start
function startGameFromLobby() {
sound.init();
sound.playClick();
// Create Teams & Worms
teams = [];
teamConfig.forEach((cfg, idx) => {
if (cfg.type !== 'off') {
let tDef = TEAMS_DEF[idx];
let worms = [];
for (let w = 1; w <= 4; w++) {
let worm = new Worm(idx, w, `${tDef.name.split(' ')[0]} ${w}`, 0, 0);
worm.isCPU = cfg.type === 'cpu';
worms.push(worm);
}
teams.push({ ...tDef, worms, isCPU: cfg.type === 'cpu' });
}
});
if (teams.length < 2) {
alert('Please enable at least 2 teams to play!');
return;
}
// Generate Terrain
generateTerrain(selectedTerrainType);
// Position Worms evenly on terrain surface
const spacing = (WORLD_WIDTH - 300) / (teams.length * 4);
let currentX = 150;
teams.forEach(team => {
team.worms.forEach(worm => {
worm.x = currentX;
// Raycast top down to find ground
for (let y = 50; y < WORLD_HEIGHT - 100; y += 4) {
if (isTerrainSolid(currentX, y)) {
worm.y = y - 12;
break;
}
}
currentX += spacing;
});
});
document.getElementById('setup-modal').style.display = 'none';
document.getElementById('gameover-modal').style.display = 'none';
gameState = 'PLAYING';
activeTeamIdx = -1;
activeWormIdx = 0;
startNextTurn();
// Turn Timer Loop
if (turnTimerInterval) clearInterval(turnTimerInterval);
turnTimerInterval = setInterval(() => {
if (gameState === 'PLAYING' && turnTimer > 0) {
turnTimer--;
document.getElementById('timer-text').innerText = turnTimer;
const timerBox = document.getElementById('timer-box');
if (turnTimer <= 10) {
timerBox.classList.add('urgent');
sound.playTimerTick(true);
} else {
timerBox.classList.remove('urgent');
sound.playTimerTick(false);
}
if (turnTimer === 0) {
addCombatText(getActiveWorm().x, getActiveWorm().y - 30, 'TIME OUT!', '#ef4444');
endTurnDelayed(1.0);
}
}
}, 1000);
}
function endGame(winningTeam) {
gameState = 'GAMEOVER';
if (turnTimerInterval) clearInterval(turnTimerInterval);
const title = document.getElementById('winner-title');
const subtitle = document.getElementById('winner-subtitle');
const statsBox = document.getElementById('match-stats');
if (winningTeam) {
title.innerText = `🏆 ${winningTeam.name.toUpperCase()} WINS!`;
title.style.color = winningTeam.color;
subtitle.innerText = 'Total Tactical Domination Achieved!';
} else {
title.innerText = 'DRAW MATCH!';
subtitle.innerText = 'Everyone was obliterated!';
}
statsBox.innerHTML = `
<div><strong>Terrain Type:</strong> ${selectedTerrainType.toUpperCase()}</div>
<div><strong>Active Teams:</strong> ${teams.length}</div>
<div><strong>Total Casualties:</strong> ${teams.length * 4 - (winningTeam ? winningTeam.worms.filter(w=>!w.isDead).length : 0)}</div>
`;
document.getElementById('gameover-modal').style.display = 'flex';
}
function returnToLobby() {
document.getElementById('gameover-modal').style.display = 'none';
document.getElementById('setup-modal').style.display = 'flex';
gameState = 'LOBBY';
}
// Main Render & Physics Update Loop
let lastTime = performance.now();
function gameLoop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.05); // cap delta time
lastTime = now;
// Handle Impact Freeze frame delay
if (impactFreezeFrames > 0) {
impactFreezeFrames--;
requestAnimationFrame(gameLoop);
return;
}
if (gameState === 'PLAYING') {
const activeWorm = getActiveWorm();
// Human Controls Input Processing
if (activeWorm && !activeWorm.isCPU && turnTimer > 0) {
if (keys['KeyA'] || keys['ArrowLeft']) {
activeWorm.vx = -100;
activeWorm.facing = -1;
}
if (keys['KeyD'] || keys['ArrowRight']) {
activeWorm.vx = 100;
activeWorm.facing = 1;
}
if (keys['KeyW'] || keys['ArrowUp']) {
aimAngle = Math.max(-Math.PI + 0.1, aimAngle - dt * 1.8);
}
if (keys['KeyS'] || keys['ArrowDown']) {
aimAngle = Math.min(-0.1, aimAngle + dt * 1.8);
}
if (keys['KeyE']) {
activeWorm.jump(-240, activeWorm.facing * 80);
keys['KeyE'] = false;
}
if (keys['KeyQ']) {
activeWorm.jump(-280, -activeWorm.facing * 100); // Backflip
keys['KeyQ'] = false;
}
if (keys['Space']) {
if (!selectedWeapon.utility && !selectedWeapon.strike) {
isCharging = true;
chargePower = Math.min(800, chargePower + dt * 500);
}
}
}
// Update Worms Physics
teams.forEach(team => {
team.worms.forEach(worm => worm.update(dt));
});
// Update Projectiles
for (let i = projectiles.length - 1; i >= 0; i--) {
projectiles[i].update(dt);
if (projectiles[i].isDead) projectiles.splice(i, 1);
}
// Update Systems
updateParticles(dt);
updateCombatTexts(dt);
updateDebrisChunks(dt);
// Smooth Camera Tracking (Follow active projectile or active worm)
let targetObj = projectiles.length > 0 ? projectiles[0] : activeWorm;
if (targetObj) {
camera.targetX = targetObj.x;
camera.targetY = targetObj.y;
}
camera.x += (camera.targetX - camera.x) * dt * 4;
camera.y += (camera.targetY - camera.y) * dt * 4;
// Dampen screen shake
if (camera.shake > 0) {
camera.shake = Math.max(0, camera.shake - dt * 25);
}
}
// RENDER CANVAS
ctx.save();
ctx.clearRect(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
// Apply Camera Transform & Screen Shake
const shakeX = (Math.random() - 0.5) * camera.shake;
const shakeY = (Math.random() - 0.5) * camera.shake;
ctx.translate(canvas.width / 2 + shakeX, canvas.height / 2 + shakeY);
ctx.scale(camera.zoom, camera.zoom);
ctx.translate(-camera.x, -camera.y);
// Sky Gradient Background
const skyGrad = ctx.createLinearGradient(0, 0, 0, WORLD_HEIGHT);
skyGrad.addColorStop(0, '#0f172a');
skyGrad.addColorStop(0.6, '#1e293b');
skyGrad.addColorStop(1, '#0284c7');
ctx.fillStyle = skyGrad;
ctx.fillRect(camera.x - WORLD_WIDTH, camera.y - WORLD_HEIGHT, WORLD_WIDTH * 2, WORLD_HEIGHT * 2);
// Draw Terrain Bitmap Mask
ctx.drawImage(terrainCanvas, 0, 0);
// Draw Water Layer
ctx.fillStyle = 'rgba(14, 165, 233, 0.65)';
ctx.beginPath();
ctx.moveTo(0, WATER_LEVEL);
for (let x = 0; x <= WORLD_WIDTH; x += 30) {
let waveY = WATER_LEVEL + Math.sin(now * 0.003 + x * 0.01) * 6;
ctx.lineTo(x, waveY);
}
ctx.lineTo(WORLD_WIDTH, WORLD_HEIGHT);
ctx.lineTo(0, WORLD_HEIGHT);
ctx.fill();
// Draw Debris Chunks
drawDebrisChunks(ctx);
// Draw Worms
teams.forEach(team => {
team.worms.forEach(worm => worm.draw(ctx));
});
// Draw Projectiles
projectiles.forEach(p => p.draw(ctx));
// Draw Particles & Combat Text
drawParticles(ctx);
drawCombatTexts(ctx);
ctx.restore();
requestAnimationFrame(gameLoop);
}
// Start Game Loop Engine
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.6-flash","provider":"openrouter","persona":"full","sessionId":"eeedc013-05c6-4fdf-8efb-6fd60cec0767","tokensIn":149534,"tokensOut":26845,"tokensTotal":176379,"turns":6,"toolCalls":5,"failedToolCalls":0,"timestamp":"2026-07-22T14:05:22.220Z"} -->