Tower Defense - Day 12
Strategy GameA complete tower defense game built with vanilla JavaScript and Canvas - featuring pathfinding, multiple tower types, projectile systems, and wave management.
// Core game constants
const TILE_SIZE = 40;
const GRID_WIDTH = 16;
const GRID_HEIGHT = 12;
// Tower type definitions
const TOWER_TYPES = {
arrow: { cost: 50, damage: 15, range: 100, fireRate: 30 },
cannon: { cost: 100, damage: 50, range: 80, splash: 30 },
ice: { cost: 75, damage: 8, range: 90, slow: 0.5 },
lightning: { cost: 150, damage: 35, range: 120, chain: 3 }
};
Each tower type has a unique mechanic: Arrow for DPS, Cannon for splash, Ice for crowd control, Lightning for chain damage. This creates strategic depth.
The map uses a 2D array where different values represent different tile types. Enemies follow a pre-calculated path from start to end.
// Map tiles: 0=grass, 1=path, 2=start, 3=end
const MAP = [
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[2,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0],
// ... (snaking path creates longer enemy travel time)
[0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,3]
];
function calculatePath() {
PATH = [];
let visited = new Set();
let current = null;
// Find start tile (value 2)
for (let y = 0; y < GRID_HEIGHT; y++) {
for (let x = 0; x < GRID_WIDTH; x++) {
if (MAP[y][x] === 2) {
current = { x, y };
PATH.push({
x: x * TILE_SIZE + TILE_SIZE/2,
y: y * TILE_SIZE + TILE_SIZE/2
});
visited.add(`${x},${y}`);
break;
}
}
}
// Follow path using 4-directional flood fill
const directions = [[1,0], [-1,0], [0,1], [0,-1]];
while (current) {
let found = false;
for (let [dx, dy] of directions) {
let nx = current.x + dx;
let ny = current.y + dy;
let key = `${nx},${ny}`;
if (isValidTile(nx, ny) && !visited.has(key) &&
(MAP[ny][nx] === 1 || MAP[ny][nx] === 3)) {
visited.add(key);
current = { x: nx, y: ny };
PATH.push({
x: nx * TILE_SIZE + TILE_SIZE/2,
y: ny * TILE_SIZE + TILE_SIZE/2
});
found = true;
break;
}
}
if (!found) break;
}
}
Each tower scans for enemies within range and targets the closest one. The tower rotates to face its target before firing.
class Tower {
update() {
if (this.cooldown > 0) {
this.cooldown--;
return;
}
// Find closest enemy in range
let closest = null;
let closestDist = Infinity;
for (let enemy of enemies) {
const dx = enemy.x - this.x;
const dy = enemy.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < this.config.range && dist < closestDist) {
closest = enemy;
closestDist = dist;
}
}
if (closest) {
// Rotate towards target
this.angle = Math.atan2(
closest.y - this.y,
closest.x - this.x
);
this.shoot(closest);
}
}
}
This uses "closest first" targeting. Other strategies include: "first" (furthest along path), "strongest" (highest HP), or "weakest" (lowest HP).
Projectiles are homing - they track their target's position and deal damage on impact. Different tower types have unique damage effects.
function updateProjectiles() {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
// Update target position for homing
if (p.target && enemies.includes(p.target)) {
p.targetX = p.target.x;
p.targetY = p.target.y;
}
// Move towards target
const dx = p.targetX - p.x;
const dy = p.targetY - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < p.speed) {
// Hit! Apply damage based on tower type
handleProjectileHit(p);
projectiles.splice(i, 1);
} else {
p.x += (dx / dist) * p.speed;
p.y += (dy / dist) * p.speed;
}
}
}
function handleProjectileHit(p) {
if (p.splash) {
// Cannon: Splash damage in radius
for (let enemy of enemies) {
const dist = distance(enemy, p);
if (dist < p.splash) {
enemy.takeDamage(p.damage);
}
}
}
if (p.slow) {
// Ice: Apply slow debuff
p.target.takeDamage(p.damage, p.slow);
// slow = 0.5 means 50% speed reduction
}
if (p.chain) {
// Lightning: Chain to nearby enemies
let nextTarget = findNearestUnhit(p, 80);
if (nextTarget && p.chainHit.length < p.chain) {
p.chainHit.push(nextTarget);
createChainProjectile(p, nextTarget);
}
}
}
Chain lightning creates new projectiles for each jump. Use a chainHit array to prevent hitting the same enemy twice and avoid infinite loops.
Each wave spawns enemies at intervals with increasing difficulty. Higher waves introduce new enemy types and more enemies.
function getEnemyTypes(waveNum) {
const baseHealth = 30 + waveNum * 15;
const baseSpeed = 1 + waveNum * 0.1;
const types = [
// Normal - always available
{ health: baseHealth, speed: baseSpeed,
color: '#ff6b6b', radius: 10, reward: 10 }
];
if (waveNum >= 3) {
// Fast - low HP, high speed
types.push({
health: baseHealth * 0.6,
speed: baseSpeed * 1.5,
color: '#ffa500', radius: 8, reward: 15
});
}
if (waveNum >= 5) {
// Tank - high HP, slow
types.push({
health: baseHealth * 2,
speed: baseSpeed * 0.6,
color: '#8b4513', radius: 14, reward: 25
});
}
return types;
}
let spawnTimer = 0;
let enemiesToSpawn = 0;
function updateSpawning() {
if (enemiesToSpawn > 0) {
spawnTimer++;
if (spawnTimer >= 40) { // Spawn every 40 frames
const type = currentEnemyTypes[
Math.floor(Math.random() * currentEnemyTypes.length)
];
enemies.push(new Enemy(type));
enemiesToSpawn--;
spawnTimer = 0;
}
} else if (enemies.length === 0 && waveInProgress) {
// Wave complete!
waveInProgress = false;
wave++;
gold += 25 + wave * 5; // Wave completion bonus
}
}
Clicking an existing tower upgrades it, increasing damage and fire rate. The economy balances tower costs with enemy rewards.
canvas.addEventListener('click', (e) => {
const gridX = Math.floor(x / TILE_SIZE);
const gridY = Math.floor(y / TILE_SIZE);
// Check for existing tower
const existingTower = towers.find(t =>
Math.floor(t.x / TILE_SIZE) === gridX &&
Math.floor(t.y / TILE_SIZE) === gridY
);
if (existingTower) {
// Upgrade cost scales with level
const upgradeCost = existingTower.config.cost * existingTower.level;
if (gold >= upgradeCost && existingTower.level < 3) {
gold -= upgradeCost;
existingTower.level++;
// Visual feedback
addParticles(existingTower.x, existingTower.y, '#ffd700', 20);
}
return;
}
// Otherwise, place new tower on grass
if (selectedTower && MAP[gridY][gridX] === 0) {
if (gold >= TOWER_TYPES[selectedTower].cost) {
gold -= TOWER_TYPES[selectedTower].cost;
towers.push(new Tower(gridX, gridY, selectedTower));
}
}
});
class Tower {
shoot(target) {
projectiles.push({
damage: this.config.damage * this.level, // +100% per level
// ... other properties
});
// Fire rate also improves
this.cooldown = Math.floor(
this.config.fireRate / this.level
);
}
draw() {
// Show stars for upgraded towers
if (this.level > 1) {
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 10px JetBrains Mono';
ctx.fillText(
'★'.repeat(this.level - 1),
this.x, this.y - 18
);
}
}
}
Upgrade cost = base cost × current level. Level 2 costs 1x base, Level 3 costs 2x base. This makes upgrades progressively more expensive but worthwhile for strong positions.