modded-tower-defense / index.html
jamesjun's picture
Add 2 files
f6abe44 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tower Defense Game</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
#game-container {
position: relative;
width: 800px;
height: 600px;
background-color: #2d3748;
overflow: hidden;
}
.cell {
width: 40px;
height: 40px;
position: absolute;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.path {
background-color: #4a5568;
}
.tower {
width: 36px;
height: 36px;
border-radius: 50%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
cursor: pointer;
z-index: 10;
}
.tower::after {
content: '';
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
opacity: 0.2;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
.tower-1 {
background-color: #4299e1;
}
.tower-1::after {
background-color: #4299e1;
}
.tower-2 {
background-color: #f56565;
}
.tower-2::after {
background-color: #f56565;
}
.tower-3 {
background-color: #68d391;
}
.tower-3::after {
background-color: #68d391;
}
.tower-4 {
background-color: #9f7aea;
}
.tower-4::after {
background-color: #9f7aea;
}
.tower-5 {
background-color: #ed8936;
}
.tower-5::after {
background-color: #ed8936;
}
.tower-6 {
background-color: #f6e05e;
}
.tower-6::after {
background-color: #f6e05e;
}
.enemy {
width: 30px;
height: 30px;
border-radius: 50%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
z-index: 5;
transition: left 0.1s linear, top 0.1s linear;
}
.enemy-1 {
background-color: #ecc94b;
}
.enemy-2 {
background-color: #ed8936;
}
.enemy-3 {
background-color: #9f7aea;
}
.projectile {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
z-index: 8;
}
.projectile-1 {
background-color: #4299e1;
}
.projectile-2 {
background-color: #f56565;
}
.projectile-3 {
background-color: #68d391;
}
.projectile-4 {
background-color: #9f7aea;
}
.projectile-5 {
background-color: #ed8936;
}
.projectile-6 {
background-color: #f6e05e;
}
.range-indicator {
position: absolute;
border: 2px dashed rgba(255, 255, 255, 0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 1;
}
#tower-menu {
position: absolute;
background-color: rgba(26, 32, 44, 0.9);
border-radius: 8px;
padding: 10px;
display: none;
z-index: 100;
min-width: 180px;
}
.health-bar {
height: 4px;
background-color: #48bb78;
position: absolute;
top: -8px;
left: 0;
width: 100%;
}
#game-over {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 200;
}
.splash-effect {
position: absolute;
width: 30px;
height: 30px;
border-radius: 50%;
opacity: 0.5;
animation: splash 0.5s ease-out;
transform: translate(-50%, -50%);
z-index: 7;
}
@keyframes splash {
0% { transform: scale(0.1); opacity: 0.8; }
100% { transform: scale(3); opacity: 0; }
}
.frost-effect {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(147, 197, 253, 0.3);
border-radius: 50%;
z-index: 6;
animation: frost 0.5s ease-out;
}
@keyframes frost {
0% { transform: scale(0.1); opacity: 0.8; }
100% { transform: scale(3); opacity: 0; }
}
.lightning-effect {
position: absolute;
width: 10px;
height: 40px;
background-color: #f6e05e;
z-index: 7;
animation: lightning 0.2s linear;
}
@keyframes lightning {
0% { transform: scaleY(0.1); opacity: 0.8; }
50% { transform: scaleY(1); opacity: 1; }
100% { transform: scaleY(0.1); opacity: 0; }
}
</style>
</head>
<body class="bg-gray-900 text-white flex flex-col items-center p-4">
<h1 class="text-3xl font-bold mb-4">Tower Defense</h1>
<div class="flex justify-between w-full max-w-4xl mb-4">
<div class="flex space-x-4">
<div class="bg-gray-800 p-3 rounded-lg">
<span class="font-bold">Wave:</span> <span id="wave">1</span>
</div>
<div class="bg-gray-800 p-3 rounded-lg">
<span class="font-bold">Lives:</span> <span id="lives">20</span>
</div>
<div class="bg-gray-800 p-3 rounded-lg">
<span class="font-bold">Money:</span> $<span id="money">100</span>
</div>
</div>
<div class="flex space-x-2">
<button id="start-wave" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg font-bold">
Start Wave
</button>
<button id="restart-wave" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg font-bold">
Restart Wave
</button>
</div>
</div>
<div id="game-container" class="rounded-lg shadow-xl">
<!-- Game elements will be added here dynamically -->
</div>
<div class="mt-4 w-full max-w-4xl">
<h2 class="text-xl font-bold mb-2">Tower Shop</h2>
<div class="grid grid-cols-3 gap-4">
<div class="bg-gray-800 p-4 rounded-lg cursor-pointer hover:bg-gray-700 tower-shop-item" data-type="1">
<div class="flex items-center mb-2">
<div class="tower-1 w-8 h-8 mr-2 flex items-center justify-center">
<i class="fas fa-bolt text-white"></i>
</div>
<h3 class="font-bold">Lightning Tower</h3>
</div>
<p class="text-sm text-gray-300">Cost: $50</p>
<p class="text-sm text-gray-300">Damage: 10</p>
<p class="text-sm text-gray-300">Range: 120px</p>
<p class="text-xs text-blue-300">Fast attacker</p>
</div>
<div class="bg-gray-800 p-4 rounded-lg cursor-pointer hover:bg-gray-700 tower-shop-item" data-type="2">
<div class="flex items-center mb-2">
<div class="tower-2 w-8 h-8 mr-2 flex items-center justify-center">
<i class="fas fa-fire text-white"></i>
</div>
<h3 class="font-bold">Flame Tower</h3>
</div>
<p class="text-sm text-gray-300">Cost: $100</p>
<p class="text-sm text-gray-300">Damage: 20</p>
<p class="text-sm text-gray-300">Range: 90px</p>
<p class="text-xs text-red-300">Splash damage</p>
</div>
<div class="bg-gray-800 p-4 rounded-lg cursor-pointer hover:bg-gray-700 tower-shop-item" data-type="3">
<div class="flex items-center mb-2">
<div class="tower-3 w-8 h-8 mr-2 flex items-center justify-center">
<i class="fas fa-leaf text-white"></i>
</div>
<h3 class="font-bold">Nature Tower</h3>
</div>
<p class="text-sm text-gray-300">Cost: $100</p>
<p class="text-sm text-gray-300">Damage: 15</p>
<p class="text-sm text-gray-300">Range: 150px</p>
<p class="text-xs text-green-300">Poison effect</p>
</div>
<div class="bg-gray-800 p-4 rounded-lg cursor-pointer hover:bg-gray-700 tower-shop-item" data-type="4">
<div class="flex items-center mb-2">
<div class="tower-4 w-8 h-8 mr-2 flex items-center justify-center">
<i class="fas fa-snowflake text-white"></i>
</div>
<h3 class="font-bold">Frost Tower</h3>
</div>
<p class="text-sm text-gray-300">Cost: $120</p>
<p class="text-sm text-gray-300">Damage: 8</p>
<p class="text-sm text-gray-300">Range: 130px</p>
<p class="text-xs text-purple-300">Slows enemies</p>
</div>
<div class="bg-gray-800 p-4 rounded-lg cursor-pointer hover:bg-gray-700 tower-shop-item" data-type="5">
<div class="flex items-center mb-2">
<div class="tower-5 w-8 h-8 mr-2 flex items-center justify-center">
<i class="fas fa-bomb text-white"></i>
</div>
<h3 class="font-bold">Bomb Tower</h3>
</div>
<p class="text-sm text-gray-300">Cost: $150</p>
<p class="text-sm text-gray-300">Damage: 40</p>
<p class="text-sm text-gray-300">Range: 70px</p>
<p class="text-xs text-orange-300">Area damage</p>
</div>
<div class="bg-gray-800 p-4 rounded-lg cursor-pointer hover:bg-gray-700 tower-shop-item" data-type="6">
<div class="flex items-center mb-2">
<div class="tower-6 w-8 h-8 mr-2 flex items-center justify-center">
<i class="fas fa-bolt-lightning text-white"></i>
</div>
<h3 class="font-bold">Tesla Tower</h3>
</div>
<p class="text-sm text-gray-300">Cost: $200</p>
<p class="text-sm text-gray-300">Damage: 25</p>
<p class="text-sm text-gray-300">Range: 110px</p>
<p class="text-xs text-yellow-300">Chains to enemies</p>
</div>
</div>
</div>
<div id="tower-menu" class="text-sm">
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold" id="tower-menu-title">Tower</h3>
<button id="sell-tower" class="bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-xs">Sell</button>
</div>
<div class="mb-2">
<p>Level: <span id="tower-level">1</span></p>
<p>Damage: <span id="tower-damage">10</span></p>
<p>Range: <span id="tower-range">120</span>px</p>
<p id="tower-special" class="text-xs"></p>
</div>
<button id="upgrade-tower" class="bg-blue-600 hover:bg-blue-700 w-full py-1 rounded">Upgrade ($<span id="upgrade-cost">50</span>)</button>
</div>
<div id="game-over" class="text-center">
<h2 class="text-4xl font-bold mb-4">Game Over</h2>
<p class="text-xl mb-6">You survived <span id="final-wave">0</span> waves!</p>
<button id="restart-game" class="bg-green-600 hover:bg-green-700 px-6 py-3 rounded-lg font-bold text-lg">
Play Again
</button>
</div>
<script>
// Game state
const gameState = {
gridWidth: 20,
gridHeight: 15,
cellSize: 40,
money: 100,
lives: 20,
wave: 1,
gameActive: false,
placingTower: false,
towerType: null,
selectedTower: null,
enemies: [],
towers: [],
projectiles: [],
effects: [],
path: [
{x: 0, y: 7},
{x: 5, y: 7},
{x: 5, y: 3},
{x: 10, y: 3},
{x: 10, y: 10},
{x: 15, y: 10},
{x: 15, y: 5},
{x: 20, y: 5}
],
enemySpawnInterval: null,
gameLoopInterval: null,
currentWaveEnemies: 0,
totalWaveEnemies: 0
};
// Tower types
const towerTypes = {
1: {
name: "Lightning Tower",
cost: 50,
damage: 10,
range: 120,
color: "blue",
upgradeCost: 50,
projectileSpeed: 12,
cooldown: 20, // Frames between shots
icon: "fa-bolt",
special: "Fast attack speed",
effect: "lightning"
},
2: {
name: "Flame Tower",
cost: 100,
damage: 20,
range: 90,
color: "red",
upgradeCost: 75,
projectileSpeed: 6,
cooldown: 45,
icon: "fa-fire",
special: "Splash damage to nearby enemies",
effect: "flame",
splashRadius: 40
},
3: {
name: "Nature Tower",
cost: 100,
damage: 15,
range: 150,
color: "green",
upgradeCost: 60,
projectileSpeed: 8,
cooldown: 40,
icon: "fa-leaf",
special: "Applies poison over time",
effect: "poison"
},
4: {
name: "Frost Tower",
cost: 120,
damage: 8,
range: 130,
color: "purple",
upgradeCost: 60,
projectileSpeed: 10,
cooldown: 35,
icon: "fa-snowflake",
special: "Slows enemies by 30% for 2s",
effect: "frost"
},
5: {
name: "Bomb Tower",
cost: 150,
damage: 40,
range: 70,
color: "orange",
upgradeCost: 90,
projectileSpeed: 4,
cooldown: 60,
icon: "fa-bomb",
special: "High damage in an area",
effect: "explosion",
splashRadius: 60
},
6: {
name: "Tesla Tower",
cost: 200,
damage: 25,
range: 110,
color: "yellow",
upgradeCost: 100,
projectileSpeed: 15,
cooldown: 50,
icon: "fa-bolt-lightning",
special: "Chains damage to 2 nearby enemies",
effect: "chain"
}
};
// Enemy types
const enemyTypes = [
{
health: 50,
speed: 1.5,
reward: 10,
color: "yellow"
},
{
health: 100,
speed: 1,
reward: 20,
color: "orange"
},
{
health: 200,
speed: 0.7,
reward: 40,
color: "purple"
}
];
// DOM elements
const gameContainer = document.getElementById('game-container');
const waveDisplay = document.getElementById('wave');
const livesDisplay = document.getElementById('lives');
const moneyDisplay = document.getElementById('money');
const startWaveBtn = document.getElementById('start-wave');
const restartWaveBtn = document.getElementById('restart-wave');
const towerShopItems = document.querySelectorAll('.tower-shop-item');
const towerMenu = document.getElementById('tower-menu');
const towerMenuTitle = document.getElementById('tower-menu-title');
const towerLevel = document.getElementById('tower-level');
const towerDamage = document.getElementById('tower-damage');
const towerRange = document.getElementById('tower-range');
const towerSpecial = document.getElementById('tower-special');
const upgradeCost = document.getElementById('upgrade-cost');
const upgradeBtn = document.getElementById('upgrade-tower');
const sellBtn = document.getElementById('sell-tower');
const gameOverScreen = document.getElementById('game-over');
const finalWaveDisplay = document.getElementById('final-wave');
const restartBtn = document.getElementById('restart-game');
// Initialize game
function initGame() {
// Clear previous game state
gameContainer.innerHTML = '';
gameState.enemies = [];
gameState.towers = [];
gameState.projectiles = [];
gameState.effects = [];
// Reset game state
gameState.money = 100;
gameState.lives = 20;
gameState.wave = 1;
gameState.gameActive = false;
gameState.placingTower = false;
gameState.towerType = null;
gameState.selectedTower = null;
gameState.currentWaveEnemies = 0;
gameState.totalWaveEnemies = 0;
// Update UI
updateUI();
// Create grid
createGrid();
// Create path
createPath();
// Hide game over screen
gameOverScreen.style.display = 'none';
// Enable buttons
startWaveBtn.disabled = false;
restartWaveBtn.disabled = false;
// Start game loop
if (gameState.gameLoopInterval) {
clearInterval(gameState.gameLoopInterval);
}
gameState.gameLoopInterval = setInterval(gameLoop, 16); // ~60fps
}
// Create grid
function createGrid() {
for (let y = 0; y < gameState.gridHeight; y++) {
for (let x = 0; x < gameState.gridWidth; x++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.style.left = `${x * gameState.cellSize}px`;
cell.style.top = `${y * gameState.cellSize}px`;
cell.dataset.x = x;
cell.dataset.y = y;
// Add click event for tower placement
cell.addEventListener('click', () => {
if (gameState.placingTower) {
placeTower(x, y);
}
});
gameContainer.appendChild(cell);
}
}
}
// Create path
function createPath() {
// Draw path cells
for (let i = 0; i < gameState.path.length - 1; i++) {
const start = gameState.path[i];
const end = gameState.path[i + 1];
// Horizontal path
if (start.y === end.y) {
const direction = start.x < end.x ? 1 : -1;
for (let x = start.x; x !== end.x; x += direction) {
if (x >= 0 && x < gameState.gridWidth && start.y >= 0 && start.y < gameState.gridHeight) {
const cell = document.querySelector(`.cell[data-x="${x}"][data-y="${start.y}"]`);
if (cell) cell.classList.add('path');
}
}
}
// Vertical path
else if (start.x === end.x) {
const direction = start.y < end.y ? 1 : -1;
for (let y = start.y; y !== end.y; y += direction) {
if (start.x >= 0 && start.x < gameState.gridWidth && y >= 0 && y < gameState.gridHeight) {
const cell = document.querySelector(`.cell[data-x="${start.x}"][data-y="${y}"]`);
if (cell) cell.classList.add('path');
}
}
}
}
// Add the last cell
const last = gameState.path[gameState.path.length - 1];
if (last.x >= 0 && last.x < gameState.gridWidth && last.y >= 0 && last.y < gameState.gridHeight) {
const cell = document.querySelector(`.cell[data-x="${last.x}"][data-y="${last.y}"]`);
if (cell) cell.classList.add('path');
}
}
// Place tower
function placeTower(x, y) {
// Check if cell is empty and not on path
const cell = document.querySelector(`.cell[data-x="${x}"][data-y="${y}"]`);
if (!cell || cell.classList.contains('path') || cell.classList.contains('has-tower')) {
return;
}
// Check if player has enough money
const towerCost = towerTypes[gameState.towerType].cost;
if (gameState.money < towerCost) {
alert('Not enough money!');
return;
}
// Deduct money
gameState.money -= towerCost;
updateUI();
// Create tower
const tower = {
type: gameState.towerType,
x: x,
y: y,
level: 1,
damage: towerTypes[gameState.towerType].damage,
range: towerTypes[gameState.towerType].range,
cooldown: 0,
maxCooldown: towerTypes[gameState.towerType].cooldown,
effect: towerTypes[gameState.towerType].effect,
splashRadius: towerTypes[gameState.towerType].splashRadius || 0
};
gameState.towers.push(tower);
// Create tower element
const towerElement = document.createElement('div');
towerElement.className = `tower tower-${tower.type}`;
towerElement.style.left = `${x * gameState.cellSize + 2}px`;
towerElement.style.top = `${y * gameState.cellSize + 2}px`;
towerElement.dataset.index = gameState.towers.length - 1;
// Add icon to tower
const icon = document.createElement('i');
icon.className = `fas ${towerTypes[tower.type].icon}`;
towerElement.appendChild(icon);
// Add click event for tower selection
towerElement.addEventListener('click', (e) => {
e.stopPropagation();
selectTower(gameState.towers.length - 1);
});
gameContainer.appendChild(towerElement);
// Mark cell as occupied
cell.classList.add('has-tower');
// Exit tower placement mode
gameState.placingTower = false;
gameState.towerType = null;
// Remove range indicator if it exists
const rangeIndicator = document.querySelector('.range-indicator');
if (rangeIndicator) rangeIndicator.remove();
}
// Select tower
function selectTower(index) {
// Close menu if clicking the same tower
if (gameState.selectedTower === index && towerMenu.style.display === 'block') {
towerMenu.style.display = 'none';
gameState.selectedTower = null;
// Remove range indicator
const rangeIndicator = document.querySelector('.range-indicator');
if (rangeIndicator) rangeIndicator.remove();
return;
}
gameState.selectedTower = index;
const tower = gameState.towers[index];
// Update tower menu
towerMenuTitle.textContent = towerTypes[tower.type].name;
towerLevel.textContent = tower.level;
towerDamage.textContent = tower.damage;
towerRange.textContent = tower.range;
towerSpecial.textContent = towerTypes[tower.type].special;
upgradeCost.textContent = towerTypes[tower.type].upgradeCost * tower.level;
// Position menu near tower
const menuX = tower.x * gameState.cellSize + gameState.cellSize;
const menuY = tower.y * gameState.cellSize;
// Adjust if near right edge
if (menuX + 180 > gameContainer.offsetWidth) {
towerMenu.style.left = `${tower.x * gameState.cellSize - 180}px`;
} else {
towerMenu.style.left = `${menuX}px`;
}
// Adjust if near bottom edge
if (menuY + 150 > gameContainer.offsetHeight) {
towerMenu.style.top = `${tower.y * gameState.cellSize - 100}px`;
} else {
towerMenu.style.top = `${menuY}px`;
}
towerMenu.style.display = 'block';
// Show range indicator
const rangeIndicator = document.createElement('div');
rangeIndicator.className = 'range-indicator';
rangeIndicator.style.width = `${tower.range * 2}px`;
rangeIndicator.style.height = `${tower.range * 2}px`;
rangeIndicator.style.left = `${tower.x * gameState.cellSize + gameState.cellSize / 2}px`;
rangeIndicator.style.top = `${tower.y * gameState.cellSize + gameState.cellSize / 2}px`;
gameContainer.appendChild(rangeIndicator);
}
// Upgrade tower
function upgradeTower() {
if (gameState.selectedTower === null) return;
const tower = gameState.towers[gameState.selectedTower];
const cost = towerTypes[tower.type].upgradeCost * tower.level;
if (gameState.money >= cost) {
gameState.money -= cost;
tower.level += 1;
tower.damage = Math.floor(towerTypes[tower.type].damage * (1 + (tower.level - 1) * 0.5));
tower.range = Math.floor(towerTypes[tower.type].range * (1 + (tower.level - 1) * 0.2));
if (tower.splashRadius > 0) {
tower.splashRadius = Math.floor(tower.splashRadius * (1 + (tower.level - 1) * 0.1));
}
// Update tower menu
towerLevel.textContent = tower.level;
towerDamage.textContent = tower.damage;
towerRange.textContent = tower.range;
upgradeCost.textContent = towerTypes[tower.type].upgradeCost * tower.level;
// Update range indicator
const rangeIndicator = document.querySelector('.range-indicator');
if (rangeIndicator) {
rangeIndicator.style.width = `${tower.range * 2}px`;
rangeIndicator.style.height = `${tower.range * 2}px`;
}
updateUI();
} else {
alert('Not enough money!');
}
}
// Sell tower
function sellTower() {
if (gameState.selectedTower === null) return;
const tower = gameState.towers[gameState.selectedTower];
const refund = Math.floor(towerTypes[tower.type].cost * 0.7 * tower.level);
gameState.money += refund;
// Remove tower element
const towerElement = document.querySelector(`.tower[data-index="${gameState.selectedTower}"]`);
towerElement.remove();
// Remove range indicator
const rangeIndicator = document.querySelector('.range-indicator');
if (rangeIndicator) rangeIndicator.remove();
// Mark cell as empty
const cell = document.querySelector(`.cell[data-x="${tower.x}"][data-y="${tower.y}"]`);
cell.classList.remove('has-tower');
// Remove tower from array
gameState.towers.splice(gameState.selectedTower, 1);
// Update all tower elements' data-index attributes
document.querySelectorAll('.tower').forEach((el, index) => {
el.dataset.index = index;
});
// Close menu
towerMenu.style.display = 'none';
gameState.selectedTower = null;
updateUI();
}
// Start wave
function startWave() {
if (gameState.gameActive) return;
gameState.gameActive = true;
startWaveBtn.disabled = true;
restartWaveBtn.disabled = true;
gameState.currentWaveEnemies = 0;
// Calculate total enemies for this wave
gameState.totalWaveEnemies = Math.floor(5 + gameState.wave * 1.5);
// Spawn enemies
let enemyType = Math.min(Math.floor(gameState.wave / 5), 2); // Stronger enemies every 5 waves
gameState.enemySpawnInterval = setInterval(() => {
if (gameState.currentWaveEnemies >= gameState.totalWaveEnemies) {
clearInterval(gameState.enemySpawnInterval);
gameState.enemySpawnInterval = null;
return;
}
spawnEnemy(enemyType);
gameState.currentWaveEnemies++;
// Every 3 enemies, increase type if possible
if (gameState.currentWaveEnemies % 3 === 0 && enemyType < 2) {
enemyType++;
}
}, 1000);
}
// Restart current wave
function restartWave() {
// Clear existing enemies and projectiles
gameState.enemies.forEach((enemy, index) => {
const enemyElement = document.querySelectorAll('.enemy')[index];
if (enemyElement) enemyElement.remove();
});
gameState.enemies = [];
gameState.projectiles.forEach((projectile, index) => {
const projectileElement = document.querySelectorAll('.projectile')[index];
if (projectileElement) projectileElement.remove();
});
gameState.projectiles = [];
// Clear any active spawn interval
if (gameState.enemySpawnInterval) {
clearInterval(gameState.enemySpawnInterval);
gameState.enemySpawnInterval = null;
}
// Reset wave state
gameState.gameActive = false;
gameState.currentWaveEnemies = 0;
// Enable start wave button
startWaveBtn.disabled = false;
restartWaveBtn.disabled = false;
}
// Spawn enemy
function spawnEnemy(type) {
const enemy = {
type: type,
health: enemyTypes[type].health,
maxHealth: enemyTypes[type].health,
speed: enemyTypes[type].speed,
reward: enemyTypes[type].reward,
pathIndex: 0,
x: gameState.path[0].x * gameState.cellSize + gameState.cellSize / 2,
y: gameState.path[0].y * gameState.cellSize + gameState.cellSize / 2,
statusEffects: []
};
gameState.enemies.push(enemy);
// Create enemy element
const enemyElement = document.createElement('div');
enemyElement.className = `enemy enemy-${type + 1}`;
enemyElement.style.left = `${enemy.x - 15}px`;
enemyElement.style.top = `${enemy.y - 15}px`;
// Add health bar
const healthBar = document.createElement('div');
healthBar.className = 'health-bar';
enemyElement.appendChild(healthBar);
gameContainer.appendChild(enemyElement);
}
// Create effect
function createEffect(x, y, type) {
const effect = { x, y, type, frame: 0, maxFrames: 30 };
gameState.effects.push(effect);
let effectElement;
switch(type) {
case 'flame':
effectElement = document.createElement('div');
effectElement.className = 'splash-effect';
effectElement.style.backgroundColor = '#f56565';
break;
case 'frost':
effectElement = document.createElement('div');
effectElement.className = 'frost-effect';
break;
case 'lightning':
effectElement = document.createElement('div');
effectElement.className = 'lightning-effect';
break;
case 'explosion':
effectElement = document.createElement('div');
effectElement.className = 'splash-effect';
effectElement.style.backgroundColor = '#ed8936';
effect.maxFrames = 20;
break;
case 'poison':
effectElement = document.createElement('div');
effectElement.className = 'splash-effect';
effectElement.style.backgroundColor = '#68d391';
effect.maxFrames = 40;
break;
default:
effectElement = document.createElement('div');
effectElement.className = 'splash-effect';
effectElement.style.backgroundColor = '#4299e1';
}
effectElement.style.left = `${x}px`;
effectElement.style.top = `${y}px`;
gameContainer.appendChild(effectElement);
return effectElement;
}
// Apply status effect
function applyStatusEffect(enemy, effectType) {
if (!enemy.statusEffects.includes(effectType)) {
enemy.statusEffects.push(effectType);
switch(effectType) {
case 'frost':
enemy.speed *= 0.7; // Slow by 30%
break;
case 'poison':
// Poison effect will be handled in game loop
break;
}
// Remove effect after duration
setTimeout(() => {
const index = enemy.statusEffects.indexOf(effectType);
if (index !== -1) {
enemy.statusEffects.splice(index, 1);
// Restore original speed for frost
if (effectType === 'frost') {
const originalSpeed = enemyTypes[enemy.type].speed;
enemy.speed = originalSpeed;
}
}
}, 2000); // 2 second duration
}
}
// Game loop
function gameLoop() {
// Process effects
for (let i = gameState.effects.length - 1; i >= 0; i--) {
const effect = gameState.effects[i];
effect.frame++;
if (effect.frame >= effect.maxFrames) {
gameState.effects.splice(i, 1);
}
}
// Move enemies and handle status effects
gameState.enemies.forEach((enemy, enemyIndex) => {
// Handle poison damage
if (enemy.statusEffects.includes('poison')) {
enemy.health -= 1;
// Update health bar
const enemyElement = document.querySelectorAll('.enemy')[enemyIndex];
if (enemyElement) {
const healthBar = enemyElement.querySelector('.health-bar');
healthBar.style.width = `${(enemy.health / enemy.maxHealth) * 100}%`;
if (enemy.health <= 0) {
// Enemy died
gameState.money += enemy.reward;
updateUI();
removeEnemy(enemyIndex);
}
}
}
// Skip if enemy is dead
if (enemy.health <= 0) return;
const target = gameState.path[enemy.pathIndex + 1];
if (!target) {
// Enemy reached the end
gameState.lives--;
updateUI();
removeEnemy(enemyIndex);
if (gameState.lives <= 0) {
gameOver();
}
return;
}
const targetX = target.x * gameState.cellSize + gameState.cellSize / 2;
const targetY = target.y * gameState.cellSize + gameState.cellSize / 2;
const dx = targetX - enemy.x;
const dy = targetY - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 2) {
// Reached current target, move to next
enemy.pathIndex++;
} else {
// Move toward target (accounting for speed reductions)
const effectiveSpeed = enemy.speed * (enemy.statusEffects.includes('frost') ? 0.7 : 1);
enemy.x += (dx / distance) * effectiveSpeed;
enemy.y += (dy / distance) * effectiveSpeed;
// Update enemy element position
const enemyElement = document.querySelectorAll('.enemy')[enemyIndex];
if (enemyElement) {
enemyElement.style.left = `${enemy.x - 15}px`;
enemyElement.style.top = `${enemy.y - 15}px`;
// Show frost visual effect if slowed
if (enemy.statusEffects.includes('frost')) {
if (!enemyElement.querySelector('.frost-visual')) {
const frostEffect = document.createElement('div');
frostEffect.className = 'absolute frost-visual';
frostEffect.style.width = '30px';
frostEffect.style.height = '30px';
frostEffect.style.borderRadius = '50%';
frostEffect.style.backgroundColor = 'rgba(147, 197, 253, 0.3)';
frostEffect.style.zIndex = '6';
enemyElement.appendChild(frostEffect);
}
} else {
const frostVisual = enemyElement.querySelector('.frost-visual');
if (frostVisual) frostVisual.remove();
}
}
}
});
// Tower actions
gameState.towers.forEach((tower, towerIndex) => {
if (tower.cooldown > 0) {
tower.cooldown--;
return;
}
// Find closest enemy in range
let closestEnemy = null;
let closestDistance = Infinity;
gameState.enemies.forEach((enemy, enemyIndex) => {
if (enemy.health <= 0) return;
const dx = enemy.x - (tower.x * gameState.cellSize + gameState.cellSize / 2);
const dy = enemy.y - (tower.y * gameState.cellSize + gameState.cellSize / 2);
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < tower.range && distance < closestDistance) {
closestDistance = distance;
closestEnemy = { enemy, enemyIndex };
}
});
if (closestEnemy) {
// Shoot at enemy
tower.cooldown = tower.maxCooldown;
// Handle different tower effects
if (tower.effect === 'chain') {
// Tesla tower - hits multiple enemies
const hitEnemies = [{ enemy: closestEnemy.enemy, enemyIndex: closestEnemy.enemyIndex }];
// Find additional enemies in range
gameState.enemies.forEach((enemy, enemyIndex) => {
if (enemyIndex === closestEnemy.enemyIndex || enemy.health <= 0) return;
const dx = enemy.x - closestEnemy.enemy.x;
const dy = enemy.y - closestEnemy.enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60 && hitEnemies.length < 3) { // Chain to 2 additional enemies
hitEnemies.push({ enemy, enemyIndex });
}
});
// Create projectiles for each hit enemy
hitEnemies.forEach((target, i) => {
// Create projectile
const projectile = {
towerIndex: towerIndex,
enemyIndex: target.enemyIndex,
x: tower.x * gameState.cellSize + gameState.cellSize / 2,
y: tower.y * gameState.cellSize + gameState.cellSize / 2,
targetX: target.enemy.x,
targetY: target.enemy.y,
speed: towerTypes[tower.type].projectileSpeed,
damage: tower.damage * (i === 0 ? 1 : 0.6), // Main target takes full damage, chained take reduced
effect: tower.effect
};
gameState.projectiles.push(projectile);
// Create projectile element
const projectileElement = document.createElement('div');
projectileElement.className = `projectile projectile-${tower.type}`;
projectileElement.style.left = `${projectile.x - 4}px`;
projectileElement.style.top = `${projectile.y - 4}px`;
gameContainer.appendChild(projectileElement);
});
} else {
// Regular projectile towers
const projectile = {
towerIndex: towerIndex,
enemyIndex: closestEnemy.enemyIndex,
x: tower.x * gameState.cellSize + gameState.cellSize / 2,
y: tower.y * gameState.cellSize + gameState.cellSize / 2,
targetX: closestEnemy.enemy.x,
targetY: closestEnemy.enemy.y,
speed: towerTypes[tower.type].projectileSpeed,
damage: tower.damage,
effect: tower.effect,
splashRadius: tower.splashRadius
};
gameState.projectiles.push(projectile);
// Create projectile element
const projectileElement = document.createElement('div');
projectileElement.className = `projectile projectile-${tower.type}`;
projectileElement.style.left = `${projectile.x - 4}px`;
projectileElement.style.top = `${projectile.y - 4}px`;
gameContainer.appendChild(projectileElement);
}
}
});
// Move projectiles and handle hits
gameState.projectiles.forEach((projectile, projectileIndex) => {
const enemy = gameState.enemies[projectile.enemyIndex];
if (!enemy || enemy.health <= 0) {
// Enemy died before projectile hit
removeProjectile(projectileIndex);
return;
}
// Update target position (enemy may have moved)
projectile.targetX = enemy.x;
projectile.targetY = enemy.y;
const dx = projectile.targetX - projectile.x;
const dy = projectile.targetY - projectile.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5) {
// Hit enemy - handle effects
if (projectile.effect === 'frost') {
applyStatusEffect(enemy, 'frost');
createEffect(enemy.x, enemy.y, 'frost');
} else if (projectile.effect === 'poison') {
applyStatusEffect(enemy, 'poison');
createEffect(enemy.x, enemy.y, 'poison');
} else if (projectile.effect === 'flame') {
// Flame splash damage
createEffect(enemy.x, enemy.y, 'flame');
gameState.enemies.forEach((e, idx) => {
if (idx === projectile.enemyIndex || e.health <= 0) return;
const edx = e.x - enemy.x;
const edy = e.y - enemy.y;
const edist = Math.sqrt(edx * edx + edy * edy);
if (edist < projectile.splashRadius) {
e.health -= projectile.damage * 0.4; // 40% splash damage
// Update health bar
const enemyElement = document.querySelectorAll('.enemy')[idx];
if (enemyElement) {
const healthBar = enemyElement.querySelector('.health-bar');
healthBar.style.width = `${(e.health / e.maxHealth) * 100}%`;
if (e.health <= 0) {
// Enemy died
gameState.money += e.reward;
updateUI();
removeEnemy(idx);
}
}
}
});
} else if (projectile.effect === 'explosion') {
// Bomb tower area damage
createEffect(enemy.x, enemy.y, 'explosion');
gameState.enemies.forEach((e, idx) => {
const edx = e.x - enemy.x;
const edy = e.y - enemy.y;
const edist = Math.sqrt(edx * edx + edy * edy);
if (edist < projectile.splashRadius && e.health > 0) {
const splashDamage = projectile.damage * (1 - (edist / projectile.splashRadius));
e.health -= splashDamage;
// Update health bar
const enemyElement = document.querySelectorAll('.enemy')[idx];
if (enemyElement) {
const healthBar = enemyElement.querySelector('.health-bar');
healthBar.style.width = `${(e.health / e.maxHealth) * 100}%`;
if (e.health <= 0) {
// Enemy died
gameState.money += e.reward;
updateUI();
removeEnemy(idx);
}
}
}
});
} else if (projectile.effect === 'lightning') {
createEffect(enemy.x, enemy.y, 'lightning');
}
// Main target damage
enemy.health -= projectile.damage;
// Update health bar
const enemyElement = document.querySelectorAll('.enemy')[projectile.enemyIndex];
if (enemyElement) {
const healthBar = enemyElement.querySelector('.health-bar');
healthBar.style.width = `${(enemy.health / enemy.maxHealth) * 100}%`;
if (enemy.health <= 0) {
// Enemy died
gameState.money += enemy.reward;
updateUI();
removeEnemy(projectile.enemyIndex);
}
}
removeProjectile(projectileIndex);
} else {
// Move toward target
projectile.x += (dx / distance) * projectile.speed;
projectile.y += (dy / distance) * projectile.speed;
// Update projectile element position
const projectileElement = document.querySelectorAll('.projectile')[projectileIndex];
if (projectileElement) {
projectileElement.style.left = `${projectile.x - 4}px`;
projectileElement.style.top = `${projectile.y - 4}px`;
}
}
});
// Check if wave is complete
if (gameState.gameActive && gameState.enemies.length === 0 && !gameState.enemySpawnInterval) {
waveComplete();
}
}
// Remove enemy
function removeEnemy(index) {
const enemyElement = document.querySelectorAll('.enemy')[index];
if (enemyElement) enemyElement.remove();
gameState.enemies.splice(index, 1);
// Update indices in projectiles
gameState.projectiles.forEach(projectile => {
if (projectile.enemyIndex > index) {
projectile.enemyIndex--;
}
});
}
// Remove projectile
function removeProjectile(index) {
const projectileElement = document.querySelectorAll('.projectile')[index];
if (projectileElement) projectileElement.remove();
gameState.projectiles.splice(index, 1);
}
// Wave complete
function waveComplete() {
gameState.gameActive = false;
gameState.wave++;
updateUI();
startWaveBtn.disabled = false;
restartWaveBtn.disabled = false;
}
// Game over
function gameOver() {
clearInterval(gameState.gameLoopInterval);
clearInterval(gameState.enemySpawnInterval);
gameState.gameActive = false;
finalWaveDisplay.textContent = gameState.wave - 1;
gameOverScreen.style.display = 'flex';
}
// Update UI
function updateUI() {
waveDisplay.textContent = gameState.wave;
livesDisplay.textContent = gameState.lives;
moneyDisplay.textContent = gameState.money;
}
// Event listeners
startWaveBtn.addEventListener('click', startWave);
restartWaveBtn.addEventListener('click', restartWave);
towerShopItems.forEach(item => {
item.addEventListener('click', () => {
if (gameState.placingTower) {
// Cancel previous placement
gameState.placingTower = false;
gameState.towerType = null;
// Remove range indicator if it exists
const rangeIndicator = document.querySelector('.range-indicator');
if (rangeIndicator) rangeIndicator.remove();
}
const type = parseInt(item.dataset.type);
const cost = towerTypes[type].cost;
if (gameState.money >= cost) {
gameState.placingTower = true;
gameState.towerType = type;
// Create range indicator
const rangeIndicator = document.createElement('div');
rangeIndicator.className = 'range-indicator';
rangeIndicator.style.width = `${towerTypes[type].range * 2}px`;
rangeIndicator.style.height = `${towerTypes[type].range * 2}px`;
gameContainer.appendChild(rangeIndicator);
// Update position on mouse move
gameContainer.addEventListener('mousemove', (e) => {
if (gameState.placingTower) {
const rect = gameContainer.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / gameState.cellSize);
const y = Math.floor((e.clientY - rect.top) / gameState.cellSize);
rangeIndicator.style.left = `${x * gameState.cellSize + gameState.cellSize / 2}px`;
rangeIndicator.style.top = `${y * gameState.cellSize + gameState.cellSize / 2}px`;
}
});
} else {
alert('Not enough money!');
}
});
});
// Close tower menu when clicking elsewhere
document.addEventListener('click', (e) => {
if (!towerMenu.contains(e.target) && e.target.className.indexOf('tower') === -1) {
towerMenu.style.display = 'none';
gameState.selectedTower = null;
// Remove range indicator
const rangeIndicator = document.querySelector('.range-indicator');
if (rangeIndicator) rangeIndicator.remove();
}
});
upgradeBtn.addEventListener('click', upgradeTower);
sellBtn.addEventListener('click', sellTower);
restartBtn.addEventListener('click', initGame);
// Initialize game
initGame();
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=jamesjun/modded-tower-defense" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>