FPS: 60

ZOMBIE DEFENSE

Survive the undead horde as long as you can!

GAME OVER

Final Score: 0

Wave Reached: 0

PAUSED

ARMORY

Prepare for the next wave!

Cash: $0

Controls:

Click or tap to shoot zombies

Press R to reload

1-4 keys to switch weapons

Headshots do extra damage!

Collect drops from zombies for health, ammo & armor

Press ESC to pause

(Click anywhere to dismiss)

Score: 0
Wave: 1
$0
10/10
RELOAD!
+
+
P
-
-
-
FIRE
R
= 'block'; setTimeout(() => { document.getElementById('reload-message').style.display = 'none'; }, 1000); return; } // Apply weapon recoil to crosshair const crosshair = document.getElementById('crosshair'); crosshair.classList.add('expanded'); setTimeout(() => { crosshair.classList.remove('expanded'); }, 100); // Apply recoil to weapon model const weaponModel = document.getElementById('weapon-model'); weaponModel.style.transform = `translateY(${weapon.recoil}px) rotate(${weapon.recoil/2}deg)`; setTimeout(() => { weaponModel.style.transform = 'translateY(0) rotate(0deg)'; }, 100); // Play shot sound playSound(weapon.shotSound); // Show muzzle flash const muzzleFlash = document.getElementById('muzzle-flash'); muzzleFlash.style.display = 'block'; setTimeout(() => { muzzleFlash.style.display = 'none'; }, 50); // Decrease ammo (unless infinite ammo power-up is active) if (!powerUps.infiniteAmmo.active) { weapon.ammo--; } updateAmmoDisplay(); // Update last fired time weapon.lastFired = now; // For shotgun, create multiple pellets if (weapon.pellets) { for (let i = 0; i < weapon.pellets; i++) { const spreadX = x + (Math.random() - 0.5) * weapon.spread * 2; const spreadY = y + (Math.random() - 0.5) * weapon.spread * 2; createBulletHole(spreadX, spreadY); checkZombieHits(spreadX, spreadY, weapon); } } else if (weapon.explosion) { // For rocket launcher, create rocket projectile createRocket(x, y, weapon); } else { // Apply random spread const spreadX = x + (Math.random() - 0.5) * weapon.spread; const spreadY = y + (Math.random() - 0.5) * weapon.spread; // Regular single bullet createBulletHole(spreadX, spreadY); checkZombieHits(spreadX, spreadY, weapon); } // Auto reload if out of ammo if (weapon.ammo <= 0) { startReload(); } } // Create rocket projectile function createRocket(targetX, targetY, weapon) { // Get screen center as starting point const startX = window.innerWidth / 2; const startY = window.innerHeight / 2; // Calculate direction const dirX = targetX - startX; const dirY = targetY - startY; const length = Math.sqrt(dirX * dirX + dirY * dirY); const normalizedDirX = dirX / length; const normalizedDirY = dirY / length; // Create rocket visual element const rocket = document.createElement('div'); rocket.style.position = 'absolute'; rocket.style.width = '10px'; rocket.style.height = '20px'; rocket.style.backgroundColor = '#ff0000'; rocket.style.borderRadius = '5px'; rocket.style.left = `${startX}px`; rocket.style.top = `${startY}px`; rocket.style.transform = `rotate(${Math.atan2(normalizedDirY, normalizedDirX) * 180 / Math.PI}deg)`; rocket.style.zIndex = '5'; // Add trail effect const trail = document.createElement('div'); trail.style.position = 'absolute'; trail.style.width = '5px'; trail.style.height = '30px'; trail.style.background = 'linear-gradient(to top, rgba(255,255,0,0), rgba(255,165,0,0.8))'; trail.style.borderRadius = '5px'; trail.style.left = '-5px'; trail.style.top = '20px'; trail.style.transform = 'rotate(180deg)'; rocket.appendChild(trail); document.getElementById('ui-layer').appendChild(rocket); // Play rocket sound playSound('rocket_shot'); // Animate rocket let posX = startX; let posY = startY; let frameCount = 0; const moveRocket = () => { // Update position posX += normalizedDirX * config.bulletSpeed; posY += normalizedDirY * config.bulletSpeed; rocket.style.left = `${posX}px`; rocket.style.top = `${posY}px`; // Check if rocket hit edge of screen or traveled far enough const distanceFromStart = Math.sqrt( Math.pow(posX - startX, 2) + Math.pow(posY - startY, 2) ); if (posX < 0 || posX > window.innerWidth || posY < 0 || posY > window.innerHeight || distanceFromStart > 2000 || frameCount > 100) { // Explode at current position createExplosion(posX, posY, weapon.explosionRadius, weapon.damage); rocket.remove(); return; } // Check for zombie collision const worldPos = screenToWorld(posX, posY); for (let i = 0; i < zombies.length; i++) { const zombie = zombies[i]; const dx = worldPos.x - zombie.mesh.position.x; const dy = worldPos.y - zombie.mesh.position.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) { // Rocket hit zombie directly - create explosion createExplosion(posX, posY, weapon.explosionRadius, weapon.damage); rocket.remove(); return; } } // Continue animation frameCount++; requestAnimationFrame(moveRocket); }; moveRocket(); } // Create explosion effect function createExplosion(x, y, radius, damage) { // Visual effect const explosion = document.createElement('div'); explosion.style.position = 'absolute'; explosion.style.left = `${x - radius}px`; explosion.style.top = `${y - radius}px`; explosion.style.width = `${radius * 2}px`; explosion.style.height = `${radius * 2}px`; explosion.style.background = 'radial-gradient(circle, rgba(255,200,0,0.8) 0%, rgba(255,100,0,0.6) 40%, rgba(255,0,0,0.4) 70%, rgba(0,0,0,0) 100%)'; explosion.style.borderRadius = '50%'; explosion.style.zIndex = '6'; explosion.style.pointerEvents = 'none'; document.getElementById('ui-layer').appendChild(explosion); // Animation let size = 0.1; let opacity = 1; const animate = () => { size += 0.15; opacity -= 0.05; explosion.style.transform = `scale(${size})`; explosion.style.opacity = opacity; if (opacity > 0) { requestAnimationFrame(animate); } else { explosion.remove(); } }; animate(); // Play explosion sound playSound('explosion'); // Screen shake screenShake(); // Damage zombies in radius const worldPos = screenToWorld(x, y); for (let i = zombies.length - 1; i >= 0; i--) { const zombie = zombies[i]; const dx = worldPos.x - zombie.mesh.position.x; const dy = worldPos.y - zombie.mesh.position.y; const distance = Math.sqrt(dx * dx + dy * dy); // Calculate damage based on distance from explosion center if (distance < radius / 3) { // Apply damage with falloff based on distance const falloffFactor = 1 - (distance / (radius / 3)); const explosionDamage = damage * falloffFactor; // Create blood effect at zombie position const zombieScreenPos = worldToScreen(zombie.mesh.position); createBloodSplatter(zombieScreenPos.x, zombieScreenPos.y); // Apply damage zombie.health -= explosionDamage; // Update health bar zombie.healthFill.style.width = `${(zombie.health / zombie.maxHealth) * 100}%`; // Check if zombie died if (zombie.health <= 0) { // Remove zombie scene.remove(zombie.mesh); zombie.healthBar.remove(); // Update score and cash score += zombie.value; cash += zombie.value / 2; updateScore(); updateCashDisplay(); // Create death effects createZombieDeathEffects(zombieScreenPos.x, zombieScreenPos.y); // Play death sound playSound('zombie_death'); // Remove from array zombies.splice(i, 1); zombiesKilled++; // Possibly drop pickup if (Math.random() < config.pickupChance) { createPickup(zombie.mesh.position.x, zombie.mesh.position.y); } // Update minimap updateMinimap(); // Check if wave completed if (zombiesKilled >= config.zombiesPerWave + (wave - 1) * config.waveIncrement) { startNextWave(); } } } } } // Create bullet hole visual function createBulletHole(x, y) { // Convert screen position to world position const worldPosition = screenToWorld(x, y); // Create bullet hole div const bulletHole = document.createElement('div'); bulletHole.className = 'bullet-hole'; bulletHole.style.left = `${x - 3}px`; bulletHole.style.top = `${y - 3}px`; document.getElementById('ui-layer').appendChild(bulletHole); // Limit number of bullet holes bulletHoles.push(bulletHole); if (bulletHoles.length > config.maxBulletHoles) { const oldHole = bulletHoles.shift(); oldHole.remove(); } } // Create blood splatter effect function createBloodSplatter(x, y) { const size = 10 + Math.random() * 20; // Create blood splatter div const blood = document.createElement('div'); blood.className = 'blood-splatter'; blood.style.left = `${x - size/2}px`; blood.style.top = `${y - size/2}px`; blood.style.width = `${size}px`; blood.style.height = `${size}px`; document.getElementById('ui-layer').appendChild(blood); // Limit number of blood splatters bloodSplatters.push(blood); if (bloodSplatters.length > config.maxBloodSplatters) { const oldBlood = bloodSplatters.shift(); oldBlood.remove(); } } // Create zombie death effects function createZombieDeathEffects(x, y) { // Create more blood for (let i = 0; i < 8; i++) { const offsetX = (Math.random() - 0.5) * 80; const offsetY = (Math.random() - 0.5) * 80; createBloodSplatter(x + offsetX, y + offsetY); } // Create floating score text const scoreText = document.createElement('div'); scoreText.className = 'floating-text'; scoreText.style.left = `${x}px`; scoreText.style.top = `${y}px`; scoreText.textContent = `+${zombies[0].value}`; document.getElementById('ui-layer').appendChild(scoreText); // Animate floating up and fading let opacity = 1; let posY = y; const animate = () => { opacity -= 0.02; posY -= 1; scoreText.style.opacity = opacity; scoreText.style.top = `${posY}px`; if (opacity > 0) { requestAnimationFrame(animate); } else { scoreText.remove(); } }; animate(); } // Create pickup item (health, ammo, armor) function createPickup(x, y) { // Determine pickup type const types = ['health', 'ammo', 'armor']; const weights = [0.4, 0.4, 0.2]; // Probability weights let type = types[0]; const rand = Math.random(); let cumulative = 0; for (let i = 0; i < weights.length; i++) { cumulative += weights[i]; if (rand < cumulative) { type = types[i]; break; } } // Create pickup element const pickup = document.createElement('div'); pickup.className = `pickup pickup-${type}`; // Convert world to screen coordinates const screenPos = worldToScreen(new THREE.Vector3(x, y, 0)); pickup.style.left = `${screenPos.x - 10}px`; pickup.style.top = `${screenPos.y - 10}px`; document.getElementById('ui-layer').appendChild(pickup); // Add to pickups array pickups.push({ element: pickup, type: type, position: new THREE.Vector3(x, y, 0), created: performance.now() }); // Set lifetime for pickup setTimeout(() => { // Remove if still exists const index = pickups.findIndex(p => p.element === pickup); if (index !== -1) { pickup.remove(); pickups.splice(index, 1); } }, config.pickupLifetime); } // Check for pickup collection function checkPickups() { // Player position is center of screen (0,0,0) in world coordinates const playerPos = new THREE.Vector3(0, 0, 0); const collectionDistance = 50; // Distance to collect pickup for (let i = pickups.length - 1; i >= 0; i--) { const pickup = pickups[i]; const distance = pickup.position.distanceTo(playerPos); if (distance < collectionDistance) { // Apply pickup effect switch (pickup.type) { case 'health': // Restore 25 health, up to max health = Math.min(100 + (purchases.healthUpgrade * 25), health + 25); updateHealthBar(); break; case 'ammo': // Restore ammo for current weapon weapons[activeWeaponIndex].ammo = weapons[activeWeaponIndex].maxAmmo; updateAmmoDisplay(); break; case 'armor': // Add 25 armor, up to max const maxArmor = purchases.armor * 50; if (maxArmor > 0) { armor = Math.min(maxArmor, armor + 25); updateArmorBar(); } else { // If player hasn't purchased armor, give them temporary armor armor = Math.min(50, armor + 25); document.getElementById('armor-bar-container').style.display = 'block'; updateArmorBar(); } break; } // Play pickup sound playSound('pickup'); // Show pickup text showPickupText(pickup.type); // Remove pickup pickup.element.remove(); pickups.splice(i, 1); } } } // Show pickup collected text function showPickupText(type) { const text = document.createElement('div'); text.className = 'floating-text'; text.style.left = `${window.innerWidth / 2}px`; text.style.top = `${window.innerHeight / 2 - 50}px`; switch (type) { case 'health': text.textContent = "+25 Health"; text.style.color = "#00ff00"; break; case 'ammo': text.textContent = "Ammo Refilled"; text.style.color = "#ffff00"; break; case 'armor': text.textContent = "+25 Armor"; text.style.color = "#00aaff"; break; } document.getElementById('ui-layer').appendChild(text); // Animate let opacity = 1; let posY = window.innerHeight / 2 - 50; const animate = () => { opacity -= 0.02; posY -= 1; text.style.opacity = opacity; text.style.top = `${posY}px`; if (opacity > 0) { requestAnimationFrame(animate); } else { text.remove(); } }; animate(); } // Update positions of pickup elements on screen function updatePickupPositions() { pickups.forEach(pickup => { const screenPos = worldToScreen(pickup.position); pickup.element.style.left = `${screenPos.x - 10}px`; pickup.element.style.top = `${screenPos.y - 10}px`; }); } // Check if bullet hit zombies function checkZombieHits(x, y, weapon) { const worldPosition = screenToWorld(x, y); // Get damage, accounting for power-ups let damage = weapon.damage; // Apply damage boost power-up if (powerUps.damageBoost.active) { damage *= 1.5; } // Check each zombie for (let i = zombies.length - 1; i >= 0; i--) { const zombie = zombies[i]; // Get zombie world position const zombiePosition = zombie.mesh.position.clone(); // Calculate horizontal distance (2D gameplay) const dx = worldPosition.x - zombiePosition.x; const dy = worldPosition.y - zombiePosition.y; const distance = Math.sqrt(dx * dx + dy * dy); // Check if hit (simple circular collision) if (distance < 20) { // Create blood splatter createBloodSplatter(x, y); // Show hit marker showHitMarker(); // Play hit sound playSound('zombie_hit'); // Check if headshot const headWorldPos = zombie.mesh.position.clone(); headWorldPos.y += zombie.headPosition.y; const headDistance = Math.sqrt( Math.pow(worldPosition.x - headWorldPos.x, 2) + Math.pow(worldPosition.y - headWorldPos.y, 2) ); let finalDamage = damage; // Headshot does more damage if (headDistance < 10) { finalDamage *= config.headShotMultiplier; // Show headshot text showHeadshotText(x, y); } // Apply damage zombie.health -= finalDamage; // Update health bar zombie.healthFill.style.width = `${(zombie.health / zombie.maxHealth) * 100}%`; // Check if zombie died if (zombie.health <= 0) { // Remove zombie scene.remove(zombie.mesh); zombie.healthBar.remove(); // Update score and cash score += zombie.value; cash += zombie.value / 2; updateScore(); updateCashDisplay(); // Create death effects const screenPos = worldToScreen(zombie.mesh.position); createZombieDeathEffects(screenPos.x, screenPos.y); // Play death sound playSound('zombie_death'); // Remove from array zombies.splice(i, 1); zombiesKilled++; // Possibly drop pickup if (Math.random() < config.pickupChance) { createPickup(zombie.mesh.position.x, zombie.mesh.position.y); } // Update minimap updateMinimap(); // Check if wave completed if (zombiesKilled >= config.zombiesPerWave + (wave - 1) * config.waveIncrement) { startNextWave(); } } // Only hit one zombie per bullet break; } } } // Show hit marker animation function showHitMarker() { const hitMarker = document.getElementById('hit-marker'); hitMarker.style.display = 'block'; hitMarker.style.transform = 'translate(-50%, -50%) scale(1.5)'; setTimeout(() => { hitMarker.style.transform = 'translate(-50%, -50%) scale(1)'; setTimeout(() => { hitMarker.style.display = 'none'; }, 100); }, 100); } // Show headshot text function showHeadshotText(x, y) { const headshot = document.createElement('div'); headshot.textContent = 'HEADSHOT!'; headshot.style.position = 'absolute'; headshot.style.left = `${x}px`; headshot.style.top = `${y - 30}px`; headshot.style.color = '#ff0000'; headshot.style.fontSize = '18px'; headshot.style.fontWeight = 'bold'; headshot.style.textShadow = '0 0 5px #000'; headshot.style.pointerEvents = 'none'; document.getElementById('ui-layer').appendChild(headshot); // Animation let opacity = 1; let yPos = y - 30; const animate = () => { opacity -= 0.02; yPos -= 0.5; headshot.style.opacity = opacity; headshot.style.top = `${yPos}px`; if (opacity > 0) { requestAnimationFrame(animate); } else { headshot.remove(); } }; animate(); } // Start reload animation function startReload() { if (reloading) return; const weapon = weapons[activeWeaponIndex]; // Don't reload if ammo is already full if (weapon.ammo === weapon.maxAmmo) return; reloading = true; document.getElementById('reload-message').style.display = 'block'; document.getElementById('reload-message').textContent = 'RELOADING...'; // Play reload sound playSound(weapon.reloadSound); setTimeout(() => { weapon.ammo = weapon.maxAmmo; reloading = false; document.getElementById('reload-message').style.display = 'none'; updateAmmoDisplay(); }, weapon.reloadTime); } // Switch weapons function switchWeapon(index) { // Check if weapon is valid if (!weaponValid(index)) return; // Cancel current reload if in progress if (reloading) { document.getElementById('reload-message').style.display