Spaces:
Running
Running
<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> |