| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>DOOM Style Game</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| body { |
| margin: 0; |
| overflow: hidden; |
| background-color: #000; |
| font-family: 'Courier New', monospace; |
| color: white; |
| touch-action: none; |
| } |
| #gameCanvas { |
| display: block; |
| width: 100%; |
| height: 100%; |
| } |
| #ui { |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| width: 100%; |
| padding: 20px; |
| box-sizing: border-box; |
| background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| } |
| #healthAmmo { |
| display: flex; |
| justify-content: space-between; |
| width: 100%; |
| max-width: 600px; |
| margin-bottom: 10px; |
| } |
| #weapon { |
| font-size: 24px; |
| margin-bottom: 10px; |
| text-shadow: 0 0 5px red; |
| } |
| #startScreen { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.9); |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| color: red; |
| text-align: center; |
| z-index: 10; |
| } |
| #startButton { |
| padding: 15px 30px; |
| font-size: 20px; |
| background-color: #8B0000; |
| color: white; |
| border: 2px solid #FF0000; |
| border-radius: 5px; |
| cursor: pointer; |
| margin-top: 30px; |
| font-family: 'Courier New', monospace; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| } |
| #startButton:hover { |
| background-color: #FF0000; |
| } |
| #crosshair { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| width: 20px; |
| height: 20px; |
| transform: translate(-50%, -50%); |
| pointer-events: none; |
| } |
| #crosshair::before, #crosshair::after { |
| content: ''; |
| position: absolute; |
| background-color: red; |
| } |
| #crosshair::before { |
| width: 20px; |
| height: 2px; |
| top: 9px; |
| left: 0; |
| } |
| #crosshair::after { |
| width: 2px; |
| height: 20px; |
| left: 9px; |
| top: 0; |
| } |
| #gameOverScreen { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.9); |
| display: none; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| color: red; |
| text-align: center; |
| z-index: 10; |
| } |
| #restartButton { |
| padding: 15px 30px; |
| font-size: 20px; |
| background-color: #8B0000; |
| color: white; |
| border: 2px solid #FF0000; |
| border-radius: 5px; |
| cursor: pointer; |
| margin-top: 30px; |
| font-family: 'Courier New', monospace; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| } |
| #restartButton:hover { |
| background-color: #FF0000; |
| } |
| #hud { |
| position: absolute; |
| top: 10px; |
| left: 10px; |
| font-size: 16px; |
| color: white; |
| text-shadow: 0 0 5px black; |
| } |
| #enemiesLeft { |
| position: absolute; |
| top: 10px; |
| right: 10px; |
| font-size: 16px; |
| color: white; |
| text-shadow: 0 0 5px black; |
| } |
| .health-bar, .ammo-bar { |
| width: 200px; |
| height: 20px; |
| border: 2px solid #333; |
| border-radius: 3px; |
| overflow: hidden; |
| position: relative; |
| } |
| .health-fill { |
| height: 100%; |
| background: linear-gradient(to right, #8B0000, #FF0000); |
| transition: width 0.3s; |
| } |
| .ammo-fill { |
| height: 100%; |
| background: linear-gradient(to right, #006400, #00FF00); |
| transition: width 0.3s; |
| } |
| #weaponImage { |
| width: 200px; |
| height: 100px; |
| background-size: contain; |
| background-repeat: no-repeat; |
| background-position: center; |
| margin-bottom: 10px; |
| } |
| #bloodEffect { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(255, 0, 0, 0); |
| pointer-events: none; |
| transition: background-color 0.1s; |
| z-index: 5; |
| } |
| #damageIndicator { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| font-size: 24px; |
| color: red; |
| opacity: 0; |
| pointer-events: none; |
| transition: opacity 0.3s; |
| text-shadow: 0 0 5px black; |
| } |
| </style> |
| </head> |
| <body> |
| <canvas id="gameCanvas"></canvas> |
| <div id="crosshair"></div> |
| <div id="bloodEffect"></div> |
| <div id="damageIndicator">HIT!</div> |
| |
| <div id="hud"> |
| <div>Level: <span id="level">1</span></div> |
| <div>Kills: <span id="kills">0</span></div> |
| </div> |
| |
| <div id="enemiesLeft"> |
| Enemies: <span id="enemiesCount">0</span> |
| </div> |
| |
| <div id="ui"> |
| <div id="weaponImage"></div> |
| <div id="weapon">PISTOL</div> |
| <div id="healthAmmo"> |
| <div> |
| <div>HEALTH</div> |
| <div class="health-bar"> |
| <div class="health-fill" id="healthBar"></div> |
| </div> |
| </div> |
| <div> |
| <div>AMMO</div> |
| <div class="ammo-bar"> |
| <div class="ammo-fill" id="ammoBar"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="startScreen"> |
| <h1 class="text-4xl font-bold mb-4">DOOM STYLE GAME</h1> |
| <p class="text-xl mb-8">KILL ALL DEMONS TO ADVANCE TO THE NEXT LEVEL</p> |
| <p class="mb-2">WASD - Move</p> |
| <p class="mb-2">Mouse - Look and Shoot</p> |
| <p class="mb-2">R - Reload</p> |
| <p class="mb-2">1-3 - Switch Weapons</p> |
| <p class="mb-2">Space - Jump</p> |
| <button id="startButton">START GAME</button> |
| </div> |
| |
| <div id="gameOverScreen"> |
| <h1 class="text-4xl font-bold mb-4">GAME OVER</h1> |
| <p class="text-xl mb-2">You killed <span id="finalKills">0</span> demons</p> |
| <p class="text-xl mb-8">Reached level <span id="finalLevel">1</span></p> |
| <button id="restartButton">TRY AGAIN</button> |
| </div> |
|
|
| <script> |
| |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const startScreen = document.getElementById('startScreen'); |
| const startButton = document.getElementById('startButton'); |
| const gameOverScreen = document.getElementById('gameOverScreen'); |
| const restartButton = document.getElementById('restartButton'); |
| const weaponImage = document.getElementById('weaponImage'); |
| const weaponDisplay = document.getElementById('weapon'); |
| const healthBar = document.getElementById('healthBar'); |
| const ammoBar = document.getElementById('ammoBar'); |
| const levelDisplay = document.getElementById('level'); |
| const killsDisplay = document.getElementById('kills'); |
| const enemiesCountDisplay = document.getElementById('enemiesCount'); |
| const finalKillsDisplay = document.getElementById('finalKills'); |
| const finalLevelDisplay = document.getElementById('finalLevel'); |
| const bloodEffect = document.getElementById('bloodEffect'); |
| const damageIndicator = document.getElementById('damageIndicator'); |
| |
| |
| canvas.width = window.innerWidth; |
| canvas.height = window.innerHeight; |
| |
| |
| let gameRunning = false; |
| let level = 1; |
| let kills = 0; |
| let enemiesLeft = 0; |
| |
| |
| const player = { |
| x: 1.5, |
| y: 1.5, |
| dirX: -1, |
| dirY: 0, |
| planeX: 0, |
| planeY: 0.66, |
| moveSpeed: 0.05, |
| rotSpeed: 0.03, |
| health: 100, |
| weapons: [ |
| { |
| name: "PISTOL", |
| damage: 25, |
| ammo: 12, |
| maxAmmo: 12, |
| reloadTime: 1000, |
| fireRate: 500, |
| range: 10, |
| accuracy: 0.95, |
| color: "#888", |
| image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100'%3E%3Crect x='20' y='40' width='120' height='20' fill='%23ccc'/%3E%3Crect x='140' y='30' width='40' height='40' fill='%23aaa'/%3E%3Crect x='180' y='40' width='10' height='20' fill='%23888'/%3E%3C/svg%3E" |
| }, |
| { |
| name: "SHOTGUN", |
| damage: 50, |
| ammo: 6, |
| maxAmmo: 6, |
| reloadTime: 1500, |
| fireRate: 1000, |
| range: 5, |
| accuracy: 0.7, |
| color: "#964B00", |
| image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100'%3E%3Crect x='20' y='40' width='150' height='20' fill='%23b87333'/%3E%3Crect x='170' y='20' width='20' height='60' fill='%238B4513'/%3E%3C/svg%3E" |
| }, |
| { |
| name: "CHAINGUN", |
| damage: 15, |
| ammo: 50, |
| maxAmmo: 50, |
| reloadTime: 2000, |
| fireRate: 100, |
| range: 15, |
| accuracy: 0.85, |
| color: "#333", |
| image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100'%3E%3Crect x='20' y='40' width='150' height='20' fill='%23444'/%3E%3Crect x='170' y='30' width='20' height='40' fill='%23222'/%3E%3Ccircle cx='40' cy='50' r='15' fill='%23555'/%3E%3C/svg%3E" |
| } |
| ], |
| currentWeapon: 0, |
| lastShot: 0, |
| reloading: false, |
| isMoving: false, |
| isShooting: false, |
| jumpHeight: 0, |
| isJumping: false |
| }; |
| |
| |
| let map = [ |
| [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
| [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] |
| ]; |
| |
| |
| const wallTextures = [ |
| '#8B0000', |
| '#006400', |
| '#00008B', |
| '#4B0082', |
| '#8B4513' |
| ]; |
| |
| |
| let enemies = []; |
| |
| |
| function generateLevel() { |
| |
| enemies = []; |
| |
| |
| const size = 10 + level * 2; |
| map = Array(size).fill().map(() => Array(size).fill(1)); |
| |
| |
| const stack = []; |
| const visited = Array(size).fill().map(() => Array(size).fill(false)); |
| |
| |
| player.x = 1.5; |
| player.y = 1.5; |
| map[1][1] = 0; |
| visited[1][1] = true; |
| stack.push([1, 1]); |
| |
| while (stack.length > 0) { |
| const [x, y] = stack[stack.length - 1]; |
| const directions = [ |
| [0, 1], [1, 0], [0, -1], [-1, 0] |
| ].sort(() => Math.random() - 0.5); |
| |
| let moved = false; |
| |
| for (const [dx, dy] of directions) { |
| const nx = x + dx * 2; |
| const ny = y + dy * 2; |
| |
| if (nx > 0 && nx < size - 1 && ny > 0 && ny < size - 1 && !visited[nx][ny]) { |
| map[x + dx][y + dy] = 0; |
| map[nx][ny] = 0; |
| visited[nx][ny] = true; |
| stack.push([nx, ny]); |
| moved = true; |
| break; |
| } |
| } |
| |
| if (!moved) { |
| stack.pop(); |
| } |
| } |
| |
| |
| for (let i = 1; i < size - 1; i++) { |
| for (let j = 1; j < size - 1; j++) { |
| if (map[i][j] === 0 && Math.random() < 0.1) { |
| map[i][j] = 2 + Math.floor(Math.random() * (wallTextures.length - 1)); |
| } |
| } |
| } |
| |
| |
| const exitX = size - 2; |
| const exitY = size - 2; |
| map[exitX][exitY] = 0; |
| |
| |
| enemiesLeft = 5 + level * 3; |
| for (let i = 0; i < enemiesLeft; i++) { |
| let x, y; |
| do { |
| x = 1 + Math.floor(Math.random() * (size - 2)); |
| y = 1 + Math.floor(Math.random() * (size - 2)); |
| } while (map[x][y] !== 0 || (x === 1 && y === 1) || (x === exitX && y === exitY)); |
| |
| enemies.push({ |
| x: x + 0.5, |
| y: y + 0.5, |
| health: 50 + level * 10, |
| speed: 0.02 + level * 0.005, |
| damage: 10 + level * 2, |
| color: `hsl(${Math.random() * 60}, 100%, 50%)`, |
| lastAttack: 0, |
| attackCooldown: 1000, |
| size: 0.5 |
| }); |
| } |
| |
| |
| enemiesCountDisplay.textContent = enemiesLeft; |
| } |
| |
| |
| function castRays() { |
| const width = canvas.width; |
| const height = canvas.height; |
| |
| for (let x = 0; x < width; x++) { |
| |
| const cameraX = 2 * x / width - 1; |
| const rayDirX = player.dirX + player.planeX * cameraX; |
| const rayDirY = player.dirY + player.planeY * cameraX; |
| |
| |
| let mapX = Math.floor(player.x); |
| let mapY = Math.floor(player.y); |
| |
| |
| let sideDistX, sideDistY; |
| |
| |
| const deltaDistX = Math.abs(1 / rayDirX); |
| const deltaDistY = Math.abs(1 / rayDirY); |
| |
| |
| let stepX, stepY; |
| |
| |
| let hit = false; |
| |
| let side; |
| |
| let perpWallDist; |
| |
| |
| if (rayDirX < 0) { |
| stepX = -1; |
| sideDistX = (player.x - mapX) * deltaDistX; |
| } else { |
| stepX = 1; |
| sideDistX = (mapX + 1.0 - player.x) * deltaDistX; |
| } |
| |
| if (rayDirY < 0) { |
| stepY = -1; |
| sideDistY = (player.y - mapY) * deltaDistY; |
| } else { |
| stepY = 1; |
| sideDistY = (mapY + 1.0 - player.y) * deltaDistY; |
| } |
| |
| |
| while (!hit) { |
| |
| if (sideDistX < sideDistY) { |
| sideDistX += deltaDistX; |
| mapX += stepX; |
| side = 0; |
| } else { |
| sideDistY += deltaDistY; |
| mapY += stepY; |
| side = 1; |
| } |
| |
| |
| if (mapX < 0 || mapX >= map.length || mapY < 0 || mapY >= map[0].length) { |
| hit = true; |
| } else if (map[mapX][mapY] > 0) { |
| hit = true; |
| } |
| } |
| |
| |
| if (side === 0) { |
| perpWallDist = (mapX - player.x + (1 - stepX) / 2) / rayDirX; |
| } else { |
| perpWallDist = (mapY - player.y + (1 - stepY) / 2) / rayDirY; |
| } |
| |
| |
| let lineHeight = Math.floor(height / perpWallDist); |
| |
| |
| let drawStart = -lineHeight / 2 + height / 2; |
| if (drawStart < 0) drawStart = 0; |
| let drawEnd = lineHeight / 2 + height / 2; |
| if (drawEnd >= height) drawEnd = height - 1; |
| |
| |
| let color; |
| if (mapX < 0 || mapX >= map.length || mapY < 0 || mapY >= map[0].length) { |
| color = '#000'; |
| } else { |
| const wallType = map[mapX][mapY]; |
| color = wallTextures[wallType - 1] || '#FFF'; |
| } |
| |
| |
| if (side === 1) { |
| color = shadeColor(color, -30); |
| } |
| |
| |
| ctx.fillStyle = color; |
| ctx.fillRect(x, drawStart + player.jumpHeight, 1, drawEnd - drawStart); |
| |
| |
| ctx.fillStyle = '#333'; |
| ctx.fillRect(x, drawEnd + player.jumpHeight, 1, height - drawEnd); |
| } |
| } |
| |
| |
| function drawEnemies() { |
| const width = canvas.width; |
| const height = canvas.height; |
| |
| |
| enemies.sort((a, b) => { |
| const distA = Math.pow(player.x - a.x, 2) + Math.pow(player.y - a.y, 2); |
| const distB = Math.pow(player.x - b.x, 2) + Math.pow(player.y - b.y, 2); |
| return distB - distA; |
| }); |
| |
| for (const enemy of enemies) { |
| |
| const relX = enemy.x - player.x; |
| const relY = enemy.y - player.y; |
| |
| |
| const invDet = 1.0 / (player.planeX * player.dirY - player.dirX * player.planeY); |
| const transformX = invDet * (player.dirY * relX - player.dirX * relY); |
| const transformY = invDet * (-player.planeY * relX + player.planeX * relY); |
| |
| |
| if (transformY <= 0) continue; |
| |
| |
| const spriteScreenX = Math.floor((width / 2) * (1 + transformX / transformY)); |
| |
| |
| const spriteHeight = Math.abs(Math.floor(height / transformY)); |
| const spriteWidth = spriteHeight; |
| |
| |
| let drawStartX = -spriteWidth / 2 + spriteScreenX; |
| let drawEndX = spriteWidth / 2 + spriteScreenX; |
| let drawStartY = -spriteHeight / 2 + height / 2; |
| let drawEndY = spriteHeight / 2 + height / 2; |
| |
| |
| if (drawStartX < 0) drawStartX = 0; |
| if (drawEndX >= width) drawEndX = width - 1; |
| if (drawStartY < 0) drawStartY = 0; |
| if (drawEndY >= height) drawEndY = height - 1; |
| |
| |
| for (let stripe = drawStartX; stripe < drawEndX; stripe++) { |
| const texX = Math.floor((stripe - (-spriteWidth / 2 + spriteScreenX)) * enemy.size / spriteWidth); |
| |
| if (transformY > 0 && stripe > 0 && stripe < width) { |
| for (let y = drawStartY; y < drawEndY; y++) { |
| const d = (y - (-spriteHeight / 2 + height / 2)) * 256 / spriteHeight; |
| const texY = Math.floor(d * enemy.size / spriteHeight); |
| |
| |
| if (texX >= 0 && texX < enemy.size * 100 && texY >= 0 && texY < enemy.size * 100) { |
| ctx.fillStyle = enemy.color; |
| ctx.fillRect(stripe, y + player.jumpHeight, 1, 1); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| |
| function drawWeapon() { |
| const weapon = player.weapons[player.currentWeapon]; |
| weaponImage.style.backgroundImage = `url("${weapon.image}")`; |
| weaponDisplay.textContent = weapon.name; |
| |
| |
| let bobOffset = 0; |
| if (player.isMoving) { |
| bobOffset = Math.sin(Date.now() / 100) * 5; |
| } |
| |
| |
| let recoilOffset = 0; |
| if (player.isShooting) { |
| recoilOffset = Math.sin(Date.now() / 50) * 10; |
| } |
| |
| weaponImage.style.transform = `translateY(${bobOffset + recoilOffset}px)`; |
| } |
| |
| |
| function updateHUD() { |
| const weapon = player.weapons[player.currentWeapon]; |
| healthBar.style.width = `${player.health}%`; |
| ammoBar.style.width = `${(weapon.ammo / weapon.maxAmmo) * 100}%`; |
| levelDisplay.textContent = level; |
| killsDisplay.textContent = kills; |
| enemiesCountDisplay.textContent = enemiesLeft; |
| |
| |
| if (player.health < 30) { |
| bloodEffect.style.backgroundColor = `rgba(255, 0, 0, ${0.3 - (player.health / 100)})`; |
| } else { |
| bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0)'; |
| } |
| } |
| |
| |
| function movePlayer() { |
| if (!gameRunning) return; |
| |
| |
| const moveSpeed = player.moveSpeed; |
| const rotSpeed = player.rotSpeed; |
| |
| |
| if (keys.ArrowLeft) { |
| const oldDirX = player.dirX; |
| player.dirX = player.dirX * Math.cos(rotSpeed) - player.dirY * Math.sin(rotSpeed); |
| player.dirY = oldDirX * Math.sin(rotSpeed) + player.dirY * Math.cos(rotSpeed); |
| |
| const oldPlaneX = player.planeX; |
| player.planeX = player.planeX * Math.cos(rotSpeed) - player.planeY * Math.sin(rotSpeed); |
| player.planeY = oldPlaneX * Math.sin(rotSpeed) + player.planeY * Math.cos(rotSpeed); |
| } |
| |
| if (keys.ArrowRight) { |
| const oldDirX = player.dirX; |
| player.dirX = player.dirX * Math.cos(-rotSpeed) - player.dirY * Math.sin(-rotSpeed); |
| player.dirY = oldDirX * Math.sin(-rotSpeed) + player.dirY * Math.cos(-rotSpeed); |
| |
| const oldPlaneX = player.planeX; |
| player.planeX = player.planeX * Math.cos(-rotSpeed) - player.planeY * Math.sin(-rotSpeed); |
| player.planeY = oldPlaneX * Math.sin(-rotSpeed) + player.planeY * Math.cos(-rotSpeed); |
| } |
| |
| |
| let moveX = 0, moveY = 0; |
| player.isMoving = false; |
| |
| if (keys.w) { |
| moveX += player.dirX * moveSpeed; |
| moveY += player.dirY * moveSpeed; |
| player.isMoving = true; |
| } |
| |
| if (keys.s) { |
| moveX -= player.dirX * moveSpeed; |
| moveY -= player.dirY * moveSpeed; |
| player.isMoving = true; |
| } |
| |
| |
| if (keys.a) { |
| moveX -= player.planeX * moveSpeed; |
| moveY -= player.planeY * moveSpeed; |
| player.isMoving = true; |
| } |
| |
| if (keys.d) { |
| moveX += player.planeX * moveSpeed; |
| moveY += player.planeY * moveSpeed; |
| player.isMoving = true; |
| } |
| |
| |
| if (keys[' '] && !player.isJumping) { |
| player.isJumping = true; |
| player.jumpHeight = -20; |
| } |
| |
| |
| if (player.isJumping) { |
| player.jumpHeight += 2; |
| if (player.jumpHeight >= 0) { |
| player.jumpHeight = 0; |
| player.isJumping = false; |
| } |
| } |
| |
| |
| if (map[Math.floor(player.x + moveX)][Math.floor(player.y)] === 0) { |
| player.x += moveX; |
| } |
| |
| if (map[Math.floor(player.x)][Math.floor(player.y + moveY)] === 0) { |
| player.y += moveY; |
| } |
| |
| |
| if (Math.floor(player.x) === map.length - 2 && Math.floor(player.y) === map[0].length - 2) { |
| if (enemiesLeft === 0) { |
| level++; |
| generateLevel(); |
| } |
| } |
| } |
| |
| |
| function updateEnemies() { |
| const now = Date.now(); |
| |
| for (let i = enemies.length - 1; i >= 0; i--) { |
| const enemy = enemies[i]; |
| |
| |
| const dx = player.x - enemy.x; |
| const dy = player.y - enemy.y; |
| const dist = Math.sqrt(dx * dx + dy * dy); |
| |
| if (dist > 0.5) { |
| enemy.x += (dx / dist) * enemy.speed; |
| enemy.y += (dy / dist) * enemy.speed; |
| |
| |
| if (map[Math.floor(enemy.x)][Math.floor(enemy.y)] !== 0) { |
| enemy.x -= (dx / dist) * enemy.speed; |
| enemy.y -= (dy / dist) * enemy.speed; |
| } |
| } |
| |
| |
| if (dist < 1.5 && now - enemy.lastAttack > enemy.attackCooldown) { |
| player.health -= enemy.damage; |
| enemy.lastAttack = now; |
| |
| |
| damageIndicator.style.opacity = 1; |
| setTimeout(() => { |
| damageIndicator.style.opacity = 0; |
| }, 300); |
| |
| |
| bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; |
| setTimeout(() => { |
| bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0)'; |
| }, 100); |
| |
| |
| if (player.health <= 0) { |
| gameOver(); |
| } |
| } |
| |
| |
| if (enemy.health <= 0) { |
| enemies.splice(i, 1); |
| enemiesLeft--; |
| kills++; |
| |
| |
| enemiesCountDisplay.textContent = enemiesLeft; |
| killsDisplay.textContent = kills; |
| } |
| } |
| } |
| |
| |
| function shoot() { |
| const now = Date.now(); |
| const weapon = player.weapons[player.currentWeapon]; |
| |
| |
| if (now - player.lastShot < weapon.fireRate || player.reloading || weapon.ammo <= 0) { |
| return; |
| } |
| |
| player.lastShot = now; |
| player.isShooting = true; |
| setTimeout(() => { |
| player.isShooting = false; |
| }, 100); |
| |
| |
| weapon.ammo--; |
| |
| |
| for (let i = 0; i < enemies.length; i++) { |
| const enemy = enemies[i]; |
| |
| |
| const dx = enemy.x - player.x; |
| const dy = enemy.y - player.y; |
| const dist = Math.sqrt(dx * dx + dy * dy); |
| |
| |
| const playerAngle = Math.atan2(player.dirY, player.dirX); |
| const enemyAngle = Math.atan2(dy, dx); |
| let angleDiff = Math.abs(playerAngle - enemyAngle); |
| |
| |
| if (angleDiff > Math.PI) { |
| angleDiff = 2 * Math.PI - angleDiff; |
| } |
| |
| |
| if (dist < weapon.range && angleDiff < 0.5 * weapon.accuracy) { |
| |
| enemy.health -= weapon.damage; |
| |
| |
| damageIndicator.style.opacity = 1; |
| setTimeout(() => { |
| damageIndicator.style.opacity = 0; |
| }, 300); |
| |
| |
| enemy.color = `hsl(${Math.random() * 60}, 100%, 70%)`; |
| setTimeout(() => { |
| enemy.color = `hsl(${Math.random() * 60}, 100%, 50%)`; |
| }, 100); |
| } |
| } |
| |
| |
| if (weapon.ammo <= 0) { |
| reload(); |
| } |
| } |
| |
| |
| function reload() { |
| if (player.reloading) return; |
| |
| const weapon = player.weapons[player.currentWeapon]; |
| if (weapon.ammo === weapon.maxAmmo) return; |
| |
| player.reloading = true; |
| setTimeout(() => { |
| weapon.ammo = weapon.maxAmmo; |
| player.reloading = false; |
| }, weapon.reloadTime); |
| } |
| |
| |
| function gameOver() { |
| gameRunning = false; |
| finalKillsDisplay.textContent = kills; |
| finalLevelDisplay.textContent = level; |
| gameOverScreen.style.display = 'flex'; |
| } |
| |
| |
| function gameLoop() { |
| if (!gameRunning) return; |
| |
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.fillStyle = '#111'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height / 2); |
| |
| |
| castRays(); |
| |
| |
| drawEnemies(); |
| |
| |
| movePlayer(); |
| updateEnemies(); |
| |
| |
| drawWeapon(); |
| |
| |
| updateHUD(); |
| |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| const keys = { |
| w: false, |
| a: false, |
| s: false, |
| d: false, |
| ' ': false, |
| ArrowLeft: false, |
| ArrowRight: false, |
| r: false |
| }; |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key in keys) keys[e.key] = true; |
| |
| |
| if (e.key >= '1' && e.key <= '3') { |
| player.currentWeapon = parseInt(e.key) - 1; |
| } |
| |
| |
| if (e.key === 'r') { |
| reload(); |
| } |
| }); |
| |
| document.addEventListener('keyup', (e) => { |
| if (e.key in keys) keys[e.key] = false; |
| }); |
| |
| |
| let mouseX = 0; |
| let mouseDown = false; |
| |
| canvas.addEventListener('mousedown', () => { |
| mouseDown = true; |
| if (gameRunning) shoot(); |
| }); |
| |
| canvas.addEventListener('mouseup', () => { |
| mouseDown = false; |
| }); |
| |
| canvas.addEventListener('mousemove', (e) => { |
| if (!gameRunning) return; |
| |
| const movementX = e.movementX || 0; |
| |
| |
| if (movementX !== 0) { |
| const rotSpeed = 0.002 * movementX; |
| const oldDirX = player.dirX; |
| player.dirX = player.dirX * Math.cos(rotSpeed) - player.dirY * Math.sin(rotSpeed); |
| player.dirY = oldDirX * Math.sin(rotSpeed) + player.dirY * Math.cos(rotSpeed); |
| |
| const oldPlaneX = player.planeX; |
| player.planeX = player.planeX * Math.cos(rotSpeed) - player.planeY * Math.sin(rotSpeed); |
| player.planeY = oldPlaneX * Math.sin(rotSpeed) + player.planeY * Math.cos(rotSpeed); |
| } |
| }); |
| |
| |
| let touchStartX = 0; |
| |
| canvas.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| touchStartX = e.touches[0].clientX; |
| if (gameRunning) shoot(); |
| }); |
| |
| canvas.addEventListener('touchmove', (e) => { |
| e.preventDefault(); |
| if (!gameRunning) return; |
| |
| const touchX = e.touches[0].clientX; |
| const movementX = (touchX - touchStartX) * 0.1; |
| touchStartX = touchX; |
| |
| |
| if (movementX !== 0) { |
| const rotSpeed = 0.002 * movementX; |
| const oldDirX = player.dirX; |
| player.dirX = player.dirX * Math.cos(rotSpeed) - player.dirY * Math.sin(rotSpeed); |
| player.dirY = oldDirX * Math.sin(rotSpeed) + player.dirY * Math.cos(rotSpeed); |
| |
| const oldPlaneX = player.planeX; |
| player.planeX = player.planeX * Math.cos(rotSpeed) - player.planeY * Math.sin(rotSpeed); |
| player.planeY = oldPlaneX * Math.sin(rotSpeed) + player.planeY * Math.cos(rotSpeed); |
| } |
| }); |
| |
| canvas.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| }); |
| |
| |
| function startGame() { |
| startScreen.style.display = 'none'; |
| gameRunning = true; |
| |
| |
| level = 1; |
| kills = 0; |
| player.health = 100; |
| player.currentWeapon = 0; |
| player.weapons.forEach(w => w.ammo = w.maxAmmo); |
| |
| |
| generateLevel(); |
| |
| |
| gameLoop(); |
| } |
| |
| |
| function restartGame() { |
| gameOverScreen.style.display = 'none'; |
| startGame(); |
| } |
| |
| |
| startButton.addEventListener('click', startGame); |
| restartButton.addEventListener('click', restartGame); |
| |
| |
| window.addEventListener('resize', () => { |
| canvas.width = window.innerWidth; |
| canvas.height = window.innerHeight; |
| }); |
| |
| |
| function shadeColor(color, percent) { |
| let R = parseInt(color.substring(1, 3), 16); |
| let G = parseInt(color.substring(3, 5), 16); |
| let B = parseInt(color.substring(5, 7), 16); |
| |
| R = parseInt(R * (100 + percent) / 100); |
| G = parseInt(G * (100 + percent) / 100); |
| B = parseInt(B * (100 + percent) / 100); |
| |
| R = R < 255 ? R : 255; |
| G = G < 255 ? G : 255; |
| B = B < 255 ? B : 255; |
| |
| const RR = R.toString(16).length === 1 ? '0' + R.toString(16) : R.toString(16); |
| const GG = G.toString(16).length === 1 ? '0' + G.toString(16) : G.toString(16); |
| const BB = B.toString(16).length === 1 ? '0' + B.toString(16) : B.toString(16); |
| |
| return '#' + RR + GG + BB; |
| } |
| </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=Greats/clone" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |