Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Fantasy Tower Defense</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> | |
| @keyframes moveEnemy { | |
| to { | |
| background-position: -512px 0; | |
| } | |
| } | |
| .enemy { | |
| width: 32px; | |
| height: 32px; | |
| background-image: url('https://i.imgur.com/Jj7Zq9G.png'); | |
| animation: moveEnemy 0.8s steps(16) infinite; | |
| position: absolute; | |
| z-index: 10; | |
| transition: left 0.1s linear, top 0.1s linear; | |
| } | |
| .tower { | |
| width: 48px; | |
| height: 48px; | |
| background-image: url('https://i.imgur.com/Q9ZvZLQ.png'); | |
| background-position: 0 0; | |
| position: absolute; | |
| z-index: 5; | |
| } | |
| .projectile { | |
| width: 16px; | |
| height: 16px; | |
| background-color: #f00; | |
| border-radius: 50%; | |
| position: absolute; | |
| z-index: 8; | |
| } | |
| .path { | |
| background-color: #8B4513; | |
| background-image: url('https://i.imgur.com/5XQ3y9j.png'); | |
| background-size: cover; | |
| } | |
| .grass { | |
| background-color: #7CFC00; | |
| background-image: url('https://i.imgur.com/4ZQ3y9j.png'); | |
| background-size: cover; | |
| } | |
| .buildable { | |
| background-color: #3a7d44; | |
| background-image: url('https://i.imgur.com/6XQ3y9j.png'); | |
| background-size: cover; | |
| cursor: pointer; | |
| } | |
| .buildable:hover { | |
| filter: brightness(1.2); | |
| box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5); | |
| } | |
| .game-grid { | |
| display: grid; | |
| grid-template-columns: repeat(20, 32px); | |
| grid-template-rows: repeat(15, 32px); | |
| gap: 1px; | |
| background-color: #333; | |
| } | |
| .tower-range { | |
| position: absolute; | |
| border-radius: 50%; | |
| background-color: rgba(0, 100, 255, 0.2); | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| z-index: 2; | |
| } | |
| .health-bar { | |
| height: 4px; | |
| background-color: red; | |
| position: absolute; | |
| top: -6px; | |
| left: 0; | |
| width: 100%; | |
| } | |
| .upgrade-btn { | |
| transition: all 0.2s; | |
| } | |
| .upgrade-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 0 10px gold; | |
| } | |
| #game-container { | |
| perspective: 1000px; | |
| } | |
| #game-board { | |
| transform-style: preserve-3d; | |
| box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); | |
| } | |
| .start-point { | |
| background-color: #ff0000; | |
| background-image: url('https://i.imgur.com/7XQ3y9j.png'); | |
| background-size: cover; | |
| } | |
| .end-point { | |
| background-color: #0000ff; | |
| background-image: url('https://i.imgur.com/8XQ3y9j.png'); | |
| background-size: cover; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white font-mono"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <h1 class="text-4xl font-bold text-center mb-6 text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-red-500"> | |
| <i class="fas fa-fort-awesome-alt mr-2"></i> Fantasy Tower Defense | |
| </h1> | |
| <div class="flex flex-col lg:flex-row gap-6"> | |
| <!-- Game Board --> | |
| <div id="game-container" class="flex-1"> | |
| <div id="game-board" class="game-grid relative overflow-hidden border-4 border-yellow-700 rounded-lg bg-gray-800"> | |
| <!-- Grid will be generated by JavaScript --> | |
| </div> | |
| </div> | |
| <!-- Game Controls --> | |
| <div class="w-full lg:w-64 bg-gray-800 p-4 rounded-lg border-2 border-gray-700"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <div> | |
| <div class="text-xl font-bold text-yellow-400">Wave: <span id="wave">1</span></div> | |
| <div class="text-sm text-gray-300">Enemies: <span id="enemies-left">0</span></div> | |
| </div> | |
| <div class="text-right"> | |
| <div class="text-xl font-bold text-green-400">$<span id="money">150</span></div> | |
| <div class="text-sm text-gray-300">Lives: <span id="lives">20</span></div> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="h-2 bg-gray-700 rounded-full"> | |
| <div id="wave-progress" class="h-2 bg-blue-500 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="flex justify-between text-xs mt-1"> | |
| <span>Wave Progress</span> | |
| <span id="wave-timer">30s</span> | |
| </div> | |
| </div> | |
| <button id="start-wave" class="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg font-bold mb-4 transition-all"> | |
| <i class="fas fa-play mr-2"></i> Start Wave | |
| </button> | |
| <div class="mb-4"> | |
| <h3 class="font-bold text-lg border-b border-gray-700 pb-2 mb-2">Towers</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button class="tower-btn bg-blue-800 hover:bg-blue-700 p-2 rounded-lg" data-type="arrow" data-cost="100"> | |
| <div class="text-center"> | |
| <i class="fas fa-arrow-up text-xl mb-1"></i> | |
| <div>Arrow Tower</div> | |
| <div class="text-xs">$100</div> | |
| </div> | |
| </button> | |
| <button class="tower-btn bg-red-800 hover:bg-red-700 p-2 rounded-lg" data-type="cannon" data-cost="150"> | |
| <div class="text-center"> | |
| <i class="fas fa-bomb text-xl mb-1"></i> | |
| <div>Cannon Tower</div> | |
| <div class="text-xs">$150</div> | |
| </div> | |
| </button> | |
| <button class="tower-btn bg-purple-800 hover:bg-purple-700 p-2 rounded-lg" data-type="magic" data-cost="200"> | |
| <div class="text-center"> | |
| <i class="fas fa-magic text-xl mb-1"></i> | |
| <div>Magic Tower</div> | |
| <div class="text-xs">$200</div> | |
| </div> | |
| </button> | |
| <button class="tower-btn bg-yellow-800 hover:bg-yellow-700 p-2 rounded-lg" data-type="gold" data-cost="300"> | |
| <div class="text-center"> | |
| <i class="fas fa-coins text-xl mb-1"></i> | |
| <div>Gold Tower</div> | |
| <div class="text-xs">$300</div> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="tower-info" class="hidden bg-gray-700 p-3 rounded-lg border border-gray-600"> | |
| <h4 class="font-bold mb-2 flex justify-between"> | |
| <span id="tower-name">Tower</span> | |
| <button id="sell-tower" class="text-xs bg-red-600 hover:bg-red-700 px-2 py-1 rounded">Sell ($<span id="sell-price">0</span>)</button> | |
| </h4> | |
| <div class="text-sm mb-2"> | |
| <div>Level: <span id="tower-level">1</span></div> | |
| <div>Damage: <span id="tower-damage">10</span></div> | |
| <div>Range: <span id="tower-range">3</span></div> | |
| <div>Speed: <span id="tower-speed">1.5</span>s</div> | |
| </div> | |
| <button id="upgrade-tower" class="upgrade-btn w-full bg-yellow-600 hover:bg-yellow-700 text-white py-1 px-3 rounded text-sm"> | |
| Upgrade ($<span id="upgrade-cost">50</span>) | |
| </button> | |
| </div> | |
| <div class="mt-4 text-sm text-gray-400"> | |
| <p><i class="fas fa-info-circle mr-1"></i> Click on green buildable tiles to place towers</p> | |
| <p><i class="fas fa-info-circle mr-1"></i> Click on a tower to upgrade it</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game State | |
| const gameState = { | |
| money: 150, | |
| lives: 20, | |
| wave: 1, | |
| waveActive: false, | |
| waveTimer: 30, | |
| selectedTowerType: null, | |
| selectedTower: null, | |
| towers: [], | |
| enemies: [], | |
| projectiles: [], | |
| path: [], | |
| waypoints: [], | |
| buildableTiles: [] | |
| }; | |
| // Tower Types | |
| const towerTypes = { | |
| arrow: { | |
| name: "Arrow Tower", | |
| damage: 10, | |
| range: 3, | |
| speed: 1.5, | |
| cost: 100, | |
| color: "blue", | |
| upgradeCost: 50, | |
| damageIncrement: 5, | |
| rangeIncrement: 0.5, | |
| speedIncrement: -0.2 | |
| }, | |
| cannon: { | |
| name: "Cannon Tower", | |
| damage: 25, | |
| range: 2.5, | |
| speed: 2.5, | |
| cost: 150, | |
| color: "red", | |
| upgradeCost: 75, | |
| damageIncrement: 10, | |
| rangeIncrement: 0.3, | |
| speedIncrement: -0.3 | |
| }, | |
| magic: { | |
| name: "Magic Tower", | |
| damage: 15, | |
| range: 3.5, | |
| speed: 1.8, | |
| cost: 200, | |
| color: "purple", | |
| upgradeCost: 100, | |
| damageIncrement: 7, | |
| rangeIncrement: 0.4, | |
| speedIncrement: -0.15 | |
| }, | |
| gold: { | |
| name: "Gold Tower", | |
| damage: 8, | |
| range: 2, | |
| speed: 1, | |
| cost: 300, | |
| color: "yellow", | |
| upgradeCost: 150, | |
| damageIncrement: 4, | |
| rangeIncrement: 0.2, | |
| speedIncrement: -0.05, | |
| moneyPerShot: 1 | |
| } | |
| }; | |
| // Enemy Types | |
| const enemyTypes = [ | |
| { name: "Goblin", health: 50, speed: 1, reward: 10, spriteY: 0 }, | |
| { name: "Orc", health: 100, speed: 0.8, reward: 20, spriteY: -32 }, | |
| { name: "Troll", health: 200, speed: 0.6, reward: 40, spriteY: -64 }, | |
| { name: "Dragon", health: 500, speed: 0.5, reward: 100, spriteY: -96 } | |
| ]; | |
| // Initialize Game Board | |
| const gameBoard = document.getElementById('game-board'); | |
| const gridWidth = 20; | |
| const gridHeight = 15; | |
| const cellSize = 32; | |
| // Generate path and buildable areas | |
| function generatePath() { | |
| const path = []; | |
| const waypoints = []; | |
| const buildableTiles = []; | |
| // Start at top middle (red) | |
| let x = 9, y = 0; | |
| path.push({x, y}); | |
| waypoints.push({x, y}); | |
| // Go down | |
| for (y = 1; y < 5; y++) { | |
| path.push({x, y}); | |
| } | |
| waypoints.push({x: x, y: y - 1}); | |
| // Go right | |
| for (x = 10; x < 15; x++) { | |
| path.push({x: x, y: y-1}); | |
| } | |
| waypoints.push({x: x-1, y: y-1}); | |
| // Go down | |
| for (y = 5; y < 10; y++) { | |
| path.push({x: x-1, y: y}); | |
| } | |
| waypoints.push({x: x-1, y: y-1}); | |
| // Go left | |
| for (x = 14; x > 5; x--) { | |
| path.push({x: x, y: y-1}); | |
| } | |
| waypoints.push({x: x+1, y: y-1}); | |
| // Go down to exit (blue) | |
| for (y = 10; y < 15; y++) { | |
| path.push({x: x+1, y: y}); | |
| } | |
| waypoints.push({x: x+1, y: y-1}); | |
| // Generate buildable areas (not on path) | |
| for (let y = 0; y < gridHeight; y++) { | |
| for (let x = 0; x < gridWidth; x++) { | |
| const isPath = path.some(point => point.x === x && point.y === y); | |
| if (!isPath) { | |
| // Check if tile is adjacent to path (for better gameplay) | |
| const adjacentToPath = | |
| path.some(point => | |
| (point.x === x && Math.abs(point.y - y) === 1) || | |
| (point.y === y && Math.abs(point.x - x) === 1) | |
| ); | |
| if (adjacentToPath) { | |
| buildableTiles.push({x, y}); | |
| } | |
| } | |
| } | |
| } | |
| gameState.path = path; | |
| gameState.waypoints = waypoints; | |
| gameState.buildableTiles = buildableTiles; | |
| renderGrid(); | |
| } | |
| // Render grid | |
| function renderGrid() { | |
| gameBoard.innerHTML = ''; | |
| // Create grid cells | |
| for (let y = 0; y < gridHeight; y++) { | |
| for (let x = 0; x < gridWidth; x++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'w-8 h-8'; | |
| cell.dataset.x = x; | |
| cell.dataset.y = y; | |
| // Check if this cell is part of the path | |
| const isPath = gameState.path.some(point => point.x === x && point.y === y); | |
| const isBuildable = gameState.buildableTiles.some(point => point.x === x && point.y === y); | |
| if (isPath) { | |
| // Check if start or end point | |
| if (x === 9 && y === 0) { | |
| cell.classList.add('start-point'); | |
| } else if (x === 6 && y === 14) { | |
| cell.classList.add('end-point'); | |
| } else { | |
| cell.classList.add('path'); | |
| } | |
| } else if (isBuildable) { | |
| cell.classList.add('buildable'); | |
| cell.addEventListener('click', () => placeTower(x, y)); | |
| } else { | |
| cell.classList.add('grass'); | |
| } | |
| gameBoard.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Place tower | |
| function placeTower(x, y) { | |
| if (gameState.selectedTowerType && !gameState.waveActive) { | |
| const towerType = towerTypes[gameState.selectedTowerType]; | |
| if (gameState.money >= towerType.cost) { | |
| gameState.money -= towerType.cost; | |
| updateMoney(); | |
| const tower = { | |
| type: gameState.selectedTowerType, | |
| x: x, | |
| y: y, | |
| level: 1, | |
| damage: towerType.damage, | |
| range: towerType.range, | |
| speed: towerType.speed, | |
| lastShot: 0, | |
| element: null, | |
| rangeElement: null | |
| }; | |
| gameState.towers.push(tower); | |
| renderTower(tower); | |
| // Deselect tower type after placement | |
| gameState.selectedTowerType = null; | |
| document.querySelectorAll('.tower-btn').forEach(btn => { | |
| btn.classList.remove('ring-2', 'ring-yellow-400'); | |
| }); | |
| } else { | |
| showMessage("Not enough money!"); | |
| } | |
| } | |
| } | |
| // Render tower | |
| function renderTower(tower) { | |
| const towerElement = document.createElement('div'); | |
| towerElement.className = 'tower'; | |
| towerElement.style.left = `${tower.x * cellSize}px`; | |
| towerElement.style.top = `${tower.y * cellSize}px`; | |
| // Add color based on tower type | |
| towerElement.style.filter = `hue-rotate(${getTowerHue(tower.type)}deg)`; | |
| towerElement.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| selectTower(tower); | |
| }); | |
| // Create range indicator | |
| const rangeElement = document.createElement('div'); | |
| rangeElement.className = 'tower-range'; | |
| rangeElement.style.width = `${tower.range * cellSize * 2}px`; | |
| rangeElement.style.height = `${tower.range * cellSize * 2}px`; | |
| rangeElement.style.left = `${(tower.x + 0.5) * cellSize}px`; | |
| rangeElement.style.top = `${(tower.y + 0.5) * cellSize}px`; | |
| rangeElement.style.display = 'none'; | |
| gameBoard.appendChild(towerElement); | |
| gameBoard.appendChild(rangeElement); | |
| tower.element = towerElement; | |
| tower.rangeElement = rangeElement; | |
| } | |
| function getTowerHue(type) { | |
| switch(type) { | |
| case 'arrow': return 200; // blue | |
| case 'cannon': return 0; // red | |
| case 'magic': return 270; // purple | |
| case 'gold': return 50; // yellow | |
| default: return 0; | |
| } | |
| } | |
| // Select tower | |
| function selectTower(tower) { | |
| if (gameState.waveActive) return; | |
| // Deselect previous tower | |
| if (gameState.selectedTower) { | |
| gameState.selectedTower.rangeElement.style.display = 'none'; | |
| } | |
| gameState.selectedTower = tower; | |
| tower.rangeElement.style.display = 'block'; | |
| // Update tower info panel | |
| const towerType = towerTypes[tower.type]; | |
| document.getElementById('tower-info').classList.remove('hidden'); | |
| document.getElementById('tower-name').textContent = towerType.name; | |
| document.getElementById('tower-level').textContent = tower.level; | |
| document.getElementById('tower-damage').textContent = tower.damage; | |
| document.getElementById('tower-range').textContent = tower.range; | |
| document.getElementById('tower-speed').textContent = tower.speed; | |
| // Calculate upgrade cost | |
| const upgradeCost = towerType.upgradeCost * tower.level; | |
| document.getElementById('upgrade-cost').textContent = upgradeCost; | |
| // Calculate sell price (50% of total invested) | |
| const totalInvested = towerType.cost + (tower.level - 1) * upgradeCost; | |
| const sellPrice = Math.floor(totalInvested * 0.7); | |
| document.getElementById('sell-price').textContent = sellPrice; | |
| } | |
| // Upgrade tower | |
| document.getElementById('upgrade-tower').addEventListener('click', function() { | |
| if (!gameState.selectedTower) return; | |
| const tower = gameState.selectedTower; | |
| const towerType = towerTypes[tower.type]; | |
| const upgradeCost = towerType.upgradeCost * tower.level; | |
| if (gameState.money >= upgradeCost) { | |
| gameState.money -= upgradeCost; | |
| updateMoney(); | |
| tower.level++; | |
| tower.damage += towerType.damageIncrement; | |
| tower.range += towerType.rangeIncrement; | |
| tower.speed += towerType.speedIncrement; | |
| // Update tower info | |
| document.getElementById('tower-level').textContent = tower.level; | |
| document.getElementById('tower-damage').textContent = tower.damage; | |
| document.getElementById('tower-range').textContent = tower.range; | |
| document.getElementById('tower-speed').textContent = tower.speed; | |
| // Update range indicator | |
| tower.rangeElement.style.width = `${tower.range * cellSize * 2}px`; | |
| tower.rangeElement.style.height = `${tower.range * cellSize * 2}px`; | |
| // Update upgrade cost for next level | |
| const newUpgradeCost = towerType.upgradeCost * tower.level; | |
| document.getElementById('upgrade-cost').textContent = newUpgradeCost; | |
| // Update sell price | |
| const totalInvested = towerType.cost + (tower.level - 1) * upgradeCost; | |
| const sellPrice = Math.floor(totalInvested * 0.7); | |
| document.getElementById('sell-price').textContent = sellPrice; | |
| showMessage(`Tower upgraded to level ${tower.level}!`); | |
| } else { | |
| showMessage("Not enough money for upgrade!"); | |
| } | |
| }); | |
| // Sell tower | |
| document.getElementById('sell-tower').addEventListener('click', function() { | |
| if (!gameState.selectedTower) return; | |
| const tower = gameState.selectedTower; | |
| const towerType = towerTypes[tower.type]; | |
| const totalInvested = towerType.cost + (tower.level - 1) * towerType.upgradeCost * tower.level; | |
| const sellPrice = Math.floor(totalInvested * 0.7); | |
| gameState.money += sellPrice; | |
| updateMoney(); | |
| // Remove tower elements | |
| gameBoard.removeChild(tower.element); | |
| gameBoard.removeChild(tower.rangeElement); | |
| // Remove from towers array | |
| gameState.towers = gameState.towers.filter(t => t !== tower); | |
| // Hide tower info | |
| document.getElementById('tower-info').classList.add('hidden'); | |
| gameState.selectedTower = null; | |
| showMessage(`Tower sold for $${sellPrice}!`); | |
| }); | |
| // Tower buttons | |
| document.querySelectorAll('.tower-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| if (gameState.waveActive) return; | |
| gameState.selectedTowerType = this.dataset.type; | |
| // Update UI | |
| document.querySelectorAll('.tower-btn').forEach(b => { | |
| b.classList.remove('ring-2', 'ring-yellow-400'); | |
| }); | |
| this.classList.add('ring-2', 'ring-yellow-400'); | |
| // Hide tower info if showing | |
| document.getElementById('tower-info').classList.add('hidden'); | |
| if (gameState.selectedTower) { | |
| gameState.selectedTower.rangeElement.style.display = 'none'; | |
| gameState.selectedTower = null; | |
| } | |
| }); | |
| }); | |
| // Start wave | |
| document.getElementById('start-wave').addEventListener('click', startWave); | |
| function startWave() { | |
| if (gameState.waveActive) return; | |
| gameState.waveActive = true; | |
| document.getElementById('start-wave').disabled = true; | |
| // Calculate number of enemies based on wave | |
| const baseEnemies = 5; | |
| const waveMultiplier = 1 + (gameState.wave - 1) * 0.3; | |
| const totalEnemies = Math.floor(baseEnemies * waveMultiplier); | |
| // Spawn enemies | |
| let spawned = 0; | |
| const spawnInterval = setInterval(() => { | |
| if (spawned >= totalEnemies) { | |
| clearInterval(spawnInterval); | |
| return; | |
| } | |
| // Random enemy type (higher waves get stronger enemies) | |
| let enemyTypeIndex = 0; | |
| const random = Math.random(); | |
| if (gameState.wave > 10 && random < 0.1) { | |
| enemyTypeIndex = 3; // Dragon | |
| } else if (gameState.wave > 5 && random < 0.3) { | |
| enemyTypeIndex = 2; // Troll | |
| } else if (gameState.wave > 3 && random < 0.6) { | |
| enemyTypeIndex = 1; // Orc | |
| } | |
| spawnEnemy(enemyTypes[enemyTypeIndex]); | |
| spawned++; | |
| // Update enemies left | |
| document.getElementById('enemies-left').textContent = gameState.enemies.length; | |
| }, 1000); | |
| // Wave timer | |
| let timeLeft = 30; | |
| const waveTimer = setInterval(() => { | |
| timeLeft--; | |
| document.getElementById('wave-timer').textContent = `${timeLeft}s`; | |
| document.getElementById('wave-progress').style.width = `${(1 - timeLeft/30) * 100}%`; | |
| if (timeLeft <= 0 || gameState.enemies.length === 0) { | |
| clearInterval(waveTimer); | |
| // Check if all enemies are dead | |
| if (gameState.enemies.length === 0) { | |
| endWave(true); | |
| } | |
| } | |
| }, 1000); | |
| } | |
| function endWave(success) { | |
| gameState.waveActive = false; | |
| document.getElementById('start-wave').disabled = false; | |
| if (success) { | |
| // Wave completed successfully | |
| gameState.money += 100 + (gameState.wave * 20); | |
| gameState.wave++; | |
| updateMoney(); | |
| updateWave(); | |
| showMessage(`Wave ${gameState.wave-1} completed! Bonus: $${100 + (gameState.wave-1) * 20}`); | |
| } | |
| // Reset wave timer UI | |
| document.getElementById('wave-timer').textContent = '30s'; | |
| document.getElementById('wave-progress').style.width = '0%'; | |
| } | |
| function spawnEnemy(type) { | |
| const enemy = { | |
| type: type.name, | |
| health: type.health, | |
| maxHealth: type.health, | |
| speed: type.speed, | |
| reward: type.reward, | |
| x: gameState.path[0].x, | |
| y: gameState.path[0].y, | |
| pathIndex: 0, | |
| element: null, | |
| healthBar: null | |
| }; | |
| gameState.enemies.push(enemy); | |
| renderEnemy(enemy); | |
| } | |
| function renderEnemy(enemy) { | |
| const enemyElement = document.createElement('div'); | |
| enemyElement.className = 'enemy'; | |
| enemyElement.style.left = `${enemy.x * cellSize}px`; | |
| enemyElement.style.top = `${enemy.y * cellSize}px`; | |
| enemyElement.style.backgroundPositionY = `${enemyTypes.find(e => e.name === enemy.type).spriteY}px`; | |
| // Health bar | |
| const healthBar = document.createElement('div'); | |
| healthBar.className = 'health-bar'; | |
| enemyElement.appendChild(healthBar); | |
| gameBoard.appendChild(enemyElement); | |
| enemy.element = enemyElement; | |
| enemy.healthBar = healthBar; | |
| } | |
| // Game loop | |
| function gameLoop(timestamp) { | |
| // Move enemies | |
| gameState.enemies.forEach(enemy => { | |
| if (enemy.pathIndex < gameState.path.length - 1) { | |
| const nextPoint = gameState.path[enemy.pathIndex + 1]; | |
| const dx = nextPoint.x - enemy.x; | |
| const dy = nextPoint.y - enemy.y; | |
| // Normalize direction | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| const speed = 0.02 * enemy.speed; | |
| if (distance < speed) { | |
| // Reached next point | |
| enemy.x = nextPoint.x; | |
| enemy.y = nextPoint.y; | |
| enemy.pathIndex++; | |
| // Check if reached end | |
| if (enemy.pathIndex === gameState.path.length - 1) { | |
| // Enemy reached the end | |
| gameState.lives--; | |
| updateLives(); | |
| // Remove enemy | |
| gameBoard.removeChild(enemy.element); | |
| gameState.enemies = gameState.enemies.filter(e => e !== enemy); | |
| if (gameState.lives <= 0) { | |
| gameOver(); | |
| } | |
| return; | |
| } | |
| } else { | |
| // Move towards next point | |
| enemy.x += (dx / distance) * speed; | |
| enemy.y += (dy / distance) * speed; | |
| } | |
| // Update position | |
| enemy.element.style.left = `${enemy.x * cellSize}px`; | |
| enemy.element.style.top = `${enemy.y * cellSize}px`; | |
| } | |
| }); | |
| // Tower attacks | |
| gameState.towers.forEach(tower => { | |
| if (timestamp - tower.lastShot > tower.speed * 1000) { | |
| // Find closest enemy in range | |
| let closestEnemy = null; | |
| let minDistance = Infinity; | |
| gameState.enemies.forEach(enemy => { | |
| const dx = enemy.x - (tower.x + 0.5); | |
| const dy = enemy.y - (tower.y + 0.5); | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance <= tower.range && distance < minDistance) { | |
| closestEnemy = enemy; | |
| minDistance = distance; | |
| } | |
| }); | |
| if (closestEnemy) { | |
| // Shoot at enemy | |
| shoot(tower, closestEnemy, timestamp); | |
| tower.lastShot = timestamp; | |
| } | |
| } | |
| }); | |
| // Move projectiles | |
| gameState.projectiles.forEach((proj, index) => { | |
| const dx = proj.targetX - proj.x; | |
| const dy = proj.targetY - proj.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 0.2) { | |
| // Hit target | |
| proj.target.health -= proj.damage; | |
| // Update health bar | |
| if (proj.target.healthBar) { | |
| const healthPercent = (proj.target.health / proj.target.maxHealth) * 100; | |
| proj.target.healthBar.style.width = `${healthPercent}%`; | |
| if (healthPercent > 50) { | |
| proj.target.healthBar.style.backgroundColor = 'green'; | |
| } else if (healthPercent > 25) { | |
| proj.target.healthBar.style.backgroundColor = 'orange'; | |
| } else { | |
| proj.target.healthBar.style.backgroundColor = 'red'; | |
| } | |
| } | |
| // Check if enemy died | |
| if (proj.target.health <= 0) { | |
| gameState.money += proj.target.reward; | |
| updateMoney(); | |
| gameBoard.removeChild(proj.target.element); | |
| gameState.enemies = gameState.enemies.filter(e => e !== proj.target); | |
| // Check if wave completed | |
| if (gameState.waveActive && gameState.enemies.length === 0) { | |
| endWave(true); | |
| } | |
| } | |
| // Remove projectile | |
| gameBoard.removeChild(proj.element); | |
| gameState.projectiles.splice(index, 1); | |
| } else { | |
| // Move projectile | |
| const speed = 0.2; | |
| proj.x += (dx / distance) * speed; | |
| proj.y += (dy / distance) * speed; | |
| proj.element.style.left = `${proj.x * cellSize}px`; | |
| proj.element.style.top = `${proj.y * cellSize}px`; | |
| } | |
| }); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function shoot(tower, enemy, timestamp) { | |
| const towerType = towerTypes[tower.type]; | |
| // Create projectile | |
| const projectile = { | |
| x: tower.x + 0.5, | |
| y: tower.y + 0.5, | |
| targetX: enemy.x + 0.5, | |
| targetY: enemy.y + 0.5, | |
| damage: tower.damage, | |
| target: enemy, | |
| element: null | |
| }; | |
| // Special effects for tower types | |
| if (tower.type === 'gold') { | |
| // Gold tower earns money per shot | |
| gameState.money += towerType.moneyPerShot; | |
| updateMoney(); | |
| } | |
| // Create projectile element | |
| const projElement = document.createElement('div'); | |
| projElement.className = 'projectile'; | |
| // Color based on tower type | |
| switch(tower.type) { | |
| case 'arrow': projElement.style.backgroundColor = '#3498db'; break; | |
| case 'cannon': projElement.style.backgroundColor = '#e74c3c'; break; | |
| case 'magic': projElement.style.backgroundColor = '#9b59b6'; break; | |
| case 'gold': projElement.style.backgroundColor = '#f1c40f'; break; | |
| } | |
| projElement.style.left = `${projectile.x * cellSize}px`; | |
| projElement.style.top = `${projectile.y * cellSize}px`; | |
| gameBoard.appendChild(projElement); | |
| projectile.element = projElement; | |
| gameState.projectiles.push(projectile); | |
| } | |
| function gameOver() { | |
| showMessage("Game Over! Refresh to play again."); | |
| gameState.waveActive = false; | |
| document.getElementById('start-wave').disabled = true; | |
| } | |
| // UI Updates | |
| function updateMoney() { | |
| document.getElementById('money').textContent = gameState.money; | |
| } | |
| function updateLives() { | |
| document.getElementById('lives').textContent = gameState.lives; | |
| // Flash lives when low | |
| if (gameState.lives <= 5) { | |
| const livesEl = document.getElementById('lives'); | |
| livesEl.classList.add('text-red-500', 'animate-pulse'); | |
| } else { | |
| document.getElementById('lives').classList.remove('text-red-500', 'animate-pulse'); | |
| } | |
| } | |
| function updateWave() { | |
| document.getElementById('wave').textContent = gameState.wave; | |
| } | |
| function showMessage(msg) { | |
| const message = document.createElement('div'); | |
| message.className = 'fixed top-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg z-50 animate-bounce'; | |
| message.textContent = msg; | |
| document.body.appendChild(message); | |
| setTimeout(() => { | |
| message.classList.remove('animate-bounce'); | |
| message.classList.add('opacity-0', 'transition-opacity', 'duration-500'); | |
| setTimeout(() => document.body.removeChild(message), 500); | |
| }, 2000); | |
| } | |
| // Initialize game | |
| generatePath(); | |
| updateMoney(); | |
| updateLives(); | |
| updateWave(); | |
| // Start game loop | |
| requestAnimationFrame(gameLoop); | |
| // Helpers | |
| document.addEventListener('click', function(e) { | |
| // Click outside to deselect tower | |
| if (!e.target.closest('#game-board') && !e.target.closest('.tower-btn') && !e.target.closest('#tower-info')) { | |
| if (gameState.selectedTower) { | |
| gameState.selectedTower.rangeElement.style.display = 'none'; | |
| gameState.selectedTower = null; | |
| document.getElementById('tower-info').classList.add('hidden'); | |
| } | |
| if (gameState.selectedTowerType) { | |
| gameState.selectedTowerType = null; | |
| document.querySelectorAll('.tower-btn').forEach(btn => { | |
| btn.classList.remove('ring-2', 'ring-yellow-400'); | |
| }); | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |