Army Tower Defense
by MagicStar573745 lines126.4 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Army Tower Defense</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap');
:root {
--army-green: #3a5a1c;
--dark-green: #1e2e0e;
--olive: #6b7c3a;
--tan: #c8a96e;
--sand: #e8d5a3;
--danger: #ff3a1a;
--gold: #ffd700;
--sky: #4a7ab5;
--hud-bg: rgba(10,18,5,0.92);
--border-col: #4a6a20;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--dark-green);
font-family: 'Share Tech Mono', monospace;
color: var(--sand);
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
#header {
background: var(--hud-bg);
border-bottom: 2px solid var(--border-col);
padding: 6px 12px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
#header h1 {
font-family: 'Orbitron', sans-serif;
font-size: 18px;
color: var(--gold);
letter-spacing: 3px;
text-shadow: 0 0 10px rgba(255,215,0,0.4);
}
.stat-group { display: flex; gap: 20px; align-items: center; }
.stat {
display: flex; align-items: center; gap: 6px;
font-size: 13px; font-family: 'Orbitron', sans-serif;
}
.stat-label { color: var(--olive); font-size: 10px; letter-spacing: 1px; }
.stat-value { color: var(--gold); font-size: 16px; font-weight: 700; }
.stat-value.lives { color: #ff6b6b; }
.stat-value.cash { color: #7fff7f; }
#main {
display: flex;
flex: 1;
overflow: hidden;
gap: 0;
}
#game-area {
position: relative;
flex: 1;
}
#gameCanvas {
display: block;
width: 100%;
height: 100%;
}
#sidebar {
width: 180px;
background: var(--hud-bg);
border-left: 2px solid var(--border-col);
display: flex;
flex-direction: column;
padding: 8px;
gap: 6px;
overflow-y: auto;
flex-shrink: 0;
}
#sidebar h3 {
font-family: 'Orbitron', sans-serif;
font-size: 11px;
color: var(--olive);
letter-spacing: 2px;
border-bottom: 1px solid var(--border-col);
padding-bottom: 4px;
margin-bottom: 2px;
}
.tower-btn {
background: linear-gradient(135deg, #1a2a08, #2a4010);
border: 1px solid var(--border-col);
border-radius: 4px;
padding: 7px 8px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
position: relative;
overflow: hidden;
}
.tower-btn:hover { border-color: var(--gold); background: linear-gradient(135deg, #2a3a10, #3a5018); }
.tower-btn.selected { border-color: var(--gold); box-shadow: 0 0 8px rgba(255,215,0,0.4); background: linear-gradient(135deg, #2a4010, #4a6a1a); }
.tower-btn.cant-afford { opacity: 0.45; cursor: not-allowed; }
.tower-btn .t-name {
font-family: 'Orbitron', sans-serif;
font-size: 11px;
color: var(--sand);
display: flex; align-items: center; gap: 5px;
}
.tower-btn .t-cost { color: #7fff7f; font-size: 11px; margin-top: 2px; }
.tower-btn .t-desc { color: #888; font-size: 9px; margin-top: 2px; line-height: 1.3; }
.tower-icon { font-size: 16px; }
#sell-btn {
background: linear-gradient(135deg, #3a0808, #5a1010);
border: 1px solid #7a2020;
border-radius: 4px;
padding: 6px;
cursor: pointer;
font-family: 'Orbitron', sans-serif;
font-size: 10px;
color: #ff9999;
text-align: center;
transition: all 0.15s;
display: none;
}
#sell-btn:hover { border-color: var(--danger); }
#sell-btn.visible { display: block; }
#wave-btn {
background: linear-gradient(135deg, #1a3a6a, #0a1a3a);
border: 2px solid #3a6aaa;
border-radius: 4px;
padding: 8px;
cursor: pointer;
font-family: 'Orbitron', sans-serif;
font-size: 10px;
color: #7ab5ff;
text-align: center;
letter-spacing: 1px;
transition: all 0.15s;
margin-top: 4px;
}
#wave-btn:hover { border-color: var(--gold); color: var(--gold); }
#wave-btn:disabled { opacity: 0.4; cursor: not-allowed; }
#msg-log {
font-size: 9px;
color: #667755;
line-height: 1.6;
max-height: 80px;
overflow-y: auto;
border-top: 1px solid var(--border-col);
padding-top: 4px;
margin-top: auto;
}
#msg-log .msg-line { margin-bottom: 1px; }
#msg-log .msg-line.warn { color: #ffaa44; }
#msg-log .msg-line.good { color: #88ff88; }
#overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.8);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
z-index: 100;
font-family: 'Orbitron', sans-serif;
}
#overlay h2 {
font-size: 36px;
color: var(--gold);
text-shadow: 0 0 20px rgba(255,215,0,0.6);
letter-spacing: 4px;
margin-bottom: 12px;
}
#overlay p { color: var(--sand); font-size: 14px; margin-bottom: 6px; text-align: center; }
#overlay button {
margin-top: 20px;
padding: 12px 30px;
font-family: 'Orbitron', sans-serif;
font-size: 14px;
background: var(--army-green);
color: var(--gold);
border: 2px solid var(--gold);
border-radius: 4px;
cursor: pointer;
letter-spacing: 2px;
transition: all 0.2s;
}
#overlay button:hover { background: var(--olive); box-shadow: 0 0 15px rgba(255,215,0,0.4); }
#tooltip {
position: absolute;
background: var(--hud-bg);
border: 1px solid var(--border-col);
border-radius: 4px;
padding: 6px 10px;
font-size: 10px;
pointer-events: none;
z-index: 50;
display: none;
max-width: 140px;
line-height: 1.5;
}
/* DEV PANEL */
#dev-panel {
display: none;
position: fixed;
top: 48px;
left: 50%;
transform: translateX(-50%);
background: rgba(5,10,3,0.97);
border: 2px solid #ffdd00;
border-radius: 6px;
padding: 10px;
z-index: 999;
min-width: 280px;
font-family: 'Share Tech Mono', monospace;
font-size: 12px;
color: var(--sand);
box-shadow: 0 0 30px rgba(255,221,0,0.3);
}
#dev-panel.visible { display: block; }
#dev-header {
font-family: 'Orbitron', sans-serif;
font-size: 13px;
color: #ffdd00;
letter-spacing: 2px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
#dev-close {
cursor: pointer;
color: #ff6666;
font-size: 16px;
line-height: 1;
}
.dev-section {
margin-bottom: 8px;
border-top: 1px solid #2a4a10;
padding-top: 8px;
}
.dev-label {
font-size: 9px;
color: var(--olive);
letter-spacing: 1px;
margin-bottom: 5px;
}
.dev-row {
display: flex;
gap: 6px;
align-items: center;
}
.dev-row button {
background: #1a3a0a;
border: 1px solid var(--border-col);
border-radius: 3px;
color: var(--sand);
font-family: 'Share Tech Mono', monospace;
font-size: 11px;
padding: 4px 8px;
cursor: pointer;
transition: all 0.1s;
flex: 1;
}
.dev-row button:hover { border-color: #ffdd00; color: #ffdd00; }
.dev-row button.active { background: #3a1a0a; border-color: #ff4400; color: #ff4400; }
.dev-row input, .dev-row select {
background: #0a1a05;
border: 1px solid var(--border-col);
border-radius: 3px;
color: var(--sand);
font-family: 'Share Tech Mono', monospace;
font-size: 11px;
padding: 4px 6px;
flex: 1;
}
#dev-tower-bar {
display: flex;
flex-wrap: wrap;
gap: 4px;
max-height: 120px;
overflow-y: auto;
padding: 4px;
background: #0a120a;
border: 1px solid var(--border-col);
border-radius: 3px;
}
.dev-tower-chip {
background: #1a3a0a;
border: 1px solid var(--border-col);
border-radius: 3px;
padding: 3px 7px;
font-size: 10px;
cursor: pointer;
color: var(--sand);
white-space: nowrap;
transition: all 0.1s;
}
.dev-tower-chip:hover { border-color: #ffdd00; color: #ffdd00; }
.dev-tower-chip.active { background: #2a5a0a; border-color: #44ff44; color: #44ff44; }
</style>
</head>
<body>
<div id="header">
<h1 id="title-btn" style="cursor:default;user-select:none;">⚔️ ARMY DEFENSE</h1>
<div class="stat-group">
<div class="stat">
<div>
<div class="stat-label">WAVE</div>
<div class="stat-value" id="wave-display">1</div>
</div>
</div>
<div class="stat">
<div>
<div class="stat-label">LIVES</div>
<div class="stat-value lives" id="lives-display">8</div>
</div>
</div>
<div class="stat">
<div>
<div class="stat-label">FUNDS</div>
<div class="stat-value cash" id="cash-display">$1500</div>
</div>
</div>
<div class="stat">
<div>
<div class="stat-label">SCORE</div>
<div class="stat-value" id="score-display">0</div>
</div>
</div>
<div class="stat">
<div>
<div class="stat-label">KILLS</div>
<div class="stat-value" id="kills-display">0</div>
</div>
</div>
</div>
</div>
<!-- DEV PANEL -->
<div id="dev-panel">
<div id="dev-header">🛠 DEV PANEL <span id="dev-close" onclick="toggleDevPanel()">✕</span></div>
<div class="dev-section">
<div class="dev-label">JUMP TO WAVE</div>
<div class="dev-row">
<input id="dev-wave-input" type="number" min="1" max="20" value="1" />
<button onclick="devJumpWave()">GO</button>
</div>
</div>
<div class="dev-section">
<div class="dev-label">ADD CASH</div>
<div class="dev-row">
<button onclick="devAddCash(500)">+$500</button>
<button onclick="devAddCash(2000)">+$2K</button>
<button onclick="devAddCash(10000)">+$10K</button>
</div>
</div>
<div class="dev-section">
<div class="dev-label">SPAWN ENEMY</div>
<div class="dev-row">
<select id="dev-enemy-select">
<option value="infantry">Infantry</option>
<option value="jeep">Jeep</option>
<option value="humvee">Humvee</option>
<option value="motorcycle">Motorcycle</option>
<option value="apc">APC</option>
<option value="ifv">IFV</option>
<option value="tank">Tank</option>
<option value="heavytank">Heavy Tank</option>
<option value="spg">SPG</option>
<option value="supplytruck">Supply Truck</option>
<option value="engineer">Engineer</option>
<option value="bombtruck">Bomb Truck</option>
<option value="plane">Plane</option>
<option value="helicopter">Helicopter</option>
<option value="gunship">Gunship</option>
<option value="drone">Drone</option>
<option value="paratrooper">Paratrooper</option>
</select>
<button onclick="devSpawnEnemy()">SPAWN</button>
</div>
</div>
<div class="dev-section">
<div class="dev-label">GOD MODE</div>
<div class="dev-row">
<button id="dev-god-btn" onclick="devToggleGod()">OFF</button>
<button onclick="devMaxLives()">FULL LIVES</button>
</div>
</div>
<div class="dev-section">
<div class="dev-label">FREE PLACE TOWER — select then click map</div>
<div id="dev-tower-bar">
<!-- populated by JS -->
</div>
<div class="dev-row" style="margin-top:5px;">
<button onclick="devCancelPlace()" id="dev-cancel-btn" style="display:none;">✕ CANCEL PLACE</button>
</div>
</div>
<div class="dev-section">
<div class="dev-label">PLACE ALL TOWERS</div>
<div class="dev-row">
<button onclick="devPlaceAllTowers()">AUTO PLACE</button>
<button onclick="devClearTowers()">CLEAR ALL</button>
</div>
</div>
</div>
<div id="main">
<div id="game-area">
<canvas id="gameCanvas"></canvas>
<div id="overlay">
<h2>🎖️ ARMY DEFENSE</h2>
<button onclick="startGame()">▶ DEPLOY TROOPS</button>
</div>
<div id="tooltip"></div>
</div>
<div id="sidebar" style="display:none;">
<button id="wave-btn" onclick="startWave()">▶ START WAVE 1</button>
<button id="sell-btn" onclick="sellSelected()">💰 SELL TOWER</button>
<h3>🏗 TOWERS</h3>
<div id="tower-buttons"></div>
<div id="msg-log"></div>
</div>
</div>
<script>
// ===================== SOUND ENGINE =====================
let audioCtx = null;
function getAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
return audioCtx;
}
function playSound(fn) {
try { fn(getAudio()); } catch(e) {}
}
// --- WEAPON FIRE SOUNDS ---
function sndM2Browning() {
// Rapid sharp crack — machine gun burst
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.12, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random() * 2 - 1) * Math.exp(-t * 60)
+ Math.sin(t * 1800 * Math.PI * 2) * Math.exp(-t * 90) * 0.4;
}
const src = ac.createBufferSource();
src.buffer = buf;
const g = ac.createGain(); g.gain.setValueAtTime(0.35, ac.currentTime);
src.connect(g); g.connect(ac.destination);
src.start();
});
}
function sndMortarFire() {
// Deep hollow thump — mortar launch
playSound(ac => {
const osc = ac.createOscillator();
const g = ac.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(90, ac.currentTime);
osc.frequency.exponentialRampToValueAtTime(30, ac.currentTime + 0.25);
g.gain.setValueAtTime(0.6, ac.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.3);
// Add noise layer for the whomp
const buf = ac.createBuffer(1, ac.sampleRate * 0.15, ac.sampleRate);
const nd = buf.getChannelData(0);
for (let i = 0; i < nd.length; i++) nd[i] = (Math.random()*2-1) * Math.exp(-(i/ac.sampleRate)*25);
const ns = ac.createBufferSource(); ns.buffer = buf;
const ng = ac.createGain(); ng.gain.setValueAtTime(0.3, ac.currentTime);
osc.connect(g); g.connect(ac.destination);
ns.connect(ng); ng.connect(ac.destination);
osc.start(); osc.stop(ac.currentTime + 0.3);
ns.start();
});
}
function sndTOWFire() {
// Rocket whoosh + ignition hiss
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.45, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
const noise = Math.random() * 2 - 1;
const env = t < 0.05 ? t / 0.05 : Math.exp(-(t - 0.05) * 4);
d[i] = noise * env * 0.8
+ Math.sin(t * 200 * Math.PI * 2) * Math.exp(-t * 8) * 0.3;
}
const src = ac.createBufferSource(); src.buffer = buf;
const g = ac.createGain(); g.gain.setValueAtTime(0.5, ac.currentTime);
const filter = ac.createBiquadFilter();
filter.type = 'bandpass'; filter.frequency.value = 600; filter.Q.value = 0.8;
src.connect(filter); filter.connect(g); g.connect(ac.destination);
src.start();
});
}
function sndEMPFire() {
// Electric zap buzz
playSound(ac => {
const osc1 = ac.createOscillator();
const osc2 = ac.createOscillator();
const g = ac.createGain();
osc1.type = 'sawtooth'; osc1.frequency.setValueAtTime(220, ac.currentTime);
osc1.frequency.linearRampToValueAtTime(880, ac.currentTime + 0.08);
osc1.frequency.linearRampToValueAtTime(110, ac.currentTime + 0.22);
osc2.type = 'square'; osc2.frequency.setValueAtTime(440, ac.currentTime);
osc2.frequency.linearRampToValueAtTime(1760, ac.currentTime + 0.1);
g.gain.setValueAtTime(0.18, ac.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.25);
osc1.connect(g); osc2.connect(g); g.connect(ac.destination);
osc1.start(); osc1.stop(ac.currentTime + 0.25);
osc2.start(); osc2.stop(ac.currentTime + 0.25);
});
}
function sndPatriotFire() {
// Loud boom + tail scream — SAM launch
playSound(ac => {
// Launch boom
const buf = ac.createBuffer(1, ac.sampleRate * 0.5, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t * 12) * 0.9
+ Math.sin(t * 60 * Math.PI*2) * Math.exp(-t*10) * 0.5;
}
const src = ac.createBufferSource(); src.buffer = buf;
// Missile scream osc
const osc = ac.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(800, ac.currentTime + 0.05);
osc.frequency.exponentialRampToValueAtTime(2400, ac.currentTime + 0.4);
const og = ac.createGain();
og.gain.setValueAtTime(0, ac.currentTime);
og.gain.linearRampToValueAtTime(0.12, ac.currentTime + 0.06);
og.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.45);
const g = ac.createGain(); g.gain.setValueAtTime(0.55, ac.currentTime);
src.connect(g); g.connect(ac.destination);
osc.connect(og); og.connect(ac.destination);
src.start(); osc.start(); osc.stop(ac.currentTime + 0.5);
});
}
// --- ENEMY KILL / DEATH SOUNDS ---
function sndKillInfantry() {
// Small pop + short grunt
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.18, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t * 45) * 0.5
+ Math.sin(t * 320 * Math.PI*2) * Math.exp(-t*30) * 0.25;
}
const src = ac.createBufferSource(); src.buffer = buf;
const g = ac.createGain(); g.gain.setValueAtTime(0.4, ac.currentTime);
src.connect(g); g.connect(ac.destination); src.start();
});
}
function sndKillJeep() {
// Crunching metal bang
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.3, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t * 18) * 0.8
+ Math.sin(t * 140 * Math.PI*2) * Math.exp(-t*22) * 0.35;
}
const src = ac.createBufferSource(); src.buffer = buf;
const filter = ac.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 1800;
const g = ac.createGain(); g.gain.setValueAtTime(0.55, ac.currentTime);
src.connect(filter); filter.connect(g); g.connect(ac.destination); src.start();
});
}
function sndKillTank() {
// Massive deep BOOM with rumble
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.8, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t * 6) * 1.0
+ Math.sin(t * 45 * Math.PI*2) * Math.exp(-t * 5) * 0.7
+ Math.sin(t * 80 * Math.PI*2) * Math.exp(-t * 8) * 0.4;
}
const src = ac.createBufferSource(); src.buffer = buf;
const filter = ac.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 900;
const g = ac.createGain(); g.gain.setValueAtTime(0.8, ac.currentTime);
src.connect(filter); filter.connect(g); g.connect(ac.destination); src.start();
});
}
function sndKillAPC() {
// Metal crunch + mid explosion
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.5, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t * 10) * 0.85
+ Math.sin(t * 70 * Math.PI*2) * Math.exp(-t*9) * 0.45
+ (Math.random()*2-1) * Math.exp(-t*30) * 0.3;
}
const src = ac.createBufferSource(); src.buffer = buf;
const filter = ac.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 1400;
const g = ac.createGain(); g.gain.setValueAtTime(0.65, ac.currentTime);
src.connect(filter); filter.connect(g); g.connect(ac.destination); src.start();
});
}
function sndKillPlane() {
// Explosion + high pitched metallic crash + descending whine
playSound(ac => {
// Explosion
const buf = ac.createBuffer(1, ac.sampleRate * 0.6, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t*9) * 0.7
+ Math.sin(t * 110 * Math.PI*2) * Math.exp(-t*12) * 0.3;
}
const src = ac.createBufferSource(); src.buffer = buf;
const g = ac.createGain(); g.gain.setValueAtTime(0.5, ac.currentTime);
// Descending engine whine
const osc = ac.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(1800, ac.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, ac.currentTime + 0.9);
const og = ac.createGain();
og.gain.setValueAtTime(0.15, ac.currentTime);
og.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.9);
src.connect(g); g.connect(ac.destination);
osc.connect(og); og.connect(ac.destination);
src.start(); osc.start(); osc.stop(ac.currentTime + 0.9);
});
}
function sndKillHelicopter() {
// Explosion + slowing rotor chop
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate * 0.55, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
const t = i / ac.sampleRate;
d[i] = (Math.random()*2-1) * Math.exp(-t*8) * 0.75
+ Math.sin(t * 65 * Math.PI*2) * Math.exp(-t*7) * 0.4;
}
const src = ac.createBufferSource(); src.buffer = buf;
const g = ac.createGain(); g.gain.setValueAtTime(0.6, ac.currentTime);
// Slowing blade chop
const osc = ac.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(22, ac.currentTime);
osc.frequency.exponentialRampToValueAtTime(4, ac.currentTime + 1.1);
const og = ac.createGain();
og.gain.setValueAtTime(0.18, ac.currentTime);
og.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 1.1);
src.connect(g); g.connect(ac.destination);
osc.connect(og); og.connect(ac.destination);
src.start(); osc.start(); osc.stop(ac.currentTime + 1.1);
});
}
const FIRE_SOUNDS = {
machinegun: sndM2Browning,
mortar: sndMortarFire,
rocket: sndTOWFire,
emp: sndEMPFire,
antiair: sndPatriotFire,
sniper: ac => playSound(ac2 => {
const buf = ac2.createBuffer(1, ac2.sampleRate*0.18, ac2.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t=i/ac2.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t*80)*0.6+Math.sin(t*2400*Math.PI*2)*Math.exp(-t*120)*0.3;}
const src=ac2.createBufferSource();src.buffer=buf;
const g=ac2.createGain();g.gain.setValueAtTime(0.5,ac2.currentTime);
src.connect(g);g.connect(ac2.destination);src.start();
}),
flamethrower: () => playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate*0.12, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t*15)*0.5;}
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type='bandpass';f.frequency.value=800;f.Q.value=0.5;
const g=ac.createGain();g.gain.setValueAtTime(0.25,ac.currentTime);
src.connect(f);f.connect(g);g.connect(ac.destination);src.start();
}),
howitzer: () => playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate*1.0, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t*4)*1.0+Math.sin(t*35*Math.PI*2)*Math.exp(-t*3.5)*0.8;}
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type='lowpass';f.frequency.value=700;
const g=ac.createGain();g.gain.setValueAtTime(0.9,ac.currentTime);
src.connect(f);f.connect(g);g.connect(ac.destination);src.start();
}),
mlrs: sndTOWFire,
napalm: () => playSound(ac => {
// Whoosh + ignition crackle
const buf = ac.createBuffer(1, ac.sampleRate*0.4, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t*6)*0.6+Math.sin(t*180*Math.PI*2)*Math.exp(-t*12)*0.3;}
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type='bandpass';f.frequency.value=600;f.Q.value=0.4;
const g=ac.createGain();g.gain.setValueAtTime(0.35,ac.currentTime);
src.connect(f);f.connect(g);g.connect(ac.destination);src.start();
}),
chaingun: () => playSound(ac => {
// Very rapid metallic rattle
const buf = ac.createBuffer(1, ac.sampleRate*0.04, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t*120)*0.5+Math.sin(t*2200*Math.PI*2)*Math.exp(-t*150)*0.2;}
const src=ac.createBufferSource();src.buffer=buf;
const g=ac.createGain();g.gain.setValueAtTime(0.2,ac.currentTime);
src.connect(g);g.connect(ac.destination);src.start();
}),
cluster: () => playSound(ac => {
// Thunk launch + scatter pops
const buf = ac.createBuffer(1, ac.sampleRate*0.35, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t*14)*0.75+Math.sin(t*75*Math.PI*2)*Math.exp(-t*10)*0.4;}
const src=ac.createBufferSource();src.buffer=buf;
const f=ac.createBiquadFilter();f.type='lowpass';f.frequency.value=1200;
const g=ac.createGain();g.gain.setValueAtTime(0.55,ac.currentTime);
src.connect(f);f.connect(g);g.connect(ac.destination);src.start();
}),
};
const KILL_SOUNDS = {
infantry: sndKillInfantry,
jeep: sndKillJeep,
tank: sndKillTank,
apc: sndKillAPC,
plane: sndKillPlane,
helicopter: sndKillHelicopter,
motorcycle: sndKillInfantry, // small pop
supplytruck: sndKillTank, // big boom
humvee: sndKillJeep, // metal crunch
heavytank: sndKillTank, // massive boom
spg: sndKillTank, // big boom
ifv: sndKillAPC, // mid explosion
gunship: sndKillHelicopter, // rotors winding down
drone: sndKillPlane, // descending whine
paratrooper: sndKillPlane, // plane explosion
engineer: sndKillInfantry, // small pop
bombtruck: sndKillTank, // massive boom
};
// ===================== DEV PANEL =====================
let devFreePlaceTower = null;
function devBuildTowerBar() {
const bar = document.getElementById('dev-tower-bar');
if (!bar) return;
bar.innerHTML = '';
TOWER_TYPES.forEach(t => {
const chip = document.createElement('div');
chip.className = 'dev-tower-chip';
chip.id = 'dev-chip-' + t.id;
chip.textContent = t.name;
chip.onclick = () => devSelectFreeTower(t.id);
bar.appendChild(chip);
});
}
function devSelectFreeTower(id) {
devFreePlaceTower = id;
document.querySelectorAll('.dev-tower-chip').forEach(c => c.classList.remove('active'));
document.getElementById('dev-chip-' + id)?.classList.add('active');
selectedTowerType = null;
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('selected'));
document.getElementById('dev-cancel-btn').style.display = 'block';
logMsg(`🛠 DEV: Click map to place ${TOWER_TYPES.find(t=>t.id===id)?.name} free`, 'good');
}
function devCancelPlace() {
devFreePlaceTower = null;
document.querySelectorAll('.dev-tower-chip').forEach(c => c.classList.remove('active'));
document.getElementById('dev-cancel-btn').style.display = 'none';
}
let devClickCount = 0, devClickTimer = null;
let devGodMode = false;
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('title-btn').addEventListener('click', () => {
devClickCount++;
if (devClickTimer) clearTimeout(devClickTimer);
devClickTimer = setTimeout(() => { devClickCount = 0; }, 600);
if (devClickCount >= 3) {
devClickCount = 0;
toggleDevPanel();
}
});
});
function toggleDevPanel() {
document.getElementById('dev-panel').classList.toggle('visible');
}
function devJumpWave() {
if (!gameRunning) { logMsg('Start the game first!', 'warn'); return; }
const target = parseInt(document.getElementById('dev-wave-input').value) || 1;
wave = target - 1;
waveActive = false;
waveEndTimer = 0;
enemies = [];
projectiles = [];
spawnQueue = [];
const wbtn = document.getElementById('wave-btn');
wbtn.disabled = false;
wbtn.textContent = `▶ START WAVE ${target}`;
document.getElementById('wave-display').textContent = target;
logMsg(`🛠 DEV: Jumped to wave ${target}`, 'good');
updateHUD();
}
function devAddCash(amount) {
if (!gameRunning) { logMsg('Start the game first!', 'warn'); return; }
cash += amount;
logMsg(`🛠 DEV: +$${amount} added`, 'good');
updateHUD();
}
function devSpawnEnemy() {
if (!gameRunning) { logMsg('Start the game first!', 'warn'); return; }
const type = document.getElementById('dev-enemy-select').value;
const base = ENEMY_TYPES[type];
if (!base) return;
spawnEnemy(type, base.hp, base.hp);
logMsg(`🛠 DEV: Spawned ${base.name}`, 'good');
}
function devToggleGod() {
devGodMode = !devGodMode;
const btn = document.getElementById('dev-god-btn');
btn.textContent = devGodMode ? 'ON' : 'OFF';
btn.classList.toggle('active', devGodMode);
logMsg(`🛠 DEV: God mode ${devGodMode ? 'ON' : 'OFF'}`, 'good');
}
function devMaxLives() {
lives = 8;
logMsg('🛠 DEV: Lives restored', 'good');
updateHUD();
}
function devPlaceAllTowers() {
if (!gameRunning) { logMsg('Start the game first!', 'warn'); return; }
// Place one of each tower type in a grid away from the path
const startX = canvasW * 0.1, startY = canvasH * 0.05;
let col = 0, row = 0;
TOWER_TYPES.forEach(tdef => {
const x = startX + col * 55;
const y = startY + row * 55;
if (!onPath(x, y)) {
towers.push({ x, y, type: tdef, cooldown: 0, target: null, angle: -Math.PI/2, disabled: 0, id: Math.random() });
}
col++;
if (col > 4) { col = 0; row++; }
});
logMsg('🛠 DEV: All towers placed', 'good');
}
function devClearTowers() {
towers = [];
selectedTower = null;
document.getElementById('sell-btn').classList.remove('visible');
logMsg('🛠 DEV: All towers cleared', 'warn');
}
const GROUND = ['infantry','jeep','tank','apc','motorcycle','supplytruck','humvee','heavytank','spg','ifv','engineer','bombtruck'];
const AIR = ['plane','helicopter','gunship','drone','paratrooper'];
const ALL = [...GROUND, ...AIR];
const TOWER_TYPES = [
{
id: 'machinegun', name: 'M2 Browning', icon: '🔫',
cost: 500, damage: 4, range: 80, fireRate: 22, color: '#7ab542',
desc: '.50 cal — fast fire, shreds infantry', targets: GROUND,
projectileColor: '#ffff44', projectileSize: 3, splash: 0
},
{
id: 'minefield', name: 'M15 Minefield', icon: '💥',
cost: 800, damage: 60, range: 40, fireRate: 999, color: '#6a5a2a',
desc: 'Places mines — detonates on contact', targets: GROUND,
projectileColor: '#ff4400', projectileSize: 8, splash: 40, mine: true
},
{
id: 'mortar', name: 'M120 Mortar', icon: '💣',
cost: 1000, damage: 18, range: 100, fireRate: 90, color: '#8a7a30',
desc: 'Slow fire, splash damage', targets: GROUND,
projectileColor: '#ff8800', projectileSize: 6, splash: 30
},
{
id: 'flamethrower', name: 'M2 Flamethrower', icon: '🔥',
cost: 1200, damage: 3, range: 55, fireRate: 4, color: '#cc4400',
desc: 'Short range, continuous fire — melts infantry', targets: GROUND,
projectileColor: '#ff6600', projectileSize: 5, splash: 18, flame: true
},
{
id: 'radar', name: 'Radar Tower', icon: '📡',
cost: 1500, damage: 0, range: 100, fireRate: 999, color: '#2a6a8a',
desc: 'Boosts range of all nearby towers by 40%', targets: [],
projectileColor: '#44ffff', projectileSize: 0, splash: 0, radarBoost: true
},
{
id: 'napalm', name: 'Napalm Tower', icon: '🔥',
cost: 1600, damage: 12, range: 105, fireRate: 600, color: '#cc5500',
desc: 'Drops napalm — burns a road zone for 5 sec', targets: GROUND,
projectileColor: '#ff6600', projectileSize: 7, splash: 0, napalm: true
},
{
id: 'sniper', name: 'M107 Sniper', icon: '🎯',
cost: 1800, damage: 120, range: 220, fireRate: 5000, color: '#8a6a2a',
desc: 'Extreme range, one-shot power, slow reload', targets: GROUND,
projectileColor: '#ffffff', projectileSize: 2, splash: 0
},
{
id: 'medic', name: 'Medic Bunker', icon: '🏥',
cost: 2000, damage: 0, range: 0, fireRate: 999, color: '#2a8a2a',
desc: 'Slowly restores 1 life every 30 seconds', targets: [],
projectileColor: '#44ff44', projectileSize: 0, splash: 0, medic: true
},
{
id: 'chaingun', name: 'M168 Chain Gun', icon: '🔫',
cost: 2200, damage: 6, range: 100, fireRate: 5, color: '#5a8a3a',
desc: 'Insane fire rate — click to toggle ON/OFF', targets: ALL,
projectileColor: '#ffff88', projectileSize: 2, splash: 0, chaingun: true
},
{
id: 'emp', name: 'JAMMER-9 EMP', icon: '⚡',
cost: 2500, damage: 6, range: 95, fireRate: 65, color: '#4a8abf',
desc: 'Electromagnetic — slows all ground', targets: GROUND,
projectileColor: '#44ddff', projectileSize: 5, splash: 0, slow: 0.45
},
{
id: 'antiair', name: 'Patriot SAM', icon: '✈️',
cost: 2750, damage: 28, range: 120, fireRate: 55, color: '#9a4abf',
desc: 'Surface-to-air — only hits aircraft!', targets: AIR,
projectileColor: '#ff44ff', projectileSize: 4, splash: 0
},
{
id: 'cluster', name: 'Cluster Bomb', icon: '💥',
cost: 2800, damage: 30, range: 140, fireRate: 180, color: '#8a3a5a',
desc: 'One shell splits into 8 bomblets mid-air', targets: GROUND,
projectileColor: '#ffaa00', projectileSize: 6, splash: 0, cluster: true
},
{
id: 'rocket', name: 'TOW Missile', icon: '🚀',
cost: 3000, damage: 40, range: 110, fireRate: 110, color: '#c84a1a',
desc: 'Anti-armor guided missile', targets: GROUND,
projectileColor: '#ff4400', projectileSize: 5, splash: 15
},
{
id: 'mlrs', name: 'MLRS Battery', icon: '🚀',
cost: 3500, damage: 25, range: 150, fireRate: 120, color: '#5a3a8a',
desc: 'Fires 6-rocket salvo — great vs clusters', targets: ALL,
projectileColor: '#ff8800', projectileSize: 4, splash: 20, salvo: 6
},
{
id: 'howitzer', name: 'M109 Howitzer', icon: '💣',
cost: 4000, damage: 90, range: 180, fireRate: 1000, color: '#7a5a1a',
desc: 'Massive artillery — huge splash, hits all ground', targets: GROUND,
projectileColor: '#ffaa00', projectileSize: 8, splash: 70
},
];
const ENEMY_TYPES = {
infantry: {
name: 'Infantry', hp: 60, speed: 0.7, reward: 25,
color: '#558844', size: 16, flying: false, wave: 1, armor: 0
},
jeep: {
name: 'Jeep', hp: 120, speed: 1.0, reward: 50,
color: '#8a7a40', size: 18, flying: false, wave: 1, armor: 0
},
tank: {
name: 'Tank', hp: 1500, speed: 0.4, reward: 300,
color: '#4a6a2a', size: 20, flying: false, wave: 3, armor: 10
},
apc: {
name: 'APC', hp: 250, speed: 0.6, reward: 100,
color: '#6a7a3a', size: 19, flying: false, wave: 4, armor: 5
},
plane: {
name: 'Bomber', hp: 180, speed: 1.3, reward: 125,
color: '#4a7ab5', size: 18, flying: true, wave: 5, armor: 0
},
helicopter: {
name: 'Chopper', hp: 300, speed: 0.9, reward: 175,
color: '#2a4a8a', size: 18, flying: true, wave: 6, armor: 0
},
// --- NEW ENEMIES ---
motorcycle: {
name: 'Motorcycle Scout', hp: 40, speed: 2.2, reward: 35,
color: '#8a6a20', size: 13, flying: false, wave: 2, armor: 0
},
supplytruck: {
name: 'Supply Truck', hp: 500, speed: 0.35, reward: 400,
color: '#7a5a30', size: 22, flying: false, wave: 3, armor: 0,
bonusCash: 300 // extra cash on kill
},
humvee: {
name: 'Humvee', hp: 160, speed: 1.4, reward: 60,
color: '#6a7a2a', size: 16, flying: false, wave: 2, armor: 2
},
heavytank: {
name: 'Heavy Tank', hp: 5000, speed: 0.28, reward: 600,
color: '#2a4a1a', size: 24, flying: false, wave: 5, armor: 25
},
spg: {
name: 'SPG', hp: 380, speed: 0.5, reward: 200,
color: '#5a5a2a', size: 20, flying: false, wave: 4, armor: 8,
shootsBack: true, shootCooldown: 8000, disableDuration: 8000, shootRange: 160
},
ifv: {
name: 'IFV', hp: 300, speed: 0.8, reward: 150,
color: '#4a6a3a', size: 19, flying: false, wave: 4, armor: 6,
spawnsInfantry: 3
},
gunship: {
name: 'Gunship', hp: 600, speed: 1.6, reward: 350,
color: '#1a2a6a', size: 20, flying: true, wave: 6, armor: 8,
shootsBack: true, shootCooldown: 8000, disableDuration: 8000, shootRange: 200
},
drone: {
name: 'Stealth Drone', hp: 120, speed: 1.8, reward: 150,
color: '#4a4a4a', size: 14, flying: true, wave: 5, armor: 0,
stealth: true // half range vs towers without radar
},
paratrooper: {
name: 'Paratroopers', hp: 80, speed: 0, reward: 0,
color: '#3a5a1a', size: 15, flying: true, wave: 5, armor: 0,
isTransport: true // drops infantry, then dies
},
engineer: {
name: 'Engineer', hp: 80, speed: 0.65, reward: 75,
color: '#8a6a1a', size: 14, flying: false, wave: 5, armor: 0,
repairsNearby: true, repairRate: 8, repairRange: 60
},
bombtruck: {
name: 'Bomb Truck', hp: 800, speed: 0.3, reward: 250,
color: '#5a2a1a', size: 22, flying: false, wave: 6, armor: 0,
baseDamage: 3 // deals 3 lives on reaching base
}
};
// ===================== STATE =====================
let canvas, ctx;
let canvasW, canvasH;
let path = [];
let towers = [];
let enemies = [];
let projectiles = [];
let particles = [];
let floaties = [];
let lives = 8;
let cash = 200;
let score = 0;
let kills = 0;
let wave = 0;
let waveActive = false;
let waveEndTimer = 0;
let napalmZones = []; // {x, y, radius, damage, life, maxLife}
let selectedTowerType = null;
let selectedTower = null;
let gameRunning = false;
let spawnQueue = [];
let spawnTimer = 0;
let animFrame;
// ===================== PATH DEFINITION =====================
function buildPath(w, h) {
// Fixed winding path across the map
const pts = [
{x: 0, y: h*0.2},
{x: w*0.15, y: h*0.2},
{x: w*0.15, y: h*0.7},
{x: w*0.35, y: h*0.7},
{x: w*0.35, y: h*0.25},
{x: w*0.55, y: h*0.25},
{x: w*0.55, y: h*0.75},
{x: w*0.75, y: h*0.75},
{x: w*0.75, y: h*0.35},
{x: w*0.92, y: h*0.35},
{x: w, y: h*0.35}
];
return pts;
}
function interpolatePath(pts, totalLen) {
let segs = [];
for (let i=0; i<pts.length-1; i++) {
let dx = pts[i+1].x - pts[i].x, dy = pts[i+1].y - pts[i].y;
segs.push({x1: pts[i].x, y1: pts[i].y, x2: pts[i+1].x, y2: pts[i+1].y, len: Math.sqrt(dx*dx+dy*dy)});
}
return segs;
}
// ===================== WAVE CONFIG =====================
function buildWave(waveNum) {
let queue = [];
const w = waveNum;
let cursor = 0; // running time cursor with random gaps
function addEnemy(type, baseDelay) {
const jitter = 2000 + Math.random() * 3000; // 2–5 sec random gap
cursor = Math.max(cursor, baseDelay) + jitter;
queue.push({type, delay: cursor});
}
// Infantry — random batches of 1-5
let infantryCount = 15 + w*8;
let spawned = 0;
while (spawned < infantryCount) {
const batchSize = Math.min(Math.ceil(Math.random()*4), infantryCount - spawned);
// Tight spacing within a batch (0.3–0.6 sec apart)
for (let b = 0; b < batchSize; b++) {
const withinBatchDelay = b * (300 + Math.random()*300);
queue.push({type:'infantry', delay: cursor + withinBatchDelay});
}
spawned += batchSize;
// Big random gap between batches (2–5 sec)
cursor += 2000 + Math.random()*3000;
}
// Jeeps from wave 2
if (w >= 2) {
cursor = 0;
let jeepCount = 5 + Math.floor(w*3);
for (let i=0; i<jeepCount; i++) addEnemy('jeep', 800);
}
// APCs from wave 4
if (w >= 4) {
cursor = 0;
let apcCount = 3 + Math.floor((w-3)*3);
for (let i=0; i<apcCount; i++) addEnemy('apc', 1800);
}
// Tanks from wave 3
if (w >= 3) {
cursor = 0;
let tankCount = 2 + Math.floor((w-2)*2);
for (let i=0; i<tankCount; i++) addEnemy('tank', 2400);
}
// Planes from wave 5
if (w >= 5) {
cursor = 0;
let planeCount = 4 + Math.floor((w-4)*3);
for (let i=0; i<planeCount; i++) addEnemy('plane', 1600);
}
// Motorcycles from wave 2 — fast scouts
if (w >= 2) {
cursor = 0;
let motoCount = 3 + Math.floor(w*2);
for (let i=0; i<motoCount; i++) addEnemy('motorcycle', 500);
}
// Humvees from wave 2
if (w >= 2) {
cursor = 0;
let humveeCount = 3 + Math.floor(w*2);
for (let i=0; i<humveeCount; i++) addEnemy('humvee', 700);
}
// Supply trucks from wave 3 — slow cash pinatas
if (w >= 3) {
cursor = 0;
let truckCount = 1 + Math.floor((w-2));
for (let i=0; i<truckCount; i++) addEnemy('supplytruck', 1200);
}
// SPGs from wave 4 — shoots back!
if (w >= 4) {
cursor = 0;
let spgCount = 1 + Math.floor((w-3));
for (let i=0; i<spgCount; i++) addEnemy('spg', 1800);
}
// IFVs from wave 4 — spawns infantry on death
if (w >= 4) {
cursor = 0;
let ifvCount = 2 + Math.floor((w-3)*1.5);
for (let i=0; i<ifvCount; i++) addEnemy('ifv', 2000);
}
// Heavy tanks from wave 5 — boss units
if (w >= 5) {
cursor = 0;
let heavyCount = 1 + Math.floor((w-4));
for (let i=0; i<heavyCount; i++) addEnemy('heavytank', 3000);
}
// Engineers from wave 5 — repairs nearby tanks
if (w >= 5) {
cursor = 0;
let engCount = 2 + Math.floor((w-4));
for (let i=0; i<engCount; i++) addEnemy('engineer', 2200);
}
// Stealth drones from wave 5
if (w >= 5) {
cursor = 0;
let droneCount = 2 + Math.floor((w-4)*2);
for (let i=0; i<droneCount; i++) addEnemy('drone', 1400);
}
// Paratroopers from wave 5 — air transport that drops infantry
if (w >= 5) {
cursor = 0;
let paraCount = 1 + Math.floor((w-4));
for (let i=0; i<paraCount; i++) addEnemy('paratrooper', 1600);
}
// Gunships from wave 6 — tougher choppers
if (w >= 6) {
cursor = 0;
let gunCount = 2 + Math.floor((w-5));
for (let i=0; i<gunCount; i++) addEnemy('gunship', 2600);
}
// Helicopters from wave 6
if (w >= 6) {
cursor = 0;
let chopCount = 3 + Math.floor((w-5)*2);
for (let i=0; i<chopCount; i++) addEnemy('helicopter', 2800);
}
// Bomb trucks from wave 6 — 3 lives if they reach base
if (w >= 6) {
cursor = 0;
let bombCount = 1 + Math.floor((w-5));
for (let i=0; i<bombCount; i++) addEnemy('bombtruck', 3500);
}
// Scale HP per wave
queue = queue.map(e => {
let base = {...ENEMY_TYPES[e.type]};
let scaledHp = Math.floor(base.hp * (1 + (w-1)*0.18));
return {...e, hp: scaledHp, maxHp: scaledHp};
});
return queue;
}
// ===================== INIT =====================
function initCanvas() {
canvas = document.getElementById('gameCanvas');
const area = document.getElementById('game-area');
canvasW = area.clientWidth;
canvasH = area.clientHeight;
canvas.width = canvasW;
canvas.height = canvasH;
ctx = canvas.getContext('2d');
path = buildPath(canvasW, canvasH);
}
function startGame() {
document.getElementById('overlay').style.display = 'none';
document.getElementById('sidebar').style.display = 'flex';
gameRunning = true;
lives = 8; cash = 1500; score = 0; kills = 0; wave = 0; waveEndTimer = 0;
towers = []; enemies = []; projectiles = []; particles = []; floaties = []; napalmZones = [];
selectedTower = null; selectedTowerType = null;
updateHUD();
buildTowerButtons();
devBuildTowerBar();
if (animFrame) cancelAnimationFrame(animFrame);
gameLoop();
}
function buildTowerButtons() {
const container = document.getElementById('tower-buttons');
container.innerHTML = '';
TOWER_TYPES.forEach(t => {
const btn = document.createElement('div');
btn.className = 'tower-btn';
btn.id = 'tbtn-'+t.id;
// Mini canvas preview
const previewId = 'preview-'+t.id;
btn.innerHTML = `
<div style="display:flex;align-items:center;gap:7px;">
<canvas id="${previewId}" width="38" height="38" style="border-radius:3px;background:#1a2a08;flex-shrink:0;"></canvas>
<div>
<div class="t-name">${t.name}</div>
<div class="t-cost">💰 $${t.cost}</div>
<div class="t-desc">${t.desc}</div>
</div>
</div>
`;
btn.onclick = () => selectTowerType(t.id);
container.appendChild(btn);
// Draw the preview after appending to DOM
requestAnimationFrame(() => {
const pc = document.getElementById(previewId);
if (!pc) return;
const pctx = pc.getContext('2d');
pctx.save();
pctx.translate(19, 22);
// Sandbags
drawSandbagsOn(pctx, 0, 5);
// Tower drawing using the shared draw functions but on pctx
const prevCtx = ctx;
ctx = pctx;
switch(t.id) {
case 'machinegun': drawM2Browning(0); break;
case 'mortar': drawMortarTower(0); break;
case 'rocket': drawTOWMissile(0); break;
case 'emp': drawEMPTower(0); break;
case 'antiair': drawPatriotSAM(0); break;
case 'sniper': drawSniper(0); break;
case 'flamethrower': drawFlamethrower(0); break;
case 'minefield': drawMinefield(0); break;
case 'howitzer': drawHowitzer(0); break;
case 'mlrs': drawMLRS(0); break;
case 'radar': drawRadar(0); break;
case 'medic': drawMedic(0); break;
case 'smoke': break; // removed
case 'apache': break; // removed
case 'napalm': drawNapalmTower(0); break;
case 'chaingun': drawChainGun(0, null); break;
case 'cluster': drawClusterLauncher(0); break;
}
ctx = prevCtx;
pctx.restore();
});
});
}
// ===================== HUD =====================
function updateHUD() {
document.getElementById('lives-display').textContent = lives;
document.getElementById('cash-display').textContent = '$'+cash;
document.getElementById('score-display').textContent = score;
document.getElementById('kills-display').textContent = kills;
document.getElementById('wave-display').textContent = wave || '-';
// Update affordability
TOWER_TYPES.forEach(t => {
const btn = document.getElementById('tbtn-'+t.id);
if (!btn) return;
btn.classList.toggle('cant-afford', cash < t.cost);
});
}
let msgLines = [];
function logMsg(text, type='') {
msgLines.unshift({text, type});
if (msgLines.length > 8) msgLines.pop();
const log = document.getElementById('msg-log');
log.innerHTML = msgLines.map(m => `<div class="msg-line ${m.type}">${m.text}</div>`).join('');
}
// ===================== TOWER SELECTION & PLACEMENT =====================
function selectTowerType(id) {
selectedTowerType = id;
selectedTower = null;
document.getElementById('sell-btn').classList.remove('visible');
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('selected'));
document.getElementById('tbtn-'+id)?.classList.add('selected');
}
function selectPlacedTower(tower) {
selectedTower = tower;
selectedTowerType = null;
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('selected'));
document.getElementById('sell-btn').classList.add('visible');
}
function sellSelected() {
if (!selectedTower) return;
const refund = Math.floor(selectedTower.type.cost * 0.6);
cash += refund;
towers = towers.filter(t => t !== selectedTower);
selectedTower = null;
document.getElementById('sell-btn').classList.remove('visible');
logMsg(`Tower sold for $${refund}`, 'good');
updateHUD();
}
// ===================== CANVAS CLICK =====================
canvas && canvas.addEventListener('click', handleClick);
// ---- Apache airstrike state ----
let apacheMode = false; // true when player has selected an apache beacon and is picking a strike point
function handleClick(e) {
if (!gameRunning) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvasW / rect.width;
const scaleY = canvasH / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
// Dev free-place mode — place tower at click point for free
if (devFreePlaceTower) {
const tdef = TOWER_TYPES.find(t => t.id === devFreePlaceTower);
if (tdef) {
towers.push({
x: mx, y: my,
type: tdef,
cooldown: tdef.medic ? 1800 : 0,
target: null,
angle: -Math.PI/2,
disabled: 0,
id: Math.random()
});
logMsg(`🛠 DEV: ${tdef.name} placed free`, 'good');
}
return;
}
// Check if clicking an existing tower
for (let t of towers) {
if (Math.hypot(t.x - mx, t.y - my) < 20) {
// Chain gun: clicking toggles it ON/OFF
if (t.type.chaingun) {
t.toggled = !t.toggled;
const state = t.toggled ? 'OFF' : 'ON';
logMsg(`Chain Gun turned ${state}`, t.toggled ? 'warn' : 'good');
return;
}
selectPlacedTower(t);
return;
}
}
// Place new tower
if (selectedTowerType) {
const tdef = TOWER_TYPES.find(t => t.id === selectedTowerType);
if (!tdef) return;
if (cash < tdef.cost) { logMsg('Not enough funds!', 'warn'); return; }
if (tdef.radar || tdef.medic) {
// Support towers can go anywhere (no road restriction)
} else if (onPath(mx, my)) {
logMsg('Cannot place on the road!', 'warn'); return;
}
towers.push({
x: mx, y: my,
type: tdef,
cooldown: tdef.medic ? 1800 : 0,
target: null,
angle: -Math.PI/2,
id: Math.random()
});
cash -= tdef.cost;
logMsg(`${tdef.name} deployed`, 'good');
updateHUD();
} else {
selectedTower = null;
document.getElementById('sell-btn').classList.remove('visible');
}
}
function launchApacheStrike(tx, ty) {
// Apache flies in from left, strafes across target, exits right
// Spawn a burst of projectiles along a horizontal line through target
playSound(ac => {
// Rotor sound + rocket burst
const osc = ac.createOscillator(); osc.type = 'sawtooth';
osc.frequency.setValueAtTime(28, ac.currentTime);
const og = ac.createGain(); og.gain.setValueAtTime(0.2, ac.currentTime);
og.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 1.5);
osc.connect(og); og.connect(ac.destination);
osc.start(); osc.stop(ac.currentTime + 1.5);
// Rocket burst
for (let i = 0; i < 8; i++) {
setTimeout(() => {
playSound(ac2 => {
const buf = ac2.createBuffer(1, ac2.sampleRate*0.2, ac2.sampleRate);
const d = buf.getChannelData(0);
for (let j=0;j<d.length;j++){const t2=j/ac2.sampleRate;d[j]=(Math.random()*2-1)*Math.exp(-t2*20)*0.7+Math.sin(t2*80*Math.PI*2)*Math.exp(-t2*15)*0.3;}
const src=ac2.createBufferSource();src.buffer=buf;
const g=ac2.createGain();g.gain.setValueAtTime(0.45,ac2.currentTime);
src.connect(g);g.connect(ac2.destination);src.start();
});
}, i * 120);
}
});
// Strafe — 8 rockets hitting in a line around target
for (let i = 0; i < 8; i++) {
setTimeout(() => {
if (!gameRunning) return;
const sx = tx + (Math.random()-0.5)*80;
const sy = ty + (Math.random()-0.5)*60;
// Damage all enemies near each rocket impact
for (let en of enemies) {
const d = Math.hypot(en.x - sx, en.y - sy);
if (d < 50) dealDamage(en, 80 * (1 - d/50));
}
spawnExplosion(sx, sy, '#ff6600');
}, i * 150);
}
}
function onPath(x, y) {
for (let i = 0; i < path.length-1; i++) {
let p1 = path[i], p2 = path[i+1];
let dx = p2.x-p1.x, dy = p2.y-p1.y;
let len = Math.sqrt(dx*dx+dy*dy);
if (len === 0) continue;
let t = ((x-p1.x)*dx + (y-p1.y)*dy) / (len*len);
t = Math.max(0, Math.min(1, t));
let cx = p1.x + t*dx, cy = p1.y + t*dy;
if (Math.hypot(x-cx, y-cy) < 28) return true;
}
return false;
}
// ===================== WAVE START =====================
function startWave() {
if (waveActive) return;
wave++;
waveActive = true;
spawnQueue = buildWave(wave);
spawnTimer = 0;
document.getElementById('wave-btn').disabled = true;
document.getElementById('wave-btn').textContent = `⚔ WAVE ${wave} IN PROGRESS...`;
document.getElementById('wave-display').textContent = wave;
logMsg(`=== WAVE ${wave} STARTED! ===`, 'warn');
}
// ===================== ENEMY SPAWNING & MOVEMENT =====================
function makePlaneWaypoints(w, h) {
// Generate a snaking aerial path loosely following the map
// but curving above the road with sweeping turns
const pts = [];
pts.push({x: -60, y: h * (0.1 + Math.random()*0.3)});
pts.push({x: w*0.18 + (Math.random()-0.5)*80, y: h*(0.15 + Math.random()*0.25)});
pts.push({x: w*0.32 + (Math.random()-0.5)*80, y: h*(0.55 + Math.random()*0.2)});
pts.push({x: w*0.5 + (Math.random()-0.5)*80, y: h*(0.1 + Math.random()*0.25)});
pts.push({x: w*0.65 + (Math.random()-0.5)*80, y: h*(0.55 + Math.random()*0.2)});
pts.push({x: w*0.82 + (Math.random()-0.5)*60, y: h*(0.2 + Math.random()*0.2)});
pts.push({x: w + 60, y: h*(0.2 + Math.random()*0.2)});
return pts;
}
function spawnEnemy(type, hp, maxHp) {
const base = ENEMY_TYPES[type];
// No lane offset — perpendicular offset breaks at sharp 90° path corners
const laneOffset = 0;
// Flying enemies get their own curving aerial waypoints
const isAerial = base.flying;
const planeWpts = isAerial ? makePlaneWaypoints(canvasW, canvasH) : null;
enemies.push({
type,
hp,
maxHp,
speed: base.speed * (1 + (wave-1)*0.04),
reward: base.reward,
bonusCash: base.bonusCash || 0,
baseDamage: base.baseDamage || 1,
color: base.color,
size: base.size,
flying: base.flying,
armor: base.armor || 0,
laneOffset: 0,
pathIdx: 0,
pathPct: 0,
x: planeWpts ? planeWpts[0].x : path[0].x,
y: planeWpts ? planeWpts[0].y : path[0].y,
angle: 0,
slow: 0,
id: Math.random(),
// special behaviors
stealth: base.stealth || false,
shootsBack: base.shootsBack || false,
shootTimer: base.shootCooldown ? Math.random() * base.shootCooldown : 0,
shootCooldown: base.shootCooldown || 0,
shootRange: base.shootRange || 150,
disableDuration: base.disableDuration || 0,
spawnsInfantry: base.spawnsInfantry || 0,
isTransport: base.isTransport || false,
transportDropped: false,
repairsNearby: base.repairsNearby || false,
repairRate: base.repairRate || 0,
repairRange: base.repairRange || 0,
repairTimer: 0,
lastAttackerId: null, // tracks which tower last hit this enemy
// aerial path
planeWpts,
planeWptIdx: 0,
planePct: 0,
});
}
function updateEnemies(dt) {
for (let e of enemies) {
let spd = e.speed * (e.slow > 0 ? 0.45 : 1);
e.slow = Math.max(0, e.slow - dt);
if (e.planeWpts) {
// ---- PLANE: follow its own curving waypoints ----
const wpts = e.planeWpts;
let moved = spd * dt * 0.08;
while (moved > 0 && e.planeWptIdx < wpts.length - 1) {
const p1 = wpts[e.planeWptIdx], p2 = wpts[e.planeWptIdx + 1];
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const segLen = Math.sqrt(dx*dx + dy*dy);
const remaining = segLen * (1 - e.planePct);
if (moved <= remaining) {
e.planePct += moved / segLen;
e.x = p1.x + dx * e.planePct;
e.y = p1.y + dy * e.planePct;
// Smoothly rotate toward direction of travel
const targetAngle = Math.atan2(dy, dx);
let da = targetAngle - e.angle;
while (da > Math.PI) da -= Math.PI*2;
while (da < -Math.PI) da += Math.PI*2;
e.angle += da * 0.08; // smooth turning
moved = 0;
} else {
moved -= remaining;
e.planeWptIdx++;
e.planePct = 0;
}
}
// Reached end of aerial path
if (e.planeWptIdx >= wpts.length - 1) {
e.hp = -1;
lives--;
logMsg(`Enemy reached base! -1 life`, 'warn');
updateHUD();
if (lives <= 0) { gameOver(); return; }
}
} else {
// ---- GROUND / HELICOPTER: follow road path ----
let moved = spd * dt * 0.06;
while (moved > 0 && e.pathIdx < path.length-1) {
let p1 = path[e.pathIdx], p2 = path[e.pathIdx+1];
let dx = p2.x-p1.x, dy = p2.y-p1.y;
let segLen = Math.sqrt(dx*dx+dy*dy);
let remaining = segLen * (1 - e.pathPct);
if (moved <= remaining) {
e.pathPct += moved / segLen;
// Apply lane offset perpendicular to road direction
const angle = Math.atan2(dy, dx);
const px = p1.x + dx*e.pathPct;
const py = p1.y + dy*e.pathPct;
const perp = angle + Math.PI/2;
e.x = px + Math.cos(perp) * e.laneOffset;
e.y = py + Math.sin(perp) * e.laneOffset;
e.angle = angle;
moved = 0;
} else {
moved -= remaining;
e.pathIdx++;
e.pathPct = 0;
if (e.pathIdx < path.length-1) {
e.x = path[e.pathIdx].x;
e.y = path[e.pathIdx].y;
}
}
}
// Reached end
if (e.pathIdx >= path.length-1) {
e.hp = -1;
const dmg = e.baseDamage || 1;
lives -= dmg;
if (dmg > 1) logMsg(`💣 Bomb Truck exploded! -${dmg} lives!`, 'warn');
else logMsg(`Enemy reached base! -1 life`, 'warn');
updateHUD();
if (lives <= 0) { gameOver(); return; }
}
}
// ---- SPECIAL BEHAVIORS ----
// SPG / Gunship: shoots back at the tower that last attacked it
if (e.shootsBack && towers.length > 0) {
e.shootTimer -= dt;
if (e.shootTimer <= 0) {
// Find the attacker tower — only if it's within shoot range
let target = null;
if (e.lastAttackerId) {
const attacker = towers.find(t => t.id === e.lastAttackerId);
if (attacker && Math.hypot(attacker.x - e.x, attacker.y - e.y) <= e.shootRange) {
target = attacker;
}
}
// Fallback: nearest tower within range
if (!target) {
let nearDist = Infinity;
for (let t of towers) {
const d = Math.hypot(t.x - e.x, t.y - e.y);
if (d <= e.shootRange && d < nearDist) { nearDist = d; target = t; }
}
}
if (target) {
e.shootTimer = e.shootCooldown;
target.disabled = e.disableDuration;
spawnExplosion(target.x, target.y, '#ff4400');
logMsg(`⚠ ${e.type === 'gunship' ? 'Gunship' : 'SPG'} disabled a tower!`, 'warn');
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate*0.4, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t2=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t2*8)*0.8+Math.sin(t2*50*Math.PI*2)*Math.exp(-t2*6)*0.5;}
const src=ac.createBufferSource();src.buffer=buf;
const g=ac.createGain();g.gain.setValueAtTime(0.6,ac.currentTime);
src.connect(g);g.connect(ac.destination);src.start();
});
} else {
e.shootTimer = e.shootCooldown; // no target in range, reset timer
}
}
}
// Engineer: repair nearby ground enemies
if (e.repairsNearby) {
e.repairTimer -= dt;
if (e.repairTimer <= 0) {
e.repairTimer = 30;
for (let other of enemies) {
if (other === e || other.flying) continue;
if (Math.hypot(other.x - e.x, other.y - e.y) < e.repairRange) {
other.hp = Math.min(other.maxHp, other.hp + e.repairRate);
}
}
}
}
// Paratrooper transport: drop infantry at midpoint
if (e.isTransport && !e.transportDropped) {
const progress = (e.planeWptIdx || 0) + (e.planePct || 0);
if (progress > 2) { // drop around the middle of the path
e.transportDropped = true;
e.hp = -1; // transport disappears after dropping
// Spawn 4 infantry on the road near current position
const dropX = e.x, dropY = e.y;
for (let pi = 0; pi < 4; pi++) {
const inf = {
type: 'infantry',
hp: 60, maxHp: 60,
speed: ENEMY_TYPES.infantry.speed,
reward: 15,
bonusCash: 0, baseDamage: 1,
color: ENEMY_TYPES.infantry.color,
size: ENEMY_TYPES.infantry.size,
flying: false, armor: 0, laneOffset: 0,
pathIdx: Math.max(0, Math.floor(path.length * 0.3)),
pathPct: Math.random() * 0.2,
x: dropX + (Math.random()-0.5)*40,
y: dropY + (Math.random()-0.5)*40,
angle: 0, slow: 0,
id: Math.random(),
stealth: false, shootsBack: false, shootTimer: 0,
shootCooldown: 0, disableDuration: 0,
spawnsInfantry: 0, isTransport: false, transportDropped: false,
repairsNearby: false, repairRate: 0, repairRange: 0, repairTimer: 0,
planeWpts: null, planeWptIdx: 0, planePct: 0,
};
enemies.push(inf);
}
logMsg('☁ Paratroopers dropped!', 'warn');
}
}
}
enemies = enemies.filter(e => e.hp > 0);
}
// ===================== TOWER SHOOTING =====================
function updateTowers(dt) {
// First pass: collect radar-boosted tower IDs
const boostedTowers = new Set();
for (let radar of towers.filter(t => t.type.radarBoost)) {
for (let t of towers) {
if (t !== radar && Math.hypot(t.x - radar.x, t.y - radar.y) < radar.type.range) {
boostedTowers.add(t.id);
}
}
}
// Medic bunker: restore 1 life every 30s
for (let t of towers.filter(t => t.type.medic)) {
t.cooldown -= dt;
if (t.cooldown <= 0) {
t.cooldown = 1800; // ~30 seconds at 60fps
if (lives < 8) {
lives++;
logMsg('Medic restored 1 life! ❤️', 'good');
updateHUD();
}
}
}
// Apache: handled via click — no auto-fire
// Radar: passive — no fire
for (let t of towers) {
if (t.type.radarBoost || t.type.medic) continue;
if (t.type.targets.length === 0) continue;
// Disabled tower (hit by SPG) — count down
if (t.disabled > 0) { t.disabled -= dt; continue; }
t.cooldown -= dt;
if (t.cooldown > 0) continue;
// Check if any radar tower covers this enemy's position (for stealth)
const hasRadarNearEnemy = (enemy) =>
towers.some(r => r.type.radarBoost && Math.hypot(r.x - enemy.x, r.y - enemy.y) < r.type.range * 1.5);
const effectiveRange = t.type.range * (boostedTowers.has(t.id) ? 1.4 : 1);
// Minefield: auto-detonate nearest enemy in range
if (t.type.mine) {
let closest = null, closestDist = Infinity;
for (let e of enemies) {
if (!t.type.targets.includes(e.type)) continue;
let d = Math.hypot(e.x - t.x, e.y - t.y);
if (d < effectiveRange && d < closestDist) { closestDist = d; closest = e; }
}
if (closest) {
t.cooldown = t.type.fireRate;
// Mine explosion — damage all in splash radius
for (let e of enemies) {
if (Math.hypot(e.x - closest.x, e.y - closest.y) < t.type.splash) {
dealDamage(e, t.type.damage);
}
}
spawnExplosion(closest.x, closest.y, '#ff8800');
playSound(ac => {
const buf = ac.createBuffer(1, ac.sampleRate*0.5, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++){const t2=i/ac.sampleRate;d[i]=(Math.random()*2-1)*Math.exp(-t2*10)*0.9+Math.sin(t2*55*Math.PI*2)*Math.exp(-t2*8)*0.5;}
const src=ac.createBufferSource();src.buffer=buf;
const g=ac.createGain();g.gain.setValueAtTime(0.7,ac.currentTime);
src.connect(g);g.connect(ac.destination);src.start();
});
}
continue;
}
// Find target (farthest along path within range)
let tgt = null, farthest = -1;
for (let e of enemies) {
if (!t.type.targets.includes(e.type)) continue;
// Stealth drones: 50% range penalty without radar coverage near the drone
const rangeVsThis = (e.stealth && !hasRadarNearEnemy(e)) ? effectiveRange * 0.5 : effectiveRange;
let dist = Math.hypot(e.x - t.x, e.y - t.y);
if (dist <= rangeVsThis) {
let prog = (e.planeWptIdx || 0) + (e.planePct || 0) + (e.pathIdx || 0) + (e.pathPct || 0);
if (prog > farthest) { farthest = prog; tgt = e; }
}
}
if (tgt) {
t.cooldown = t.type.fireRate;
t.angle = Math.atan2(tgt.y - t.y, tgt.x - t.x);
// Napalm: drop a burn zone at target location
if (t.type.napalm) {
FIRE_SOUNDS['napalm']?.();
napalmZones.push({
x: tgt.x, y: tgt.y,
radius: 38,
damage: t.type.damage,
life: 5000, // 5 seconds
maxLife: 5000,
id: Math.random()
});
spawnExplosion(tgt.x, tgt.y, '#ff6600');
// Cluster: fires one shell that splits into 8 bomblets
} else if (t.type.cluster) {
FIRE_SOUNDS['cluster']?.();
// Main shell travels to target then splits
const shellX = tgt.x, shellY = tgt.y;
projectiles.push({
x: t.x, y: t.y,
tx: shellX, ty: shellY,
targetId: tgt.id,
towerId: t.id,
speed: 5,
damage: 0, // no direct damage — splits on arrival
color: '#ffaa00',
size: 8,
splash: 0,
slow: 0,
towerType: 'cluster',
cluster: true,
clusterDamage: t.type.damage,
id: Math.random()
});
// MLRS: fire salvo of 6 rockets with slight spread
} else if (t.type.salvo) {
FIRE_SOUNDS['rocket']?.();
for (let s = 0; s < t.type.salvo; s++) {
const delay = s * 80;
setTimeout(() => {
if (!gameRunning) return;
const spread = (Math.random()-0.5) * 30;
projectiles.push({
x: t.x, y: t.y,
tx: tgt.x + spread, ty: tgt.y + spread,
targetId: tgt.id,
speed: 7,
damage: t.type.damage,
color: t.type.projectileColor,
size: t.type.projectileSize,
splash: t.type.splash,
slow: 0,
towerType: t.type.id,
id: Math.random()
});
}, delay);
}
} else {
fireProjectile(t, tgt);
}
}
}
}
function fireProjectile(tower, target) {
FIRE_SOUNDS[tower.type.id]?.();
projectiles.push({
x: tower.x, y: tower.y,
tx: target.x, ty: target.y,
targetId: target.id,
towerId: tower.id, // track which tower fired this
speed: 6,
damage: tower.type.damage,
color: tower.type.projectileColor,
size: tower.type.projectileSize,
splash: tower.type.splash || 0,
slow: tower.type.slow || 0,
towerType: tower.type.id,
id: Math.random()
});
}
function updateProjectiles(dt) {
for (let p of projectiles) {
// Track target
let tgt = enemies.find(e => e.id === p.targetId);
// If target is gone and no splash, discard the projectile
if (!tgt && p.splash <= 0) { p.dead = true; continue; }
if (tgt) { p.tx = tgt.x; p.ty = tgt.y; }
let dx = p.tx - p.x, dy = p.ty - p.y;
let dist = Math.sqrt(dx*dx+dy*dy);
if (dist < p.speed * 1.5) {
// Hit!
if (p.cluster) {
// Split into 8 bomblets in a spread
for (let b = 0; b < 8; b++) {
const angle = (b / 8) * Math.PI * 2;
const spread = 35 + Math.random() * 25;
const bx = p.tx + Math.cos(angle) * spread;
const by = p.ty + Math.sin(angle) * spread;
// Damage enemies near each bomblet
for (let e of enemies) {
if (Math.hypot(e.x - bx, e.y - by) < 28) {
dealDamage(e, p.clusterDamage, p.towerId);
}
}
spawnExplosion(bx, by, '#ffaa00');
}
spawnExplosion(p.tx, p.ty, '#ff6600');
FIRE_SOUNDS['cluster']?.();
} else if (p.splash > 0) {
for (let e of enemies) {
if (Math.hypot(e.x-p.tx, e.y-p.ty) < p.splash) {
dealDamage(e, p.damage * (1 - Math.hypot(e.x-p.tx,e.y-p.ty)/p.splash * 0.5), p.towerId);
}
}
spawnExplosion(p.tx, p.ty, '#ff8800');
} else if (tgt) {
dealDamage(tgt, p.damage, p.towerId);
if (p.slow) tgt.slow = 60;
}
p.dead = true;
} else {
p.x += (dx/dist)*p.speed;
p.y += (dy/dist)*p.speed;
}
}
projectiles = projectiles.filter(p => !p.dead);
}
function dealDamage(enemy, dmg, attackerTowerId = null) {
if (enemy.hp <= 0) return;
// Record which tower last hit this enemy (for shoot-back)
if (attackerTowerId) enemy.lastAttackerId = attackerTowerId;
let actualDmg = devGodMode ? enemy.hp : Math.max(1, dmg - (enemy.armor || 0));
enemy.hp -= actualDmg;
if (enemy.hp <= 0) {
const totalReward = enemy.reward + (enemy.bonusCash || 0);
cash += totalReward;
score += totalReward * 10;
kills++;
KILL_SOUNDS[enemy.type]?.();
if (enemy.bonusCash) logMsg(`💰 Supply truck! +$${totalReward}`, 'good');
floaties.push({x: enemy.x, y: enemy.y, text: '+$'+totalReward, life: 60, color: '#7fff7f'});
spawnExplosion(enemy.x, enemy.y, enemy.color);
// IFV: spawn infantry on death
if (enemy.spawnsInfantry > 0) {
for (let i = 0; i < enemy.spawnsInfantry; i++) {
setTimeout(() => {
if (!gameRunning) return;
enemies.push({
type: 'infantry', hp: 60, maxHp: 60,
speed: ENEMY_TYPES.infantry.speed,
reward: 10, bonusCash: 0, baseDamage: 1,
color: ENEMY_TYPES.infantry.color,
size: ENEMY_TYPES.infantry.size,
flying: false, armor: 0, laneOffset: 0,
pathIdx: enemy.pathIdx, pathPct: enemy.pathPct,
x: enemy.x + (Math.random()-0.5)*20,
y: enemy.y + (Math.random()-0.5)*20,
angle: 0, slow: 0, id: Math.random(),
stealth: false, shootsBack: false, shootTimer: 0,
shootCooldown: 0, disableDuration: 0,
spawnsInfantry: 0, isTransport: false, transportDropped: false,
repairsNearby: false, repairRate: 0, repairRange: 0, repairTimer: 0,
planeWpts: null, planeWptIdx: 0, planePct: 0,
});
}, i * 300);
}
logMsg('IFV destroyed — infantry bailing out!', 'warn');
}
updateHUD();
}
}
// ===================== PARTICLES =====================
function spawnExplosion(x, y, color) {
for (let i=0; i<8; i++) {
let angle = Math.random()*Math.PI*2;
let spd = 1+Math.random()*3;
particles.push({x,y,vx:Math.cos(angle)*spd,vy:Math.sin(angle)*spd,life:25,color,size:2+Math.random()*3});
}
}
function updateParticles(dt) {
for (let p of particles) {
p.x+=p.vx; p.y+=p.vy; p.life-=dt*0.05; p.vy+=0.08;
}
particles = particles.filter(p => p.life > 0);
for (let f of floaties) {
f.y -= 0.5; f.life -= dt*0.05;
}
floaties = floaties.filter(f => f.life > 0);
}
function updateNapalm(dt) {
for (let z of napalmZones) {
z.life -= dt;
// Damage enemies in zone every 300ms
if (!z.damageTimer) z.damageTimer = 0;
z.damageTimer -= dt;
if (z.damageTimer <= 0) {
z.damageTimer = 300;
for (let e of enemies) {
if (!e.flying && Math.hypot(e.x - z.x, e.y - z.y) < z.radius) {
dealDamage(e, z.damage);
}
}
// Occasional fire particle
for (let i = 0; i < 3; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * z.radius;
particles.push({
x: z.x + Math.cos(angle)*dist,
y: z.y + Math.sin(angle)*dist,
vx: (Math.random()-0.5)*1.5,
vy: -1 - Math.random()*2,
life: 20 + Math.random()*15,
color: Math.random() > 0.5 ? '#ff6600' : '#ffaa00',
size: 3 + Math.random()*4
});
}
}
}
napalmZones = napalmZones.filter(z => z.life > 0);
}
function updateSpawning(dt) {
if (!waveActive) return;
spawnTimer += dt;
while (spawnQueue.length > 0 && spawnTimer >= spawnQueue[0].delay) {
let e = spawnQueue.shift();
spawnEnemy(e.type, e.hp, e.maxHp);
}
// Only declare wave complete when queue is empty AND no enemies remain
// AND a 2 second grace period has passed (catches IFV/paratrooper spawns)
if (spawnQueue.length === 0 && enemies.length === 0) {
if (!waveEndTimer) waveEndTimer = 0;
waveEndTimer += dt;
if (waveEndTimer >= 2000) {
waveEndTimer = 0;
waveActive = false;
logMsg(`Wave ${wave} complete! 🎖`, 'good');
const wbtn = document.getElementById('wave-btn');
wbtn.disabled = false;
wbtn.textContent = `▶ START WAVE ${wave+1}`;
}
} else {
waveEndTimer = 0;
// Show live enemy count on the button so player knows wave is still going
const remaining = spawnQueue.length + enemies.length;
document.getElementById('wave-btn').textContent = `⚔ WAVE ${wave} — ${remaining} left`;
}
}
// ===================== DRAW =====================
function draw() {
ctx.clearRect(0, 0, canvasW, canvasH);
// Background terrain
drawTerrain();
// Path
drawPath();
// Range indicator for selected tower
if (selectedTower) {
ctx.beginPath();
ctx.arc(selectedTower.x, selectedTower.y, selectedTower.type.range, 0, Math.PI*2);
ctx.strokeStyle = 'rgba(255,215,0,0.4)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = 'rgba(255,215,0,0.06)';
ctx.fill();
}
// Preview range for placing
if (selectedTowerType) {
// Already shown on hover via mousemove
}
// Napalm burn zones
for (let z of napalmZones) {
const alpha = (z.life / z.maxLife) * 0.6;
const pulse = Math.sin(Date.now() * 0.01) * 0.1;
ctx.beginPath();
ctx.arc(z.x, z.y, z.radius, 0, Math.PI*2);
ctx.fillStyle = `rgba(255,80,0,${alpha + pulse})`;
ctx.fill();
ctx.strokeStyle = `rgba(255,160,0,${alpha + 0.2})`;
ctx.lineWidth = 2;
ctx.stroke();
}
// Towers
for (let t of towers) drawTower(t);
// Enemies
for (let e of enemies) drawEnemy(e);
// Projectiles
for (let p of projectiles) drawProjectile(p);
// Particles
for (let p of particles) {
ctx.globalAlpha = p.life/25;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Floaties
for (let f of floaties) {
ctx.globalAlpha = f.life/60;
ctx.fillStyle = f.color;
ctx.font = 'bold 12px Share Tech Mono';
ctx.fillText(f.text, f.x-14, f.y);
}
ctx.globalAlpha = 1;
// Base indicator
drawBase();
}
function drawTerrain() {
// Grass gradient
const grd = ctx.createLinearGradient(0,0,canvasW,canvasH);
grd.addColorStop(0,'#2a4a12');
grd.addColorStop(0.5,'#1e3a0e');
grd.addColorStop(1,'#2a4a12');
ctx.fillStyle = grd;
ctx.fillRect(0,0,canvasW,canvasH);
// Texture dots
ctx.fillStyle = 'rgba(60,90,20,0.18)';
for (let i=0; i<200; i++) {
let x = (i*137)%canvasW, y = (i*211)%canvasH;
ctx.fillRect(x,y,3,3);
}
}
function drawPath() {
// Road shadow
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (let i=1;i<path.length;i++) ctx.lineTo(path[i].x,path[i].y);
ctx.strokeStyle = 'rgba(0,0,0,0.4)';
ctx.lineWidth = 38;
ctx.lineJoin = 'round';
ctx.stroke();
// Road surface
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (let i=1;i<path.length;i++) ctx.lineTo(path[i].x,path[i].y);
ctx.strokeStyle = '#8a7a50';
ctx.lineWidth = 32;
ctx.lineJoin = 'round';
ctx.stroke();
// Road lines
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (let i=1;i<path.length;i++) ctx.lineTo(path[i].x,path[i].y);
ctx.strokeStyle = 'rgba(200,180,100,0.3)';
ctx.lineWidth = 2;
ctx.setLineDash([20,15]);
ctx.stroke();
ctx.setLineDash([]);
// Arrow indicators
for (let i=0; i<path.length-1; i++) {
let p1=path[i], p2=path[i+1];
let mx=(p1.x+p2.x)/2, my=(p1.y+p2.y)/2;
let angle=Math.atan2(p2.y-p1.y, p2.x-p1.x);
ctx.save();
ctx.translate(mx,my);
ctx.rotate(angle);
ctx.fillStyle='rgba(200,180,100,0.25)';
ctx.beginPath();
ctx.moveTo(8,0); ctx.lineTo(-8,-6); ctx.lineTo(-8,6);
ctx.closePath(); ctx.fill();
ctx.restore();
}
}
function drawTower(t) {
const selected = selectedTower === t;
const x = t.x, y = t.y;
const angle = t.angle || -Math.PI/2;
ctx.save();
ctx.translate(x, y);
// Drop shadow
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.beginPath();
ctx.ellipse(3, 14, 13, 5, 0, 0, Math.PI*2);
ctx.fill();
// Sandbag base (all towers share this)
drawSandbagsOn(ctx, 0, 6);
// Draw tower-specific body + barrel
switch(t.type.id) {
case 'machinegun': drawM2Browning(angle); break;
case 'mortar': drawMortarTower(angle); break;
case 'rocket': drawTOWMissile(angle); break;
case 'emp': drawEMPTower(angle); break;
case 'antiair': drawPatriotSAM(angle); break;
case 'sniper': drawSniper(angle); break;
case 'flamethrower': drawFlamethrower(angle); break;
case 'minefield': drawMinefield(angle); break;
case 'howitzer': drawHowitzer(angle); break;
case 'mlrs': drawMLRS(angle); break;
case 'radar': drawRadar(angle); break;
case 'medic': drawMedic(angle); break;
case 'napalm': drawNapalmTower(angle); break;
case 'chaingun': drawChainGun(angle, t); break;
case 'cluster': drawClusterLauncher(angle); break;
}
// Selection ring
if (selected) {
ctx.strokeStyle = '#ffd700';
ctx.lineWidth = 2;
ctx.setLineDash([4,4]);
ctx.beginPath();
ctx.arc(0, 0, 18, 0, Math.PI*2);
ctx.stroke();
ctx.setLineDash([]);
}
ctx.restore();
}
function drawSandbagsOn(pctx, cx, cy) {
const positions = [[-10,cy],[-4,cy],[2,cy],[8,cy],[-7,cy-4],[5,cy-4]];
for (let [bx,by] of positions) {
pctx.fillStyle = '#9a7a30';
pctx.beginPath();
pctx.ellipse(bx, by, 5, 3.5, 0, 0, Math.PI*2);
pctx.fill();
pctx.strokeStyle = '#6a5020';
pctx.lineWidth = 0.8;
pctx.stroke();
}
}
function drawSniper(angle) {
// Camo netting over platform
ctx.fillStyle = '#3a5a1a';
ctx.fillRect(-10, -4, 20, 10);
ctx.fillStyle = '#2a4a10';
for (let i=0;i<5;i++) ctx.fillRect(-8+i*4, -4, 2, 10);
// Prone shooter silhouette
ctx.fillStyle = '#4a6a22';
ctx.beginPath(); ctx.ellipse(-3, -2, 8, 4, 0, 0, Math.PI*2); ctx.fill();
// Head
ctx.fillStyle = '#c8a070'; ctx.beginPath(); ctx.arc(-9, -4, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#2a4a12'; ctx.beginPath(); ctx.arc(-9, -6, 4, Math.PI, 0); ctx.fill();
// Sniper barrel — very long, rotates
ctx.save(); ctx.rotate(angle);
ctx.fillStyle = '#111'; ctx.fillRect(0, -1.5, 26, 3);
ctx.fillStyle = '#333'; ctx.fillRect(-3, -3, 7, 6);
// Scope
ctx.fillStyle = '#444'; ctx.fillRect(6, -4, 8, 2);
// Suppressor
ctx.fillStyle = '#1a1a1a'; ctx.beginPath(); ctx.arc(26, 0, 2.5, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawFlamethrower(angle) {
// Tank on back
ctx.fillStyle = '#8a3a1a';
ctx.beginPath(); ctx.roundRect(-6, -8, 12, 14, 3); ctx.fill();
ctx.fillStyle = '#cc4a1a';
ctx.beginPath(); ctx.roundRect(-4, -6, 8, 10, 2); ctx.fill();
// Soldier body
ctx.fillStyle = '#4a6a22';
ctx.beginPath(); ctx.ellipse(2, -1, 6, 7, 0, 0, Math.PI*2); ctx.fill();
// Nozzle arm
ctx.save(); ctx.rotate(angle);
ctx.fillStyle = '#555'; ctx.fillRect(0, -2, 14, 4);
ctx.fillStyle = '#333'; ctx.fillRect(12, -3, 4, 6);
// Flame tip
ctx.fillStyle = 'rgba(255,120,0,0.8)'; ctx.beginPath(); ctx.arc(17, 0, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'rgba(255,200,0,0.6)'; ctx.beginPath(); ctx.arc(17, 0, 2, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawMinefield(angle) {
// Warning sign post
ctx.fillStyle = '#8a7a30';
ctx.fillRect(-1, -14, 2, 14);
ctx.fillStyle = '#ffdd00';
ctx.fillRect(-7, -16, 14, 8);
ctx.fillStyle = '#cc0000';
ctx.font = 'bold 7px monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('!', 0, -12);
// Mines on ground (3 visible bumps)
for (let [mx2,my2] of [[-8,4],[0,2],[8,5]]) {
ctx.fillStyle = '#2a2a1a';
ctx.beginPath(); ctx.ellipse(mx2, my2, 6, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#444';
ctx.beginPath(); ctx.arc(mx2, my2-1, 2, 0, Math.PI*2); ctx.fill();
}
}
function drawHowitzer(angle) {
// Heavy base platform
ctx.fillStyle = '#3a2a0a';
ctx.fillRect(-14, 2, 28, 10);
// Wheels
for (let wx of [-9, 9]) {
ctx.fillStyle = '#1a1a0a'; ctx.beginPath(); ctx.arc(wx, 11, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#333'; ctx.beginPath(); ctx.arc(wx, 11, 2.5, 0, Math.PI*2); ctx.fill();
}
// Shield plate
ctx.fillStyle = '#4a3a1a';
ctx.beginPath(); ctx.roundRect(-10, -8, 20, 14, 2); ctx.fill();
// Barrel assembly — rotates, very thick
ctx.save(); ctx.rotate(angle);
ctx.fillStyle = '#2a2010'; ctx.fillRect(-2, -5, 28, 10);
ctx.fillStyle = '#3a3020'; ctx.fillRect(-5, -6, 10, 12);
// Muzzle brake (wide)
ctx.fillStyle = '#1a1510'; ctx.fillRect(24, -7, 6, 14);
ctx.fillStyle = '#0a0a05';
for (let hy of [-4,0,4]) ctx.fillRect(26, hy-1, 4, 2);
ctx.restore();
}
function drawMLRS(angle) {
// Vehicle cab
ctx.fillStyle = '#4a5a2a';
ctx.beginPath(); ctx.roundRect(-12, -2, 10, 10, 2); ctx.fill();
ctx.fillStyle = 'rgba(150,210,255,0.5)';
ctx.fillRect(-11, -1, 8, 5);
// Chassis
ctx.fillStyle = '#3a4a1a'; ctx.fillRect(-12, 6, 24, 6);
// Wheels
for (let wx of [-8, 0, 8]) {
ctx.fillStyle = '#1a1a0a'; ctx.beginPath(); ctx.arc(wx, 12, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#444'; ctx.beginPath(); ctx.arc(wx, 12, 2, 0, Math.PI*2); ctx.fill();
}
// Rocket pod array — rotates
ctx.save(); ctx.rotate(angle);
ctx.fillStyle = '#2a3a1a'; ctx.beginPath(); ctx.roundRect(-2, -8, 22, 16, 3); ctx.fill();
// 6 rocket tubes
const tubeColors = ['#1a2a0a','#2a3a1a'];
for (let row=0;row<2;row++) for (let col=0;col<3;col++) {
ctx.fillStyle = tubeColors[(row+col)%2];
ctx.beginPath(); ctx.ellipse(2+col*6, -5+row*10, 2.5, 2.5, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#0a0a0a'; ctx.beginPath(); ctx.arc(2+col*6, -5+row*10, 1.2, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
function drawRadar(angle) {
// Tower mast
ctx.fillStyle = '#2a4a6a';
ctx.fillRect(-3, -18, 6, 22);
// Crossbrace
ctx.strokeStyle = '#1a3a5a'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-3,-4); ctx.lineTo(3,-12); ctx.stroke();
ctx.beginPath(); ctx.moveTo(3,-4); ctx.lineTo(-3,-12); ctx.stroke();
// Rotating dish
ctx.save(); ctx.rotate(angle * 0.3 + Date.now()*0.002);
ctx.fillStyle = '#3a6a9a';
ctx.beginPath(); ctx.ellipse(0, -18, 12, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#2a5a8a';
ctx.beginPath(); ctx.ellipse(0, -18, 12, 4, 0, 0, Math.PI); ctx.fill();
// Dish struts
ctx.strokeStyle = '#4a8abb'; ctx.lineWidth = 1.5;
for (let a=0;a<3;a++) {
ctx.beginPath(); ctx.moveTo(0,-18);
ctx.lineTo(Math.cos(a*Math.PI/1.5)*10, -18+Math.sin(a*Math.PI/1.5)*3); ctx.stroke();
}
ctx.restore();
// Pulse ring (visual effect)
ctx.strokeStyle = 'rgba(68,220,255,0.3)';
ctx.lineWidth = 1;
const pulse = (Date.now() % 1500) / 1500;
ctx.beginPath(); ctx.arc(0, -10, pulse * 30, 0, Math.PI*2); ctx.stroke();
}
function drawMedic(angle) {
// Bunker base
ctx.fillStyle = '#3a3a2a';
ctx.beginPath(); ctx.roundRect(-12, 0, 24, 12, 3); ctx.fill();
// Sandbag front
for (let bx of [-8,-2,4]) {
ctx.fillStyle = '#8a7030'; ctx.beginPath(); ctx.ellipse(bx, 1, 5, 3.5, 0, 0, Math.PI*2); ctx.fill();
}
// Bunker wall
ctx.fillStyle = '#5a5a4a';
ctx.beginPath(); ctx.roundRect(-10, -10, 20, 14, 4); ctx.fill();
// Red cross
ctx.fillStyle = '#cc2222';
ctx.fillRect(-2, -9, 4, 12);
ctx.fillRect(-6, -5, 12, 4);
// White cross highlight
ctx.fillStyle = '#ee4444';
ctx.fillRect(-1, -8, 2, 10);
ctx.fillRect(-5, -4, 10, 2);
// Window slit
ctx.fillStyle = '#222'; ctx.fillRect(-8, -4, 6, 3);
}
function drawNapalmTower(angle) {
// Fuel tank base — red danger cylinder
ctx.fillStyle = '#8a1a0a';
ctx.beginPath(); ctx.roundRect(-8, -2, 16, 14, 3); ctx.fill();
ctx.fillStyle = '#aa2a10';
ctx.beginPath(); ctx.roundRect(-6, 0, 12, 10, 2); ctx.fill();
// Warning stripes
ctx.fillStyle = '#ffaa00';
ctx.fillRect(-6, 2, 12, 2);
ctx.fillRect(-6, 6, 12, 2);
// Operator
ctx.fillStyle = '#4a6a22';
ctx.beginPath(); ctx.ellipse(3, -4, 6, 7, 0, 0, Math.PI*2); ctx.fill();
// Launch arm — rotates
ctx.save(); ctx.rotate(angle);
ctx.fillStyle = '#cc3300'; ctx.fillRect(0, -3, 16, 6);
ctx.fillStyle = '#ff4400'; ctx.fillRect(14, -4, 4, 8);
// Flame nozzle
ctx.fillStyle = 'rgba(255,100,0,0.8)'; ctx.beginPath(); ctx.arc(20, 0, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'rgba(255,200,0,0.7)'; ctx.beginPath(); ctx.arc(20, 0, 2.5, 0, Math.PI*2); ctx.fill();
ctx.restore();
// Fire flicker
const flicker = Math.sin(Date.now()*0.02)*0.3+0.7;
ctx.fillStyle = `rgba(255,80,0,${flicker*0.4})`;
ctx.beginPath(); ctx.arc(0, -6, 8, 0, Math.PI*2); ctx.fill();
}
function drawChainGun(angle, tower) {
// Dim when toggled off
if (tower && tower.toggled) ctx.globalAlpha = 0.5;
// Mount base
ctx.fillStyle = '#3a5a20'; ctx.fillRect(-10, 0, 20, 10);
ctx.fillStyle = '#2a4a15'; ctx.fillRect(-8, -4, 16, 8);
// Operator
ctx.fillStyle = '#4a6a22';
ctx.beginPath(); ctx.ellipse(-4, -6, 5, 6, 0, 0, Math.PI*2); ctx.fill();
// Helmet
ctx.fillStyle = '#2a4a12'; ctx.beginPath(); ctx.arc(-4, -10, 5, Math.PI, 0); ctx.fill();
// Rotating barrel assembly
ctx.save(); ctx.rotate(angle);
// Barrel housing
ctx.fillStyle = '#333'; ctx.fillRect(-3, -5, 10, 10);
// 6 rotating barrels (Vulcan style)
const spin = tower ? (Date.now() * (tower.overheated || tower.toggled ? 0 : 0.015)) : 0;
for (let b = 0; b < 6; b++) {
const ba = spin + b * Math.PI / 3;
const bx = Math.cos(ba) * 3.5;
const by = Math.sin(ba) * 3.5;
ctx.fillStyle = '#555';
ctx.beginPath(); ctx.arc(bx + 12, by, 1.8, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#777'; ctx.fillRect(3 + bx*0.5, by - 0.8, 10, 1.6);
}
// Muzzle
ctx.fillStyle = '#111'; ctx.fillRect(18, -3, 4, 6);
ctx.restore();
// ON/OFF indicator dot
if (tower) {
ctx.globalAlpha = 1;
ctx.fillStyle = tower.toggled ? '#ff3333' : '#33ff33';
ctx.beginPath(); ctx.arc(8, -12, 3, 0, Math.PI*2); ctx.fill();
ctx.font = 'bold 6px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = '#000';
ctx.fillText(tower.toggled ? 'OFF' : 'ON', 8, -12);
}
ctx.globalAlpha = 1;
}
function drawClusterLauncher(angle) {
// Launcher body on tripod
ctx.strokeStyle = '#4a4a2a'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(-9, 8); ctx.lineTo(0, -2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(9, 8); ctx.lineTo(0, -2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(0, -2); ctx.stroke();
// Operator
ctx.fillStyle = '#4a6a22';
ctx.beginPath(); ctx.ellipse(-6, -2, 5, 6, -0.2, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#2a4a12'; ctx.beginPath(); ctx.arc(-6, -7, 4, Math.PI, 0); ctx.fill();
// Barrel — rotates
ctx.save(); ctx.rotate(angle);
// Main tube
ctx.fillStyle = '#5a3a6a'; ctx.beginPath(); ctx.roundRect(-2, -5, 22, 10, 3); ctx.fill();
// Shell in chamber
ctx.fillStyle = '#ffaa00'; ctx.beginPath(); ctx.ellipse(8, 0, 5, 3.5, 0, 0, Math.PI*2); ctx.fill();
// Muzzle
ctx.fillStyle = '#2a1a3a'; ctx.fillRect(18, -6, 5, 12);
// Bomblet indicators (8 dots on shell)
ctx.fillStyle = '#ff4400';
for (let i=0;i<8;i++) {
const a = i * Math.PI/4;
ctx.beginPath(); ctx.arc(8 + Math.cos(a)*2.5, Math.sin(a)*2.5, 1, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
function drawM2Browning(angle) {
ctx.fillStyle = '#4a6a2a';
ctx.beginPath();
ctx.ellipse(0, -2, 6, 7, 0, 0, Math.PI*2);
ctx.fill();
// Helmet
ctx.fillStyle = '#3a5a1a';
ctx.beginPath();
ctx.arc(0, -8, 5, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#4a6a2a';
ctx.fillRect(-5, -9, 10, 3);
// Gun barrel — rotates
ctx.save();
ctx.rotate(angle);
ctx.fillStyle = '#222';
ctx.fillRect(0, -2, 18, 3);
ctx.fillStyle = '#333';
ctx.fillRect(-4, -4, 8, 6);
// Muzzle flash slot
ctx.fillStyle = '#111';
ctx.fillRect(16, -1, 4, 1);
ctx.restore();
}
function drawMortarTower(angle) {
// Soldier crouching body
ctx.fillStyle = '#4a6a2a';
ctx.beginPath();
ctx.ellipse(0, -1, 7, 6, 0, 0, Math.PI*2);
ctx.fill();
// Helmet
ctx.fillStyle = '#3a5a1a';
ctx.beginPath();
ctx.arc(0, -7, 5, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#4a6a2a';
ctx.fillRect(-5, -8, 10, 3);
// Mortar tube — fixed at steep angle (mortars fire up)
ctx.save();
ctx.rotate(-Math.PI * 0.65);
ctx.fillStyle = '#555';
ctx.fillRect(-2, -18, 5, 18);
// Tube end
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(0.5, -18, 3.5, 0, Math.PI*2);
ctx.fill();
// Base plate
ctx.fillStyle = '#444';
ctx.fillRect(-6, -1, 12, 3);
ctx.restore();
}
function drawTOWMissile(angle) {
// Tripod base
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-8, 4); ctx.lineTo(0, -2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(8, 4); ctx.lineTo(0, -2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, 6); ctx.lineTo(0, -2); ctx.stroke();
// Operator body
ctx.fillStyle = '#4a6a2a';
ctx.beginPath();
ctx.ellipse(-5, -3, 5, 6, -0.3, 0, Math.PI*2);
ctx.fill();
// Launcher tube — rotates
ctx.save();
ctx.rotate(angle);
// Launch tube body
ctx.fillStyle = '#5a4a30';
ctx.fillRect(-2, -4, 20, 7);
// Missile inside
ctx.fillStyle = '#cc3300';
ctx.fillRect(4, -2, 12, 4);
// Nose cone
ctx.fillStyle = '#ff4400';
ctx.beginPath();
ctx.moveTo(16, -2); ctx.lineTo(20, 0); ctx.lineTo(16, 2);
ctx.closePath(); ctx.fill();
// Wire guidance reel
ctx.fillStyle = '#888';
ctx.beginPath();
ctx.arc(-2, 0, 4, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
function drawEMPTower(angle) {
// Generator box base
ctx.fillStyle = '#2a4a7a';
ctx.fillRect(-9, -4, 18, 12);
ctx.fillStyle = '#3a5a9a';
ctx.fillRect(-7, -2, 14, 8);
// Vents
ctx.fillStyle = '#1a3a6a';
for (let i=0; i<3; i++) {
ctx.fillRect(-5 + i*4, 0, 2, 5);
}
// Dish / emitter — rotates
ctx.save();
ctx.rotate(angle);
// Arm
ctx.fillStyle = '#446688';
ctx.fillRect(0, -2, 12, 4);
// Dish head
ctx.fillStyle = '#5588bb';
ctx.beginPath();
ctx.arc(12, 0, 5, -Math.PI*0.6, Math.PI*0.6);
ctx.lineTo(12, 0);
ctx.closePath();
ctx.fill();
// Energy glow
ctx.fillStyle = 'rgba(68,180,255,0.5)';
ctx.beginPath();
ctx.arc(14, 0, 3, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
function drawPatriotSAM(angle) {
// Launch vehicle base
ctx.fillStyle = '#4a4a3a';
ctx.fillRect(-12, 2, 24, 8);
// Wheels
ctx.fillStyle = '#222';
for (let wx of [-8, 0, 8]) {
ctx.beginPath();
ctx.arc(wx, 10, 4, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#444';
ctx.beginPath();
ctx.arc(wx, 10, 2, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#222';
}
// Launcher arm — rotates
ctx.save();
ctx.rotate(angle);
// Arm
ctx.fillStyle = '#5a5a3a';
ctx.fillRect(-2, -3, 14, 6);
// Missile tubes (x2)
ctx.fillStyle = '#3a3a2a';
ctx.fillRect(4, -6, 12, 5);
ctx.fillRect(4, 1, 12, 5);
// Missile tips
ctx.fillStyle = '#cc2200';
ctx.beginPath();
ctx.moveTo(16, -6); ctx.lineTo(20, -3.5); ctx.lineTo(16, -1); ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(16, 1); ctx.lineTo(20, 3.5); ctx.lineTo(16, 6); ctx.closePath(); ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
const r = e.size;
let drawY = e.flying ? e.y - 14 : e.y;
const slowTint = e.slow > 0;
// Flying shadow on ground
if (e.flying) {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.ellipse(e.x, e.y + 2, r * 1.2, 4, 0, 0, Math.PI*2);
ctx.fill();
}
ctx.save();
ctx.translate(e.x, drawY);
// All flying units rotate to face direction of travel
if (e.flying && e.angle !== undefined) {
ctx.rotate(e.angle);
}
if (slowTint) { ctx.filter = 'hue-rotate(180deg) brightness(1.3)'; }
switch(e.type) {
case 'infantry': drawInfantrySoldier(r); break;
case 'jeep': drawJeep(r); break;
case 'tank': drawTank(r); break;
case 'apc': drawAPC(r); break;
case 'plane': drawPlane(r); break;
case 'helicopter': drawHelicopter(r); break;
case 'motorcycle': drawMotorcycle(r); break;
case 'supplytruck': drawSupplyTruck(r); break;
case 'humvee': drawHumvee(r); break;
case 'heavytank': drawHeavyTank(r); break;
case 'spg': drawSPG(r, e); break;
case 'ifv': drawIFV(r); break;
case 'gunship': drawGunship(r); break;
case 'drone': drawDrone(r, e); break;
case 'paratrooper': drawParatrooper(r); break;
case 'engineer': drawEngineer(r); break;
case 'bombtruck': drawBombTruck(r); break;
}
ctx.filter = 'none';
ctx.restore();
// HP bar (always above unit, unrotated)
let hpPct = e.hp / e.maxHp;
let barW = r * 2.4;
ctx.fillStyle = '#111';
ctx.fillRect(e.x - barW/2, drawY - r - 8, barW, 4);
ctx.fillStyle = hpPct > 0.5 ? '#44ff44' : hpPct > 0.25 ? '#ffaa00' : '#ff3333';
ctx.fillRect(e.x - barW/2, drawY - r - 8, barW * hpPct, 4);
}
function drawInfantrySoldier(r) {
const s = r * 1.4; // scale up
// Back leg
ctx.fillStyle = '#2a4a12';
ctx.fillRect(s*0.1, s*0.3, s*0.28, s*0.65);
// Boot back
ctx.fillStyle = '#1a1208';
ctx.fillRect(s*0.05, s*0.88, s*0.38, s*0.22);
// Front leg
ctx.fillStyle = '#3a5a1a';
ctx.fillRect(-s*0.28, s*0.3, s*0.28, s*0.65);
// Boot front
ctx.fillStyle = '#1a1208';
ctx.fillRect(-s*0.38, s*0.88, s*0.42, s*0.22);
// Torso / flak jacket
ctx.fillStyle = '#4a6a22';
ctx.beginPath();
ctx.roundRect(-s*0.38, -s*0.22, s*0.76, s*0.58, s*0.08);
ctx.fill();
// Camo patches on jacket
ctx.fillStyle = '#2a4010';
ctx.fillRect(-s*0.28, -s*0.1, s*0.18, s*0.14);
ctx.fillRect(s*0.04, s*0.05, s*0.14, s*0.16);
ctx.fillRect(-s*0.1, s*0.18, s*0.2, s*0.1);
// Belt/webbing
ctx.fillStyle = '#8a7a30';
ctx.fillRect(-s*0.38, s*0.2, s*0.76, s*0.1);
// Left arm extended holding rifle
ctx.fillStyle = '#4a6a22';
ctx.fillRect(-s*0.6, -s*0.18, s*0.28, s*0.22);
// Right arm
ctx.fillStyle = '#4a6a22';
ctx.fillRect(s*0.32, -s*0.12, s*0.22, s*0.22);
// Rifle body
ctx.fillStyle = '#1a1008';
ctx.fillRect(-s*0.72, -s*0.34, s*0.16, s*0.72);
// Rifle stock
ctx.fillStyle = '#3a2010';
ctx.fillRect(-s*0.66, s*0.22, s*0.22, s*0.16);
// Rifle barrel
ctx.fillStyle = '#0a0808';
ctx.fillRect(-s*0.76, -s*0.46, s*0.1, s*0.18);
// Neck
ctx.fillStyle = '#c8a070';
ctx.fillRect(-s*0.1, -s*0.34, s*0.2, s*0.16);
// Head
ctx.fillStyle = '#c8a070';
ctx.beginPath();
ctx.ellipse(0, -s*0.52, s*0.26, s*0.24, 0, 0, Math.PI*2);
ctx.fill();
// Helmet dome
ctx.fillStyle = '#3a5a1a';
ctx.beginPath();
ctx.ellipse(0, -s*0.58, s*0.3, s*0.22, 0, 0, Math.PI*2);
ctx.fill();
// Helmet brim
ctx.fillStyle = '#2a4a12';
ctx.fillRect(-s*0.32, -s*0.5, s*0.64, s*0.1);
// Eyes
ctx.fillStyle = '#1a1208';
ctx.fillRect(-s*0.1, -s*0.54, s*0.07, s*0.06);
ctx.fillRect(s*0.04, -s*0.54, s*0.07, s*0.06);
}
function drawJeep(r) {
const s = r * 1.1;
// Drop shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(0, s*0.85, s*1.1, s*0.2, 0, 0, Math.PI*2);
ctx.fill();
// Chassis / undercarriage
ctx.fillStyle = '#2a3a10';
ctx.fillRect(-s*1.0, s*0.3, s*2.0, s*0.4);
// Rear wheel
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.arc(-s*0.68, s*0.62, s*0.32, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#444';
ctx.beginPath(); ctx.arc(-s*0.68, s*0.62, s*0.18, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#777';
ctx.beginPath(); ctx.arc(-s*0.68, s*0.62, s*0.07, 0, Math.PI*2); ctx.fill();
// Front wheel
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.arc(s*0.68, s*0.62, s*0.32, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#444';
ctx.beginPath(); ctx.arc(s*0.68, s*0.62, s*0.18, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#777';
ctx.beginPath(); ctx.arc(s*0.68, s*0.62, s*0.07, 0, Math.PI*2); ctx.fill();
// Main body
ctx.fillStyle = '#5a7a28';
ctx.beginPath();
ctx.roundRect(-s*0.92, -s*0.28, s*1.84, s*0.72, s*0.08);
ctx.fill();
// Hood (front)
ctx.fillStyle = '#4a6a20';
ctx.beginPath();
ctx.roundRect(s*0.5, -s*0.18, s*0.48, s*0.55, s*0.06);
ctx.fill();
// Hood vents
ctx.fillStyle = '#3a5a18';
for (let i=0; i<3; i++) {
ctx.fillRect(s*0.56 + i*s*0.1, -s*0.06, s*0.06, s*0.28);
}
// Windshield frame
ctx.fillStyle = '#2a3a10';
ctx.fillRect(-s*0.12, -s*0.28, s*0.68, s*0.08);
ctx.fillRect(-s*0.12, s*0.18, s*0.62, s*0.07);
// Windshield glass
ctx.fillStyle = 'rgba(150,220,255,0.55)';
ctx.fillRect(-s*0.1, -s*0.22, s*0.58, s*0.42);
// Windshield reflection
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillRect(-s*0.06, -s*0.18, s*0.16, s*0.3);
// Driver head
ctx.fillStyle = '#c8a070';
ctx.beginPath(); ctx.arc(s*0.08, -s*0.32, s*0.18, 0, Math.PI*2); ctx.fill();
// Helmet
ctx.fillStyle = '#3a5a1a';
ctx.beginPath(); ctx.arc(s*0.08, -s*0.36, s*0.2, Math.PI, 0); ctx.fill();
// Spare tire on back
ctx.fillStyle = '#1a1a10';
ctx.beginPath(); ctx.arc(-s*0.82, s*0.08, s*0.24, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#333';
ctx.beginPath(); ctx.arc(-s*0.82, s*0.08, s*0.14, 0, Math.PI*2); ctx.fill();
// Headlights
ctx.fillStyle = '#ffffaa';
ctx.beginPath(); ctx.ellipse(s*0.94, -s*0.05, s*0.07, s*0.1, 0, 0, Math.PI*2); ctx.fill();
// Exhaust pipe
ctx.fillStyle = '#555';
ctx.fillRect(-s*0.95, s*0.1, s*0.08, s*0.28);
}
function drawTank(r) {
const s = r * 1.2;
// Ground shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(0, s*0.82, s*1.1, s*0.18, 0, 0, Math.PI*2);
ctx.fill();
// Track housings (outer)
ctx.fillStyle = '#1a1a0e';
ctx.beginPath();
ctx.roundRect(-s*1.05, -s*0.22, s*2.1, s*0.95, s*0.18);
ctx.fill();
// Track links
ctx.fillStyle = '#2e2e1e';
for (let i = -s*0.95; i < s*0.95; i += s*0.14) {
ctx.fillRect(i, -s*0.2, s*0.1, s*0.9);
}
// Track top highlight
ctx.fillStyle = '#3a3a28';
ctx.fillRect(-s*1.0, -s*0.22, s*2.0, s*0.12);
// Hull body
ctx.fillStyle = '#4e6e2a';
ctx.beginPath();
ctx.roundRect(-s*0.88, -s*0.44, s*1.76, s*0.72, s*0.06);
ctx.fill();
// Hull armor panel lines
ctx.strokeStyle = '#3a5a1a';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-s*0.88, -s*0.1); ctx.lineTo(s*0.88, -s*0.1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-s*0.4, -s*0.44); ctx.lineTo(-s*0.4, s*0.28); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s*0.4, -s*0.44); ctx.lineTo(s*0.4, s*0.28); ctx.stroke();
// Turret base ring
ctx.fillStyle = '#2e4e18';
ctx.beginPath();
ctx.ellipse(s*0.08, -s*0.42, s*0.62, s*0.2, 0, 0, Math.PI*2);
ctx.fill();
// Turret body
ctx.fillStyle = '#3e5e20';
ctx.beginPath();
ctx.roundRect(-s*0.42, -s*0.78, s*1.0, s*0.42, s*0.1);
ctx.fill();
// Turret side slope
ctx.fillStyle = '#4a6a28';
ctx.beginPath();
ctx.moveTo(-s*0.42, -s*0.38);
ctx.lineTo(-s*0.55, -s*0.62);
ctx.lineTo(s*0.58, -s*0.62);
ctx.lineTo(s*0.58, -s*0.38);
ctx.closePath(); ctx.fill();
// Main barrel
ctx.fillStyle = '#1e2e0e';
ctx.beginPath();
ctx.roundRect(s*0.38, -s*0.64, s*0.95, s*0.18, s*0.04);
ctx.fill();
// Barrel muzzle brake
ctx.fillStyle = '#111';
ctx.fillRect(s*1.26, -s*0.68, s*0.1, s*0.26);
// Gunner hatch
ctx.fillStyle = '#2a4a18';
ctx.beginPath();
ctx.ellipse(s*0.08, -s*0.7, s*0.2, s*0.14, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#1a3a10';
ctx.beginPath();
ctx.arc(s*0.08, -s*0.7, s*0.08, 0, Math.PI*2);
ctx.fill();
// Commander's MG on top
ctx.fillStyle = '#222';
ctx.fillRect(s*0.2, -s*0.82, s*0.08, s*0.2);
ctx.fillRect(s*0.18, -s*0.84, s*0.28, s*0.08);
// Antenna
ctx.strokeStyle = '#2a2a1a';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-s*0.3, -s*0.76); ctx.lineTo(-s*0.22, -s*1.18); ctx.stroke();
}
function drawAPC(r) {
const s = r * 1.1;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.beginPath();
ctx.ellipse(0, s*0.8, s*1.0, s*0.16, 0, 0, Math.PI*2);
ctx.fill();
// Track housings
ctx.fillStyle = '#1e1e12';
ctx.beginPath();
ctx.roundRect(-s*0.98, -s*0.1, s*1.96, s*0.82, s*0.15);
ctx.fill();
// Track links
ctx.fillStyle = '#2a2a18';
for (let i = -s*0.9; i < s*0.9; i += s*0.13) {
ctx.fillRect(i, -s*0.08, s*0.09, s*0.78);
}
// Main armored hull
ctx.fillStyle = '#5a7a30';
ctx.beginPath();
ctx.roundRect(-s*0.88, -s*0.68, s*1.76, s*0.82, s*0.1);
ctx.fill();
// Sloped front armor
ctx.fillStyle = '#4a6a24';
ctx.beginPath();
ctx.moveTo(s*0.62, -s*0.68);
ctx.lineTo(s*0.88, -s*0.38);
ctx.lineTo(s*0.88, s*0.14);
ctx.lineTo(s*0.62, s*0.14);
ctx.closePath(); ctx.fill();
// Armor panel seams
ctx.strokeStyle = '#3a5a20';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-s*0.88, -s*0.18); ctx.lineTo(s*0.62, -s*0.18); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-s*0.2, -s*0.68); ctx.lineTo(-s*0.2, s*0.14); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s*0.22, -s*0.68); ctx.lineTo(s*0.22, -s*0.18); ctx.stroke();
// Viewport windows (armored slits)
ctx.fillStyle = 'rgba(100,180,220,0.5)';
ctx.fillRect(s*0.64, -s*0.52, s*0.18, s*0.12);
ctx.fillRect(s*0.64, -s*0.32, s*0.18, s*0.12);
// Roof hatch
ctx.fillStyle = '#3a5a1a';
ctx.beginPath();
ctx.ellipse(-s*0.18, -s*0.72, s*0.28, s*0.18, 0, 0, Math.PI*2);
ctx.fill();
// Pintle-mounted MG
ctx.fillStyle = '#1a1a10';
ctx.fillRect(-s*0.1, -s*0.96, s*0.1, s*0.32);
ctx.fillRect(-s*0.22, -s*0.98, s*0.34, s*0.1);
// Rear door
ctx.fillStyle = '#3a5a20';
ctx.fillRect(-s*0.88, -s*0.55, s*0.12, s*0.6);
ctx.strokeStyle = '#2a4a18';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-s*0.88, -s*0.24); ctx.lineTo(-s*0.76, -s*0.24); ctx.stroke();
// Headlights
ctx.fillStyle = '#ffff88';
ctx.beginPath(); ctx.ellipse(s*0.86, -s*0.52, s*0.06, s*0.1, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(s*0.86, -s*0.28, s*0.06, s*0.1, 0, 0, Math.PI*2); ctx.fill();
// Antenna
ctx.strokeStyle = '#2a2a10';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-s*0.55, -s*0.68); ctx.lineTo(-s*0.44, -s*1.14); ctx.stroke();
}
function drawPlane(r) {
const s = r * 1.3;
// Engine exhaust glow
ctx.fillStyle = 'rgba(255,140,0,0.25)';
ctx.beginPath();
ctx.ellipse(-s*1.32, 0, s*0.22, s*0.16, 0, 0, Math.PI*2);
ctx.fill();
// Swept wings (back)
ctx.fillStyle = '#3a5a8a';
ctx.beginPath();
ctx.moveTo(s*0.1, s*0.06);
ctx.lineTo(-s*0.55, s*1.1);
ctx.lineTo(-s*0.8, s*1.08);
ctx.lineTo(-s*0.3, s*0.06);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(s*0.1, -s*0.06);
ctx.lineTo(-s*0.55, -s*1.1);
ctx.lineTo(-s*0.8, -s*1.08);
ctx.lineTo(-s*0.3, -s*0.06);
ctx.closePath(); ctx.fill();
// Wing markings
ctx.fillStyle = '#2a4a7a';
ctx.beginPath();
ctx.moveTo(-s*0.2, s*0.3);
ctx.lineTo(-s*0.55, s*0.85);
ctx.lineTo(-s*0.62, s*0.82);
ctx.lineTo(-s*0.28, s*0.3);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(-s*0.2, -s*0.3);
ctx.lineTo(-s*0.55, -s*0.85);
ctx.lineTo(-s*0.62, -s*0.82);
ctx.lineTo(-s*0.28, -s*0.3);
ctx.closePath(); ctx.fill();
// Fuselage
ctx.fillStyle = '#4a6aaa';
ctx.beginPath();
ctx.ellipse(0, 0, s*1.28, s*0.32, 0, 0, Math.PI*2);
ctx.fill();
// Fuselage belly shade
ctx.fillStyle = '#3a5a9a';
ctx.beginPath();
ctx.ellipse(0, s*0.1, s*1.1, s*0.2, 0, 0, Math.PI*2);
ctx.fill();
// Tail fins (vertical)
ctx.fillStyle = '#2a4a8a';
ctx.beginPath();
ctx.moveTo(-s*0.95, 0);
ctx.lineTo(-s*1.28, -s*0.55);
ctx.lineTo(-s*1.22, -s*0.55);
ctx.lineTo(-s*0.72, 0);
ctx.closePath(); ctx.fill();
// Horizontal stabilizers
ctx.fillStyle = '#3a5a9a';
ctx.beginPath();
ctx.moveTo(-s*0.88, s*0.04);
ctx.lineTo(-s*1.15, s*0.42);
ctx.lineTo(-s*1.22, s*0.4);
ctx.lineTo(-s*0.95, s*0.04);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(-s*0.88, -s*0.04);
ctx.lineTo(-s*1.15, -s*0.42);
ctx.lineTo(-s*1.22, -s*0.4);
ctx.lineTo(-s*0.95, -s*0.04);
ctx.closePath(); ctx.fill();
// Engine intakes
ctx.fillStyle = '#1a2a5a';
ctx.beginPath();
ctx.ellipse(-s*0.18, s*0.28, s*0.12, s*0.08, 0.4, 0, Math.PI*2); ctx.fill();
ctx.beginPath();
ctx.ellipse(-s*0.18, -s*0.28, s*0.12, s*0.08, -0.4, 0, Math.PI*2); ctx.fill();
// Exhaust nozzle
ctx.fillStyle = '#1a1a2a';
ctx.beginPath();
ctx.ellipse(-s*1.28, 0, s*0.14, s*0.18, 0, 0, Math.PI*2); ctx.fill();
// Nose cone
ctx.fillStyle = '#5a7abb';
ctx.beginPath();
ctx.moveTo(s*1.28, 0);
ctx.bezierCurveTo(s*1.1, -s*0.12, s*0.82, -s*0.2, s*0.72, -s*0.2);
ctx.lineTo(s*0.72, s*0.2);
ctx.bezierCurveTo(s*0.82, s*0.2, s*1.1, s*0.12, s*1.28, 0);
ctx.fill();
// Cockpit canopy
ctx.fillStyle = 'rgba(140,210,255,0.7)';
ctx.beginPath();
ctx.ellipse(s*0.52, -s*0.06, s*0.3, s*0.2, -0.2, 0, Math.PI*2);
ctx.fill();
// Canopy frame
ctx.strokeStyle = '#2a4a8a';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.ellipse(s*0.52, -s*0.06, s*0.3, s*0.2, -0.2, 0, Math.PI*2);
ctx.stroke();
// Bombs/payload under wings
ctx.fillStyle = '#3a3a2a';
ctx.beginPath(); ctx.ellipse(-s*0.38, s*0.5, s*0.18, s*0.07, 0.3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(-s*0.38, -s*0.5, s*0.18, s*0.07, -0.3, 0, Math.PI*2); ctx.fill();
}
function drawHelicopter(r) {
const s = r * 1.2;
const time = Date.now() * 0.01;
// Ground shadow (moving)
ctx.fillStyle = 'rgba(0,0,0,0.12)';
ctx.beginPath();
ctx.ellipse(0, s*0.78, s*1.2, s*0.18, 0, 0, Math.PI*2);
ctx.fill();
// Tail boom
ctx.fillStyle = '#1e3a52';
ctx.beginPath();
ctx.moveTo(-s*0.32, -s*0.06);
ctx.lineTo(-s*1.2, -s*0.06);
ctx.lineTo(-s*1.22, s*0.1);
ctx.lineTo(-s*0.32, s*0.12);
ctx.closePath(); ctx.fill();
// Tail fin (vertical)
ctx.fillStyle = '#162e42';
ctx.beginPath();
ctx.moveTo(-s*1.08, -s*0.06);
ctx.lineTo(-s*1.28, -s*0.42);
ctx.lineTo(-s*1.18, -s*0.42);
ctx.lineTo(-s*0.98, -s*0.06);
ctx.closePath(); ctx.fill();
// Tail rotor (spinning)
ctx.strokeStyle = '#8888aa';
ctx.lineWidth = 2.5;
for (let i=0; i<2; i++) {
const a = time*2.5 + i * Math.PI;
ctx.beginPath();
ctx.moveTo(-s*1.2, Math.cos(a)*s*0.06);
ctx.lineTo(-s*1.2, Math.cos(a)*s*0.38);
ctx.stroke();
}
// Skid struts
ctx.strokeStyle = '#1a2e3e';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-s*0.38, s*0.35); ctx.lineTo(-s*0.52, s*0.65); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s*0.28, s*0.35); ctx.lineTo(s*0.38, s*0.65); ctx.stroke();
// Skid bars
ctx.strokeStyle = '#1e3448';
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(-s*0.62, s*0.65); ctx.lineTo(s*0.52, s*0.65); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-s*0.58, s*0.72); ctx.lineTo(s*0.48, s*0.72); ctx.stroke();
// Main body
ctx.fillStyle = '#2e4e6e';
ctx.beginPath();
ctx.ellipse(0, 0, s*0.82, s*0.5, 0, 0, Math.PI*2);
ctx.fill();
// Body belly plate
ctx.fillStyle = '#243e58';
ctx.beginPath();
ctx.ellipse(s*0.05, s*0.2, s*0.68, s*0.28, 0, 0, Math.PI*2);
ctx.fill();
// Engine housing (top)
ctx.fillStyle = '#1e3448';
ctx.beginPath();
ctx.roundRect(-s*0.28, -s*0.52, s*0.56, s*0.22, s*0.06);
ctx.fill();
// Main rotor mast
ctx.fillStyle = '#333';
ctx.fillRect(-s*0.06, -s*0.62, s*0.12, s*0.14);
// Main rotor blades (3 blades, animated)
ctx.strokeStyle = '#666688';
ctx.lineWidth = 3.5;
for (let i=0; i<3; i++) {
const a = time + (i * Math.PI * 2 / 3);
const bx = Math.cos(a) * s*1.45;
const by = Math.sin(a) * s*0.28;
ctx.beginPath();
ctx.moveTo(Math.cos(a)*s*0.06, Math.sin(a)*s*0.02);
ctx.quadraticCurveTo(bx*0.5, by*0.5 - s*0.04, bx, by);
ctx.stroke();
}
// Rotor hub
ctx.fillStyle = '#444';
ctx.beginPath(); ctx.arc(0, -s*0.58, s*0.07, 0, Math.PI*2); ctx.fill();
// Cockpit bubble
ctx.fillStyle = 'rgba(140,210,255,0.65)';
ctx.beginPath();
ctx.ellipse(s*0.36, -s*0.06, s*0.42, s*0.36, -0.15, 0, Math.PI*2);
ctx.fill();
// Cockpit frame
ctx.strokeStyle = '#1a2e42';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.ellipse(s*0.36, -s*0.06, s*0.42, s*0.36, -0.15, 0, Math.PI*2);
ctx.stroke();
// Cockpit divider bar
ctx.beginPath(); ctx.moveTo(s*0.36, -s*0.4); ctx.lineTo(s*0.28, s*0.28); ctx.stroke();
// Pilot silhouette
ctx.fillStyle = '#1a2830';
ctx.beginPath(); ctx.ellipse(s*0.24, -s*0.1, s*0.12, s*0.14, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(s*0.22, -s*0.22, s*0.1, s*0.1, 0, 0, Math.PI*2); ctx.fill();
// Weapon pylons
ctx.fillStyle = '#1e3040';
ctx.fillRect(-s*0.45, s*0.2, s*0.18, s*0.1);
ctx.fillRect(s*0.04, s*0.2, s*0.18, s*0.1);
// Rocket pods
ctx.fillStyle = '#2a3a2a';
ctx.beginPath(); ctx.ellipse(-s*0.36, s*0.3, s*0.2, s*0.08, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(s*0.13, s*0.3, s*0.2, s*0.08, 0, 0, Math.PI*2); ctx.fill();
}
function drawMotorcycle(r) {
const s = r * 1.0;
// Wheels
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.arc(-s*0.6, s*0.4, s*0.35, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(s*0.6, s*0.4, s*0.35, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#444';
ctx.beginPath(); ctx.arc(-s*0.6, s*0.4, s*0.18, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(s*0.6, s*0.4, s*0.18, 0, Math.PI*2); ctx.fill();
// Frame
ctx.strokeStyle = '#6a5a20'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(-s*0.6, s*0.1); ctx.lineTo(0, -s*0.2); ctx.lineTo(s*0.6, s*0.1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, -s*0.2); ctx.lineTo(0, s*0.1); ctx.stroke();
// Handlebar
ctx.strokeStyle = '#888'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(s*0.4, -s*0.3); ctx.lineTo(s*0.7, -s*0.2); ctx.stroke();
// Rider body
ctx.fillStyle = '#4a6a22';
ctx.beginPath(); ctx.ellipse(s*0.05, -s*0.35, s*0.22, s*0.28, -0.3, 0, Math.PI*2); ctx.fill();
// Helmet
ctx.fillStyle = '#1a1a1a';
ctx.beginPath(); ctx.arc(s*0.15, -s*0.58, s*0.2, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'rgba(100,180,255,0.6)';
ctx.beginPath(); ctx.arc(s*0.22, -s*0.55, s*0.1, 0, Math.PI*2); ctx.fill();
}
function drawSupplyTruck(r) {
const s = r * 1.1;
// Wheels (6 — big truck)
ctx.fillStyle = '#111';
for (let wx of [-s*0.75, 0, s*0.75]) {
ctx.beginPath(); ctx.arc(wx, s*0.6, s*0.3, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(wx, s*0.6, s*0.14, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#111';
}
// Chassis
ctx.fillStyle = '#5a4a20'; ctx.fillRect(-s*0.95, s*0.1, s*1.9, s*0.55);
// Cargo box (big)
ctx.fillStyle = '#7a6a30';
ctx.beginPath(); ctx.roundRect(-s*0.88, -s*0.7, s*1.76, s*0.88, s*0.06); ctx.fill();
// Cargo markings
ctx.strokeStyle = '#4a3a10'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-s*0.88, -s*0.1); ctx.lineTo(s*0.88, -s*0.1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, -s*0.7); ctx.lineTo(0, -s*0.1); ctx.stroke();
// Supply star
ctx.fillStyle = '#ffdd00'; ctx.font = `bold ${s*0.4}px monospace`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('$', 0, -s*0.38);
// Cab
ctx.fillStyle = '#6a5a28'; ctx.beginPath(); ctx.roundRect(s*0.5, -s*0.42, s*0.42, s*0.55, s*0.06); ctx.fill();
ctx.fillStyle = 'rgba(150,210,255,0.5)'; ctx.fillRect(s*0.54, -s*0.36, s*0.34, s*0.28);
}
function drawHumvee(r) {
const s = r * 1.0;
// Wheels
ctx.fillStyle = '#111';
for (let [wx,wy] of [[-s*0.7,s*0.45],[s*0.7,s*0.45],[-s*0.7,-s*0.05],[s*0.7,-s*0.05]]) {
ctx.beginPath(); ctx.arc(wx, wy, s*0.3, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(wx, wy, s*0.14, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#111';
}
// Body — boxy military
ctx.fillStyle = '#6a7a28';
ctx.beginPath(); ctx.roundRect(-s*0.82, -s*0.45, s*1.64, s*0.72, s*0.1); ctx.fill();
// Roof turret ring
ctx.fillStyle = '#3a4a18'; ctx.beginPath(); ctx.arc(0, -s*0.4, s*0.22, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#222'; ctx.fillRect(-s*0.04, -s*0.72, s*0.08, s*0.35);
// Windows
ctx.fillStyle = 'rgba(140,200,255,0.55)';
ctx.fillRect(-s*0.55, -s*0.38, s*0.42, s*0.26);
ctx.fillRect(s*0.12, -s*0.38, s*0.42, s*0.26);
// Grill
ctx.fillStyle = '#2a3a10';
for (let i=0;i<3;i++) ctx.fillRect(s*0.65, -s*0.28+i*s*0.14, s*0.2, s*0.08);
}
function drawHeavyTank(r) {
const s = r * 1.3;
// Extra wide tracks
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.roundRect(-s*1.1, -s*0.28, s*2.2, s*1.05, s*0.2); ctx.fill();
ctx.fillStyle = '#2a2a18';
for (let i = -s*1.0; i < s*1.0; i += s*0.15) ctx.fillRect(i, -s*0.26, s*0.11, s*1.0);
ctx.fillStyle = '#3a3a28'; ctx.fillRect(-s*1.05, -s*0.28, s*2.1, s*0.14);
// Heavy hull with sloped armor
ctx.fillStyle = '#3a5a20';
ctx.beginPath();
ctx.moveTo(-s*0.95, s*0.72); ctx.lineTo(-s*0.95, -s*0.55);
ctx.lineTo(-s*0.7, -s*0.72); ctx.lineTo(s*0.7, -s*0.72);
ctx.lineTo(s*0.95, -s*0.55); ctx.lineTo(s*0.95, s*0.72);
ctx.closePath(); ctx.fill();
// Armour panels
ctx.strokeStyle = '#2a4a18'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-s*0.95, -s*0.1); ctx.lineTo(s*0.95, -s*0.1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-s*0.4, -s*0.72); ctx.lineTo(-s*0.4, s*0.72); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s*0.4, -s*0.72); ctx.lineTo(s*0.4, s*0.72); ctx.stroke();
// Massive turret
ctx.fillStyle = '#2a4a18';
ctx.beginPath(); ctx.roundRect(-s*0.55, -s*1.05, s*1.3, s*0.55, s*0.1); ctx.fill();
ctx.beginPath(); ctx.ellipse(s*0.12, -s*0.55, s*0.65, s*0.22, 0, 0, Math.PI*2); ctx.fill();
// Very long barrel
ctx.fillStyle = '#111'; ctx.fillRect(s*0.38, -s*0.82, s*1.2, s*0.22);
ctx.fillStyle = '#0a0a0a'; ctx.fillRect(s*1.5, -s*0.9, s*0.12, s*0.38);
// Commanders hatch
ctx.fillStyle = '#1a3a10'; ctx.beginPath(); ctx.ellipse(s*0.05, -s*0.92, s*0.22, s*0.15, 0, 0, Math.PI*2); ctx.fill();
// Extra armor skirts
ctx.fillStyle = '#2a4a18'; ctx.fillRect(-s*1.1, s*0.42, s*2.2, s*0.2);
}
function drawSPG(r, e) {
const s = r * 1.1;
// Tracks
ctx.fillStyle = '#1a1a0e'; ctx.beginPath(); ctx.roundRect(-s, -s*0.2, s*2, s*0.95, s*0.18); ctx.fill();
ctx.fillStyle = '#2a2a18';
for (let i=-s*0.92;i<s*0.92;i+=s*0.14) ctx.fillRect(i, -s*0.18, s*0.1, s*0.9);
// Hull
ctx.fillStyle = '#5a5a28'; ctx.beginPath(); ctx.roundRect(-s*0.88, -s*0.55, s*1.76, s*0.65, s*0.08); ctx.fill();
// Gun shield
ctx.fillStyle = '#4a4a20';
ctx.beginPath(); ctx.moveTo(-s*0.3, -s*0.55); ctx.lineTo(-s*0.5, -s*0.9); ctx.lineTo(s*0.5, -s*0.9); ctx.lineTo(s*0.3, -s*0.55); ctx.closePath(); ctx.fill();
// Big howitzer barrel
ctx.fillStyle = '#222'; ctx.fillRect(s*0.1, -s*0.78, s*1.1, s*0.22);
ctx.fillStyle = '#111'; ctx.fillRect(s*1.14, -s*0.84, s*0.1, s*0.34);
// Disabled flash
if (e.disabled > 0) {
ctx.fillStyle = `rgba(255,80,0,${0.4 + Math.sin(Date.now()*0.02)*0.4})`;
ctx.beginPath(); ctx.arc(0, -s*0.3, s*0.5, 0, Math.PI*2); ctx.fill();
}
}
function drawIFV(r) {
const s = r * 1.05;
// Tracks
ctx.fillStyle = '#1e1e12'; ctx.beginPath(); ctx.roundRect(-s*0.95, -s*0.12, s*1.9, s*0.85, s*0.15); ctx.fill();
ctx.fillStyle = '#2a2a18';
for (let i=-s*0.88;i<s*0.88;i+=s*0.13) ctx.fillRect(i, -s*0.1, s*0.09, s*0.82);
// Hull
ctx.fillStyle = '#4a6a30'; ctx.beginPath(); ctx.roundRect(-s*0.85, -s*0.65, s*1.7, s*0.78, s*0.1); ctx.fill();
// Turret with 30mm cannon
ctx.fillStyle = '#3a5a20'; ctx.beginPath(); ctx.ellipse(s*0.1, -s*0.6, s*0.45, s*0.3, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#1a1a0a'; ctx.fillRect(s*0.25, -s*0.7, s*0.85, s*0.14);
// Side hatch doors
ctx.fillStyle = '#3a5a20'; ctx.fillRect(-s*0.85, -s*0.45, s*0.1, s*0.55);
ctx.strokeStyle = '#2a4a18'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-s*0.85, -s*0.18); ctx.lineTo(-s*0.75, -s*0.18); ctx.stroke();
// Viewport slits
ctx.fillStyle = 'rgba(100,180,255,0.45)';
ctx.fillRect(s*0.52, -s*0.52, s*0.26, s*0.12);
// Troop silhouettes visible through hatch
ctx.fillStyle = '#2a3a18';
for (let i=0;i<3;i++) {
ctx.beginPath(); ctx.arc(-s*0.35 + i*s*0.3, -s*0.72, s*0.1, 0, Math.PI*2); ctx.fill();
}
}
function drawGunship(r) {
const s = r * 1.2;
const time = Date.now() * 0.012;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.12)'; ctx.beginPath(); ctx.ellipse(0, s*0.65, s*1.1, s*0.15, 0, 0, Math.PI*2); ctx.fill();
// Tail boom — heavier than chopper
ctx.fillStyle = '#111e30';
ctx.beginPath(); ctx.moveTo(-s*0.28, -s*0.08); ctx.lineTo(-s*1.25, -s*0.08); ctx.lineTo(-s*1.28, s*0.12); ctx.lineTo(-s*0.28, s*0.14); ctx.closePath(); ctx.fill();
// Tail fin
ctx.fillStyle = '#0e1828'; ctx.beginPath(); ctx.moveTo(-s*1.1, -s*0.08); ctx.lineTo(-s*1.32, -s*0.48); ctx.lineTo(-s*1.18, -s*0.48); ctx.lineTo(-s*0.95, -s*0.08); ctx.closePath(); ctx.fill();
// Tail rotor
ctx.strokeStyle = '#667799'; ctx.lineWidth = 2.5;
for (let i=0;i<2;i++) { const a=time*2.8+i*Math.PI; ctx.beginPath(); ctx.moveTo(-s*1.25, Math.cos(a)*s*0.04); ctx.lineTo(-s*1.25, Math.cos(a)*s*0.42); ctx.stroke(); }
// Skids — wider/sturdier
ctx.strokeStyle = '#111e30'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(-s*0.4, s*0.38); ctx.lineTo(-s*0.55, s*0.62); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s*0.3, s*0.38); ctx.lineTo(s*0.4, s*0.62); ctx.stroke();
ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(-s*0.65, s*0.62); ctx.lineTo(s*0.55, s*0.62); ctx.stroke();
// Main body — more angular
ctx.fillStyle = '#1a2e4a';
ctx.beginPath(); ctx.moveTo(s*0.85, 0); ctx.lineTo(s*0.6, -s*0.5); ctx.lineTo(-s*0.32, -s*0.52); ctx.lineTo(-s*0.32, s*0.48); ctx.lineTo(s*0.6, s*0.46); ctx.closePath(); ctx.fill();
// Chin gun turret
ctx.fillStyle = '#0a1820'; ctx.beginPath(); ctx.ellipse(s*0.72, s*0.18, s*0.22, s*0.18, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#111'; ctx.fillRect(s*0.88, s*0.12, s*0.3, s*0.1);
// Rocket pods (bigger)
ctx.fillStyle = '#162840';
ctx.beginPath(); ctx.ellipse(-s*0.08, s*0.36, s*0.25, s*0.1, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(-s*0.08, -s*0.36, s*0.25, s*0.1, 0, 0, Math.PI*2); ctx.fill();
// Cockpit
ctx.fillStyle = 'rgba(140,210,255,0.6)'; ctx.beginPath(); ctx.ellipse(s*0.42, -s*0.08, s*0.38, s*0.32, -0.1, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#0e1a2a'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.ellipse(s*0.42, -s*0.08, s*0.38, s*0.32, -0.1, 0, Math.PI*2); ctx.stroke();
// Rotor mast
ctx.fillStyle = '#333'; ctx.fillRect(-s*0.06, -s*0.62, s*0.12, s*0.14);
// Main rotors
ctx.strokeStyle = '#556688'; ctx.lineWidth = 4;
for (let i=0;i<3;i++) { const a=time+i*Math.PI*2/3; ctx.beginPath(); ctx.moveTo(Math.cos(a)*s*0.06, Math.sin(a)*s*0.02); ctx.quadraticCurveTo(Math.cos(a)*s*0.7, Math.sin(a)*s*0.14-s*0.04, Math.cos(a)*s*1.55, Math.sin(a)*s*0.32); ctx.stroke(); }
ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(0, -s*0.58, s*0.08, 0, Math.PI*2); ctx.fill();
}
function drawDrone(r, e) {
const s = r * 1.0;
const stealth = e.stealth;
ctx.globalAlpha = stealth ? 0.4 : 1.0; // half-visible stealth effect
// Fuselage — angular, dark
ctx.fillStyle = '#2a2a2a';
ctx.beginPath(); ctx.ellipse(0, 0, s*1.1, s*0.25, 0, 0, Math.PI*2); ctx.fill();
// Swept wings — very angular
ctx.fillStyle = '#1a1a1a';
ctx.beginPath(); ctx.moveTo(s*0.2, 0); ctx.lineTo(-s*0.6, -s*0.9); ctx.lineTo(-s*0.85, -s*0.88); ctx.lineTo(-s*0.2, 0); ctx.closePath(); ctx.fill();
ctx.beginPath(); ctx.moveTo(s*0.2, 0); ctx.lineTo(-s*0.6, s*0.9); ctx.lineTo(-s*0.85, s*0.88); ctx.lineTo(-s*0.2, 0); ctx.closePath(); ctx.fill();
// V-tail
ctx.beginPath(); ctx.moveTo(-s*0.95, 0); ctx.lineTo(-s*1.1, -s*0.38); ctx.lineTo(-s*0.98, -s*0.36); ctx.lineTo(-s*0.82, 0); ctx.closePath(); ctx.fill();
ctx.beginPath(); ctx.moveTo(-s*0.95, 0); ctx.lineTo(-s*1.1, s*0.38); ctx.lineTo(-s*0.98, s*0.36); ctx.lineTo(-s*0.82, 0); ctx.closePath(); ctx.fill();
// Sensor nose
ctx.fillStyle = '#333'; ctx.beginPath(); ctx.ellipse(s*1.1, 0, s*0.15, s*0.1, 0, 0, Math.PI*2); ctx.fill();
// Stealth shimmer
if (stealth) {
ctx.strokeStyle = 'rgba(100,200,255,0.5)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.ellipse(0, 0, s*1.1, s*0.25, 0, 0, Math.PI*2); ctx.stroke();
}
ctx.globalAlpha = 1.0;
}
function drawParatrooper(r) {
const s = r * 1.1;
// Transport plane body
ctx.fillStyle = '#3a5a3a';
ctx.beginPath(); ctx.ellipse(0, 0, s*1.2, s*0.38, 0, 0, Math.PI*2); ctx.fill();
// Wings
ctx.fillStyle = '#2a4a2a';
ctx.beginPath(); ctx.moveTo(s*0.1, s*0.06); ctx.lineTo(-s*0.65, s*1.0); ctx.lineTo(-s*0.9, s*0.98); ctx.lineTo(-s*0.28, s*0.06); ctx.closePath(); ctx.fill();
ctx.beginPath(); ctx.moveTo(s*0.1, -s*0.06); ctx.lineTo(-s*0.65, -s*1.0); ctx.lineTo(-s*0.9, -s*0.98); ctx.lineTo(-s*0.28, -s*0.06); ctx.closePath(); ctx.fill();
// Cargo door open (showing paratroopers)
ctx.fillStyle = '#1a2a1a'; ctx.fillRect(-s*0.3, -s*0.3, s*0.45, s*0.6);
// Parachute figures falling
ctx.fillStyle = '#ffffff';
for (let i=0;i<2;i++) {
const py = s*0.5 + i*s*0.45;
// Chute
ctx.beginPath(); ctx.arc(-s*0.08 + i*s*0.3, py - s*0.35, s*0.2, Math.PI, 0); ctx.fill();
// Strings
ctx.strokeStyle = '#cccccc'; ctx.lineWidth = 0.8;
ctx.beginPath(); ctx.moveTo(-s*0.28 + i*s*0.3, py-s*0.35); ctx.lineTo(-s*0.08 + i*s*0.3, py-s*0.08); ctx.stroke();
ctx.beginPath(); ctx.moveTo(s*0.12 + i*s*0.3, py-s*0.35); ctx.lineTo(-s*0.08 + i*s*0.3, py-s*0.08); ctx.stroke();
// Trooper
ctx.fillStyle = '#4a6a22'; ctx.beginPath(); ctx.arc(-s*0.08 + i*s*0.3, py, s*0.1, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#4a6a22';
}
// Cockpit
ctx.fillStyle = 'rgba(140,210,255,0.55)'; ctx.beginPath(); ctx.ellipse(s*0.55, 0, s*0.28, s*0.18, 0, 0, Math.PI*2); ctx.fill();
}
function drawEngineer(r) {
const s = r * 1.3;
// Legs
ctx.fillStyle = '#3a5a1a'; ctx.fillRect(-s*0.25, s*0.28, s*0.22, s*0.6);
ctx.fillRect(s*0.04, s*0.28, s*0.22, s*0.6);
// Boots
ctx.fillStyle = '#1a1208'; ctx.fillRect(-s*0.3, s*0.8, s*0.32, s*0.2);
ctx.fillRect(s*0.0, s*0.8, s*0.32, s*0.2);
// Tool vest (tan/yellow)
ctx.fillStyle = '#c8922a'; ctx.fillRect(-s*0.32, -s*0.2, s*0.64, s*0.54);
// Tool pouches
ctx.fillStyle = '#a87020';
ctx.fillRect(-s*0.3, -s*0.1, s*0.16, s*0.2);
ctx.fillRect(s*0.14, -s*0.1, s*0.16, s*0.2);
// Wrench in hand
ctx.fillStyle = '#888'; ctx.fillRect(s*0.32, -s*0.18, s*0.08, s*0.4);
ctx.fillRect(s*0.28, -s*0.18, s*0.18, s*0.08);
ctx.fillRect(s*0.28, -s*0.06, s*0.18, s*0.08);
// Head
ctx.fillStyle = '#c8a070'; ctx.beginPath(); ctx.arc(0, -s*0.42, s*0.24, 0, Math.PI*2); ctx.fill();
// Hard hat (yellow)
ctx.fillStyle = '#ffcc00'; ctx.beginPath(); ctx.arc(0, -s*0.5, s*0.27, Math.PI, 0); ctx.fill();
ctx.fillRect(-s*0.3, -s*0.52, s*0.6, s*0.1);
// Repair aura (green pulse)
ctx.strokeStyle = `rgba(80,255,80,${0.2 + Math.sin(Date.now()*0.004)*0.2})`;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(0, 0, s*1.2, 0, Math.PI*2); ctx.stroke();
}
function drawBombTruck(r) {
const s = r * 1.2;
// Wheels
ctx.fillStyle = '#111';
for (let wx of [-s*0.7, 0, s*0.7]) {
ctx.beginPath(); ctx.arc(wx, s*0.62, s*0.28, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#444'; ctx.beginPath(); ctx.arc(wx, s*0.62, s*0.13, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#111';
}
// Chassis
ctx.fillStyle = '#3a1a0a'; ctx.fillRect(-s*0.9, s*0.18, s*1.8, s*0.48);
// Bomb cargo box — dark and ominous
ctx.fillStyle = '#2a0a0a';
ctx.beginPath(); ctx.roundRect(-s*0.85, -s*0.68, s*1.7, s*0.92, s*0.06); ctx.fill();
// Warning stripes
ctx.fillStyle = '#ffaa00';
for (let i=0;i<5;i++) {
ctx.fillRect(-s*0.85 + i*s*0.34, -s*0.68, s*0.17, s*0.1);
ctx.fillRect(-s*0.85 + i*s*0.34, -s*0.08, s*0.17, s*0.1);
}
// Skull symbol
ctx.fillStyle = '#ff2200'; ctx.font = `bold ${s*0.5}px monospace`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('☠', 0, -s*0.32);
// Danger pulse
const pulse = Math.sin(Date.now()*0.006)*0.5+0.5;
ctx.strokeStyle = `rgba(255,30,0,${0.3+pulse*0.5})`; ctx.lineWidth = 2;
ctx.beginPath(); ctx.roundRect(-s*0.85, -s*0.68, s*1.7, s*0.92, s*0.06); ctx.stroke();
// Cab
ctx.fillStyle = '#4a1a0a'; ctx.beginPath(); ctx.roundRect(s*0.48, -s*0.42, s*0.4, s*0.65, s*0.06); ctx.fill();
ctx.fillStyle = 'rgba(150,210,255,0.4)'; ctx.fillRect(s*0.52, -s*0.35, s*0.32, s*0.28);
}
function drawProjectile(p) {
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
// Glow
ctx.fillStyle = p.color + '55';
ctx.beginPath();
ctx.arc(p.x, p.y, p.size*2, 0, Math.PI*2);
ctx.fill();
}
function drawBase() {
// Start marker
ctx.fillStyle = '#44ff44';
ctx.font = 'bold 11px Orbitron';
ctx.textAlign = 'center';
ctx.fillText('SPAWN', path[0].x + 28, path[0].y - 20);
ctx.fillStyle = '#44ff44';
ctx.fillRect(path[0].x - 8, path[0].y - 8, 16, 16);
// End marker / base
const last = path[path.length-1];
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 11px Orbitron';
ctx.textAlign = 'center';
ctx.fillText('⭐ BASE', last.x - 28, last.y - 20);
ctx.fillStyle = '#ff4444';
ctx.beginPath();
ctx.arc(last.x - 12, last.y, 10, 0, Math.PI*2);
ctx.fill();
}
// ===================== HOVER PREVIEW =====================
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('gameCanvas').addEventListener('mousemove', e => {
if (!selectedTowerType || !gameRunning) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvasW / rect.width;
const scaleY = canvasH / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
const tdef = TOWER_TYPES.find(t => t.id === selectedTowerType);
if (!tdef) return;
// Draw range preview (next frame will overdraw)
// We draw directly for immediate feedback
ctx.beginPath();
ctx.arc(mx, my, tdef.range, 0, Math.PI*2);
ctx.strokeStyle = onPath(mx,my) ? 'rgba(255,50,50,0.5)' : 'rgba(255,215,0,0.35)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = onPath(mx,my) ? 'rgba(255,50,50,0.08)' : 'rgba(255,215,0,0.06)';
ctx.fill();
});
});
// ===================== GAME OVER / WIN =====================
function gameOver() {
gameRunning = false;
const ov = document.getElementById('overlay');
ov.style.display = 'flex';
ov.innerHTML = `
<h2>💀 MISSION FAILED</h2>
<p>The base has been overrun!</p>
<p style="color:#ffaa44">Wave Reached: ${wave} | Score: ${score} | Kills: ${kills}</p>
<button onclick="startGame()">🔄 TRY AGAIN</button>
`;
}
// ===================== MAIN LOOP =====================
let lastTime = 0;
function gameLoop(ts = 0) {
const dt = Math.min(ts - lastTime, 32);
lastTime = ts;
if (gameRunning) {
updateSpawning(dt);
updateEnemies(dt);
updateTowers(dt);
updateProjectiles(dt);
updateParticles(dt);
updateNapalm(dt);
}
draw();
animFrame = requestAnimationFrame(gameLoop);
}
// ===================== HELPERS =====================
function shadeColor(col, pct) {
let r = parseInt(col.slice(1,3),16), g = parseInt(col.slice(3,5),16), b = parseInt(col.slice(5,7),16);
r = Math.min(255,Math.max(0,r+pct)); g = Math.min(255,Math.max(0,g+pct)); b = Math.min(255,Math.max(0,b+pct));
return '#'+[r,g,b].map(v=>v.toString(16).padStart(2,'0')).join('');
}
// ===================== RESIZE =====================
window.addEventListener('resize', () => {
if (!gameRunning) return;
const oldW = canvasW, oldH = canvasH;
initCanvas();
const scaleX = canvasW / oldW;
const scaleY = canvasH / oldH;
// Scale all tower positions to match new canvas size
towers.forEach(t => {
t.x = Math.round(t.x * scaleX);
t.y = Math.round(t.y * scaleY);
});
// Rebuild path for new dimensions
path = buildPath(canvasW, canvasH);
});
// ===================== BOOT =====================
window.addEventListener('DOMContentLoaded', () => {
initCanvas();
buildTowerButtons();
// Attach canvas click after DOM ready
document.getElementById('gameCanvas').addEventListener('click', handleClick);
gameLoop();
});
</script>
</body>
</html>Game Source: Army Tower Defense
Creator: MagicStar57
Libraries: none
Complexity: complex (3745 lines, 126.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: army-tower-defense-magicstar57" to link back to the original. Then publish at arcadelab.ai/publish.