| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>2D FPS 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> |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); |
| |
| body { |
| font-family: 'Press Start 2P', cursive; |
| margin: 0; |
| padding: 0; |
| overflow: hidden; |
| background-color: #111; |
| color: white; |
| } |
| |
| #gameContainer { |
| position: relative; |
| width: 100vw; |
| height: 100vh; |
| overflow: hidden; |
| perspective: 1000px; |
| } |
| |
| #gameCanvas { |
| position: absolute; |
| width: 100%; |
| height: 100%; |
| background-color: #222; |
| image-rendering: pixelated; |
| } |
| |
| #crosshair { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| width: 20px; |
| height: 20px; |
| pointer-events: none; |
| z-index: 100; |
| } |
| |
| #crosshair::before, #crosshair::after { |
| content: ''; |
| position: absolute; |
| background-color: rgba(255, 255, 255, 0.8); |
| } |
| |
| #crosshair::before { |
| width: 2px; |
| height: 12px; |
| top: 4px; |
| left: 9px; |
| } |
| |
| #crosshair::after { |
| width: 12px; |
| height: 2px; |
| top: 9px; |
| left: 4px; |
| } |
| |
| #uiContainer { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| pointer-events: none; |
| z-index: 50; |
| } |
| |
| #healthBar { |
| position: absolute; |
| bottom: 20px; |
| left: 20px; |
| width: 200px; |
| height: 30px; |
| background-color: rgba(70, 0, 0, 0.7); |
| border: 3px solid #444; |
| border-radius: 5px; |
| overflow: hidden; |
| } |
| |
| #healthFill { |
| height: 100%; |
| width: 100%; |
| background-color: #f00; |
| transition: width 0.3s; |
| } |
| |
| #ammoCount { |
| position: absolute; |
| bottom: 20px; |
| right: 20px; |
| font-size: 24px; |
| color: white; |
| text-shadow: 2px 2px 0 #000; |
| } |
| |
| #scoreDisplay { |
| position: absolute; |
| top: 20px; |
| left: 20px; |
| font-size: 24px; |
| color: white; |
| text-shadow: 2px 2px 0 #000; |
| } |
| |
| #weaponDisplay { |
| position: absolute; |
| bottom: 70px; |
| right: 20px; |
| font-size: 24px; |
| color: white; |
| text-shadow: 2px 2px 0 #000; |
| } |
| |
| #gameOverScreen { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.8); |
| display: none; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| z-index: 200; |
| } |
| |
| #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; |
| z-index: 150; |
| } |
| |
| .btn { |
| background-color: #f00; |
| color: white; |
| border: none; |
| padding: 15px 30px; |
| font-size: 18px; |
| border-radius: 5px; |
| cursor: pointer; |
| margin-top: 30px; |
| font-family: 'Press Start 2P', cursive; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| box-shadow: 0 0 10px rgba(255, 0, 0, 0.5); |
| transition: all 0.3s; |
| } |
| |
| .btn:hover { |
| background-color: #c00; |
| transform: translateY(-3px); |
| box-shadow: 0 0 20px rgba(255, 0, 0, 0.8); |
| } |
| |
| #bloodEffect { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(255, 0, 0, 0); |
| pointer-events: none; |
| z-index: 60; |
| transition: background-color 0.3s; |
| } |
| |
| #reloadIndicator { |
| position: absolute; |
| bottom: 120px; |
| right: 20px; |
| width: 100px; |
| height: 10px; |
| background-color: rgba(255, 255, 255, 0.3); |
| border-radius: 5px; |
| overflow: hidden; |
| display: none; |
| } |
| |
| #reloadFill { |
| height: 100%; |
| width: 0%; |
| background-color: #ff0; |
| transition: width 0.1s linear; |
| } |
| |
| #enemyHitEffect { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(255, 255, 255, 0); |
| pointer-events: none; |
| z-index: 60; |
| transition: background-color 0.1s; |
| } |
| |
| #mobileControls { |
| position: absolute; |
| bottom: 20px; |
| width: 100%; |
| display: none; |
| justify-content: space-between; |
| padding: 0 20px; |
| box-sizing: border-box; |
| } |
| |
| .mobile-btn { |
| width: 70px; |
| height: 70px; |
| background-color: rgba(255, 255, 255, 0.2); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-size: 24px; |
| pointer-events: auto; |
| user-select: none; |
| touch-action: manipulation; |
| } |
| |
| #shootBtn { |
| background-color: rgba(255, 0, 0, 0.5); |
| } |
| |
| #movePad { |
| position: relative; |
| width: 120px; |
| height: 120px; |
| background-color: rgba(255, 255, 255, 0.1); |
| border-radius: 50%; |
| } |
| |
| #moveKnob { |
| position: absolute; |
| width: 50px; |
| height: 50px; |
| background-color: rgba(255, 255, 255, 0.3); |
| border-radius: 50%; |
| top: 35px; |
| left: 35px; |
| } |
| |
| @media (max-width: 768px) { |
| #mobileControls { |
| display: flex; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="gameContainer"> |
| <canvas id="gameCanvas"></canvas> |
| <div id="crosshair"></div> |
| <div id="bloodEffect"></div> |
| <div id="enemyHitEffect"></div> |
| |
| <div id="uiContainer"> |
| <div id="healthBar"> |
| <div id="healthFill"></div> |
| </div> |
| <div id="ammoCount">30/30</div> |
| <div id="scoreDisplay">SCORE: 0</div> |
| <div id="weaponDisplay">PISTOL</div> |
| <div id="reloadIndicator"> |
| <div id="reloadFill"></div> |
| </div> |
| |
| <div id="mobileControls"> |
| <div id="movePad"> |
| <div id="moveKnob"></div> |
| </div> |
| <div id="shootBtn" class="mobile-btn"> |
| <i class="fas fa-fire"></i> |
| </div> |
| </div> |
| </div> |
| |
| <div id="gameOverScreen"> |
| <h1 class="text-5xl text-red-600 mb-8">GAME OVER</h1> |
| <p class="text-2xl mb-4">YOUR SCORE:</p> |
| <p class="text-4xl mb-12" id="finalScore">0</p> |
| <button id="restartBtn" class="btn">PLAY AGAIN</button> |
| </div> |
| |
| <div id="startScreen"> |
| <h1 class="text-6xl text-red-600 mb-8">2D FPS</h1> |
| <p class="text-xl mb-12">SURVIVE AS LONG AS YOU CAN</p> |
| <button id="startBtn" class="btn">START GAME</button> |
| <div class="mt-16 text-gray-400 text-center"> |
| <p class="mb-4">CONTROLS:</p> |
| <p class="mb-2">WASD - MOVE</p> |
| <p class="mb-2">MOUSE - AIM</p> |
| <p class="mb-2">LEFT CLICK - SHOOT</p> |
| <p class="mb-2">R - RELOAD</p> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const healthFill = document.getElementById('healthFill'); |
| const ammoCount = document.getElementById('ammoCount'); |
| const scoreDisplay = document.getElementById('scoreDisplay'); |
| const weaponDisplay = document.getElementById('weaponDisplay'); |
| const gameOverScreen = document.getElementById('gameOverScreen'); |
| const finalScore = document.getElementById('finalScore'); |
| const startScreen = document.getElementById('startScreen'); |
| const startBtn = document.getElementById('startBtn'); |
| const restartBtn = document.getElementById('restartBtn'); |
| const bloodEffect = document.getElementById('bloodEffect'); |
| const reloadIndicator = document.getElementById('reloadIndicator'); |
| const reloadFill = document.getElementById('reloadFill'); |
| const enemyHitEffect = document.getElementById('enemyHitEffect'); |
| const shootBtn = document.getElementById('shootBtn'); |
| const movePad = document.getElementById('movePad'); |
| const moveKnob = document.getElementById('moveKnob'); |
| |
| |
| let gameWidth = window.innerWidth; |
| let gameHeight = window.innerHeight; |
| let playerX = 0; |
| let playerY = 0; |
| let playerAngle = 0; |
| let playerSpeed = 0.1; |
| let health = 100; |
| let score = 0; |
| let gameRunning = false; |
| let lastShotTime = 0; |
| let lastEnemySpawn = 0; |
| let enemySpawnRate = 2000; |
| let isReloading = false; |
| let movePadActive = false; |
| let moveDirection = { x: 0, y: 0 }; |
| |
| |
| const weapons = { |
| pistol: { |
| name: "PISTOL", |
| damage: 25, |
| ammo: 30, |
| maxAmmo: 30, |
| reloadTime: 1000, |
| fireRate: 300, |
| color: "#aaa" |
| }, |
| rifle: { |
| name: "RIFLE", |
| damage: 15, |
| ammo: 90, |
| maxAmmo: 30, |
| reloadTime: 1500, |
| fireRate: 100, |
| color: "#5a5" |
| }, |
| shotgun: { |
| name: "SHOTGUN", |
| damage: 40, |
| ammo: 24, |
| maxAmmo: 8, |
| reloadTime: 2000, |
| fireRate: 800, |
| color: "#a55" |
| } |
| }; |
| |
| let currentWeapon = "pistol"; |
| let enemies = []; |
| let walls = []; |
| let particles = []; |
| |
| |
| canvas.width = gameWidth; |
| canvas.height = gameHeight; |
| |
| |
| startBtn.addEventListener('click', startGame); |
| |
| |
| restartBtn.addEventListener('click', startGame); |
| |
| |
| const keys = { |
| w: false, |
| a: false, |
| s: false, |
| d: false |
| }; |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key.toLowerCase() === 'w') keys.w = true; |
| if (e.key.toLowerCase() === 'a') keys.a = true; |
| if (e.key.toLowerCase() === 's') keys.s = true; |
| if (e.key.toLowerCase() === 'd') keys.d = true; |
| if (e.key === 'r' && gameRunning && !isReloading) reload(); |
| }); |
| |
| document.addEventListener('keyup', (e) => { |
| if (e.key.toLowerCase() === 'w') keys.w = false; |
| if (e.key.toLowerCase() === 'a') keys.a = false; |
| if (e.key.toLowerCase() === 's') keys.s = false; |
| if (e.key.toLowerCase() === 'd') keys.d = false; |
| }); |
| |
| |
| document.addEventListener('mousemove', (e) => { |
| if (!gameRunning) return; |
| |
| |
| const centerX = gameWidth / 2; |
| const centerY = gameHeight / 2; |
| const mouseX = e.clientX; |
| const mouseY = e.clientY; |
| |
| playerAngle = Math.atan2(mouseY - centerY, mouseX - centerX); |
| }); |
| |
| document.addEventListener('mousedown', (e) => { |
| if (e.button === 0 && gameRunning) { |
| shoot(); |
| } |
| }); |
| |
| |
| shootBtn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| if (gameRunning) shoot(); |
| }); |
| |
| movePad.addEventListener('touchstart', handleMoveStart); |
| document.addEventListener('touchmove', handleMoveMove); |
| document.addEventListener('touchend', handleMoveEnd); |
| |
| function handleMoveStart(e) { |
| e.preventDefault(); |
| movePadActive = true; |
| handleMoveMove(e); |
| } |
| |
| function handleMoveMove(e) { |
| if (!movePadActive) return; |
| e.preventDefault(); |
| |
| const padRect = movePad.getBoundingClientRect(); |
| const padCenterX = padRect.left + padRect.width / 2; |
| const padCenterY = padRect.top + padRect.height / 2; |
| |
| let touchX, touchY; |
| if (e.touches) { |
| touchX = e.touches[0].clientX; |
| touchY = e.touches[0].clientY; |
| } else { |
| touchX = e.clientX; |
| touchY = e.clientY; |
| } |
| |
| const dx = touchX - padCenterX; |
| const dy = touchY - padCenterY; |
| const distance = Math.min(50, Math.sqrt(dx * dx + dy * dy)); |
| const angle = Math.atan2(dy, dx); |
| |
| |
| moveDirection = { |
| x: Math.cos(angle) * (distance / 50), |
| y: Math.sin(angle) * (distance / 50) |
| }; |
| |
| |
| const knobX = Math.cos(angle) * distance; |
| const knobY = Math.sin(angle) * distance; |
| |
| moveKnob.style.transform = `translate(${knobX}px, ${knobY}px)`; |
| } |
| |
| function handleMoveEnd(e) { |
| e.preventDefault(); |
| movePadActive = false; |
| moveDirection = { x: 0, y: 0 }; |
| moveKnob.style.transform = 'translate(0, 0)'; |
| } |
| |
| function startGame() { |
| |
| health = 100; |
| score = 0; |
| playerX = 0; |
| playerY = 0; |
| playerAngle = 0; |
| enemies = []; |
| particles = []; |
| walls = []; |
| |
| |
| weapons.pistol.ammo = weapons.pistol.maxAmmo; |
| weapons.rifle.ammo = weapons.rifle.maxAmmo; |
| weapons.shotgun.ammo = weapons.shotgun.maxAmmo; |
| currentWeapon = "pistol"; |
| |
| |
| updateHealth(); |
| updateAmmo(); |
| updateScore(); |
| updateWeapon(); |
| |
| |
| gameOverScreen.style.display = 'none'; |
| startScreen.style.display = 'none'; |
| |
| |
| generateWalls(); |
| |
| |
| gameRunning = true; |
| lastEnemySpawn = Date.now(); |
| requestAnimationFrame(gameLoop); |
| } |
| |
| function gameLoop() { |
| if (!gameRunning) return; |
| |
| const now = Date.now(); |
| |
| |
| ctx.clearRect(0, 0, gameWidth, gameHeight); |
| |
| |
| ctx.fillStyle = '#333'; |
| ctx.fillRect(0, 0, gameWidth, gameHeight / 2); |
| ctx.fillStyle = '#555'; |
| ctx.fillRect(0, gameHeight / 2, gameWidth, gameHeight / 2); |
| |
| |
| drawWalls(); |
| |
| |
| let moveX = 0; |
| let moveY = 0; |
| |
| if (keys.w || moveDirection.y < -0.5) moveY -= playerSpeed; |
| if (keys.s || moveDirection.y > 0.5) moveY += playerSpeed; |
| if (keys.a || moveDirection.x < -0.5) moveX -= playerSpeed; |
| if (keys.d || moveDirection.x > 0.5) moveX += playerSpeed; |
| |
| |
| if (moveX !== 0 && moveY !== 0) { |
| moveX *= 0.7071; |
| moveY *= 0.7071; |
| } |
| |
| |
| const newX = playerX + moveX * Math.cos(playerAngle) - moveY * Math.sin(playerAngle); |
| const newY = playerY + moveX * Math.sin(playerAngle) + moveY * Math.cos(playerAngle); |
| |
| if (!checkWallCollision(newX, newY)) { |
| playerX = newX; |
| playerY = newY; |
| } |
| |
| |
| if (now - lastEnemySpawn > enemySpawnRate) { |
| spawnEnemy(); |
| lastEnemySpawn = now; |
| |
| |
| if (score > 0 && score % 5 === 0) { |
| enemySpawnRate = Math.max(500, enemySpawnRate - 100); |
| } |
| } |
| |
| |
| updateEnemies(); |
| |
| |
| updateParticles(); |
| |
| |
| drawWeapon(); |
| |
| |
| if (health <= 0) { |
| gameOver(); |
| return; |
| } |
| |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| function generateWalls() { |
| |
| for (let x = -10; x <= 10; x++) { |
| for (let y = -10; y <= 10; y++) { |
| |
| if (x === -10 || x === 10 || y === -10 || y === 10) { |
| walls.push({ x, y }); |
| } |
| |
| else if (Math.abs(x) > 2 || Math.abs(y) > 2) { |
| if (Math.random() < 0.2) { |
| walls.push({ x, y }); |
| } |
| } |
| } |
| } |
| } |
| |
| function drawWalls() { |
| const fov = Math.PI / 3; |
| const halfFov = fov / 2; |
| const numRays = gameWidth; |
| const rayAngleStep = fov / numRays; |
| |
| for (let i = 0; i < numRays; i++) { |
| const rayAngle = playerAngle - halfFov + i * rayAngleStep; |
| |
| |
| let rayX = playerX; |
| let rayY = playerY; |
| let rayDist = 0; |
| let hitWall = false; |
| let wallType = 0; |
| |
| |
| while (!hitWall && rayDist < 20) { |
| rayDist += 0.05; |
| rayX = playerX + Math.cos(rayAngle) * rayDist; |
| rayY = playerY + Math.sin(rayAngle) * rayDist; |
| |
| |
| const mapX = Math.floor(rayX + 0.5); |
| const mapY = Math.floor(rayY + 0.5); |
| |
| if (walls.some(wall => wall.x === mapX && wall.y === mapY)) { |
| hitWall = true; |
| wallType = (mapX + mapY) % 2; |
| } |
| } |
| |
| |
| const correctedDist = rayDist * Math.cos(rayAngle - playerAngle); |
| |
| |
| const wallHeight = Math.min(gameHeight, gameHeight / correctedDist * 2); |
| const wallTop = (gameHeight - wallHeight) / 2; |
| |
| |
| const wallColor = wallType === 0 ? '#600' : '#333'; |
| ctx.fillStyle = wallColor; |
| ctx.fillRect(i, wallTop, 1, wallHeight); |
| |
| |
| const shade = Math.min(1, correctedDist / 10); |
| ctx.fillStyle = `rgba(0, 0, 0, ${shade})`; |
| ctx.fillRect(i, wallTop, 1, wallHeight); |
| } |
| } |
| |
| function spawnEnemy() { |
| |
| let angle = Math.random() * Math.PI * 2; |
| let distance = 5 + Math.random() * 5; |
| let x = playerX + Math.cos(angle) * distance; |
| let y = playerY + Math.sin(angle) * distance; |
| |
| |
| while (checkWallCollision(x, y)) { |
| angle = Math.random() * Math.PI * 2; |
| distance = 5 + Math.random() * 5; |
| x = playerX + Math.cos(angle) * distance; |
| y = playerY + Math.sin(angle) * distance; |
| } |
| |
| enemies.push({ |
| x, |
| y, |
| health: 100, |
| speed: 0.03 + Math.random() * 0.02, |
| lastAttack: 0, |
| attackCooldown: 1000, |
| damage: 10 |
| }); |
| } |
| |
| function updateEnemies() { |
| for (let i = enemies.length - 1; i >= 0; i--) { |
| const enemy = enemies[i]; |
| |
| |
| const angleToPlayer = Math.atan2(playerY - enemy.y, playerX - enemy.x); |
| const moveX = Math.cos(angleToPlayer) * enemy.speed; |
| const moveY = Math.sin(angleToPlayer) * enemy.speed; |
| |
| |
| if (!checkWallCollision(enemy.x + moveX, enemy.y + moveY)) { |
| enemy.x += moveX; |
| enemy.y += moveY; |
| } |
| |
| |
| const dx = playerX - enemy.x; |
| const dy = playerY - enemy.y; |
| const distance = Math.sqrt(dx * dx + dy * dy); |
| |
| |
| const now = Date.now(); |
| if (distance < 1 && now - enemy.lastAttack > enemy.attackCooldown) { |
| takeDamage(enemy.damage); |
| enemy.lastAttack = now; |
| |
| |
| bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; |
| setTimeout(() => { |
| bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0)'; |
| }, 200); |
| } |
| |
| |
| const enemyAngle = angleToPlayer - playerAngle; |
| const enemyDistance = distance; |
| |
| |
| if (Math.abs(enemyAngle) < Math.PI / 3) { |
| const correctedDist = enemyDistance * Math.cos(enemyAngle); |
| const enemySize = Math.min(200, gameHeight / correctedDist); |
| const enemyX = gameWidth / 2 + Math.tan(enemyAngle) * gameWidth / 2; |
| const enemyY = gameHeight / 2; |
| |
| |
| ctx.fillStyle = '#f00'; |
| ctx.beginPath(); |
| ctx.arc(enemyX, enemyY, enemySize / 2, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| const healthBarWidth = enemySize; |
| const healthBarHeight = 5; |
| ctx.fillStyle = '#300'; |
| ctx.fillRect(enemyX - healthBarWidth / 2, enemyY - enemySize / 2 - 10, healthBarWidth, healthBarHeight); |
| ctx.fillStyle = '#f00'; |
| ctx.fillRect(enemyX - healthBarWidth / 2, enemyY - enemySize / 2 - 10, healthBarWidth * (enemy.health / 100), healthBarHeight); |
| } |
| |
| |
| if (enemy.health <= 0) { |
| enemies.splice(i, 1); |
| |
| |
| for (let j = 0; j < 10; j++) { |
| particles.push({ |
| x: enemy.x, |
| y: enemy.y, |
| size: 0.1 + Math.random() * 0.1, |
| color: '#f00', |
| speed: 0.05 + Math.random() * 0.1, |
| angle: Math.random() * Math.PI * 2, |
| life: 30 + Math.random() * 30 |
| }); |
| } |
| |
| |
| score += 10; |
| updateScore(); |
| |
| |
| if (Math.random() < 0.2) { |
| const weaponTypes = ['pistol', 'rifle', 'shotgun']; |
| const randomWeapon = weaponTypes[Math.floor(Math.random() * weaponTypes.length)]; |
| |
| if (randomWeapon !== currentWeapon) { |
| weapons[randomWeapon].ammo = weapons[randomWeapon].maxAmmo; |
| currentWeapon = randomWeapon; |
| updateWeapon(); |
| |
| |
| enemyHitEffect.style.backgroundColor = 'rgba(0, 255, 0, 0.3)'; |
| setTimeout(() => { |
| enemyHitEffect.style.backgroundColor = 'rgba(255, 255, 255, 0)'; |
| }, 200); |
| } |
| } |
| } |
| } |
| } |
| |
| function updateParticles() { |
| for (let i = particles.length - 1; i >= 0; i--) { |
| const p = particles[i]; |
| |
| |
| p.x += Math.cos(p.angle) * p.speed; |
| p.y += Math.sin(p.angle) * p.speed; |
| p.life--; |
| |
| |
| const angle = Math.atan2(p.y - playerY, p.x - playerX) - playerAngle; |
| const distance = Math.sqrt(Math.pow(p.x - playerX, 2) + Math.pow(p.y - playerY, 2)); |
| |
| if (Math.abs(angle) < Math.PI / 3) { |
| const correctedDist = distance * Math.cos(angle); |
| const size = Math.min(20, p.size * gameHeight / correctedDist); |
| const x = gameWidth / 2 + Math.tan(angle) * gameWidth / 2; |
| const y = gameHeight / 2; |
| |
| ctx.fillStyle = p.color; |
| ctx.beginPath(); |
| ctx.arc(x, y, size, 0, Math.PI * 2); |
| ctx.fill(); |
| } |
| |
| |
| if (p.life <= 0) { |
| particles.splice(i, 1); |
| } |
| } |
| } |
| |
| function drawWeapon() { |
| const weapon = weapons[currentWeapon]; |
| const weaponHeight = gameHeight / 2; |
| const weaponWidth = weaponHeight * 0.5; |
| const weaponX = gameWidth / 2 - weaponWidth / 2; |
| const weaponY = gameHeight - weaponHeight; |
| |
| |
| ctx.fillStyle = weapon.color; |
| ctx.fillRect(weaponX, weaponY, weaponWidth, weaponHeight); |
| |
| |
| ctx.fillStyle = '#333'; |
| ctx.fillRect(weaponX + 10, weaponY + 20, weaponWidth - 20, 10); |
| |
| |
| const now = Date.now(); |
| if (now - lastShotTime < 100) { |
| ctx.fillStyle = '#ff0'; |
| ctx.fillRect(weaponX + weaponWidth, weaponY + 20, 30, 10); |
| } |
| } |
| |
| function shoot() { |
| const now = Date.now(); |
| const weapon = weapons[currentWeapon]; |
| |
| if (now - lastShotTime < weapon.fireRate || isReloading) return; |
| if (weapon.ammo <= 0) { |
| reload(); |
| return; |
| } |
| |
| lastShotTime = now; |
| weapon.ammo--; |
| updateAmmo(); |
| |
| |
| particles.push({ |
| x: playerX + Math.cos(playerAngle) * 0.5, |
| y: playerY + Math.sin(playerAngle) * 0.5, |
| size: 0.2, |
| color: '#ff0', |
| speed: 0.2, |
| angle: playerAngle, |
| life: 5 |
| }); |
| |
| |
| for (let i = 0; i < enemies.length; i++) { |
| const enemy = enemies[i]; |
| |
| |
| const angleToEnemy = Math.atan2(enemy.y - playerY, enemy.x - playerX); |
| const angleDiff = Math.abs(normalizeAngle(angleToEnemy - playerAngle)); |
| |
| |
| if (angleDiff < 0.087) { |
| const distance = Math.sqrt(Math.pow(enemy.x - playerX, 2) + Math.pow(enemy.y - playerY, 2)); |
| |
| |
| let blocked = false; |
| const steps = 10; |
| for (let j = 1; j <= steps; j++) { |
| const checkX = playerX + (enemy.x - playerX) * (j / steps); |
| const checkY = playerY + (enemy.y - playerY) * (j / steps); |
| |
| if (checkWallCollision(checkX, checkY)) { |
| blocked = true; |
| break; |
| } |
| } |
| |
| if (!blocked) { |
| |
| enemy.health -= weapon.damage; |
| |
| |
| enemyHitEffect.style.backgroundColor = 'rgba(255, 255, 255, 0.5)'; |
| setTimeout(() => { |
| enemyHitEffect.style.backgroundColor = 'rgba(255, 255, 255, 0)'; |
| }, 100); |
| |
| |
| for (let j = 0; j < 5; j++) { |
| particles.push({ |
| x: enemy.x, |
| y: enemy.y, |
| size: 0.1 + Math.random() * 0.1, |
| color: '#f00', |
| speed: 0.05 + Math.random() * 0.1, |
| angle: playerAngle + (Math.random() - 0.5) * 0.5, |
| life: 20 + Math.random() * 20 |
| }); |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| |
| if (weapon.ammo <= 0) { |
| reload(); |
| } |
| } |
| |
| function reload() { |
| if (isReloading || weapons[currentWeapon].ammo === weapons[currentWeapon].maxAmmo) return; |
| |
| isReloading = true; |
| reloadIndicator.style.display = 'block'; |
| reloadFill.style.width = '0%'; |
| |
| const reloadTime = weapons[currentWeapon].reloadTime; |
| const startTime = Date.now(); |
| |
| const reloadInterval = setInterval(() => { |
| const elapsed = Date.now() - startTime; |
| const progress = (elapsed / reloadTime) * 100; |
| |
| reloadFill.style.width = `${progress}%`; |
| |
| if (elapsed >= reloadTime) { |
| clearInterval(reloadInterval); |
| weapons[currentWeapon].ammo = weapons[currentWeapon].maxAmmo; |
| updateAmmo(); |
| isReloading = false; |
| reloadIndicator.style.display = 'none'; |
| } |
| }, 16); |
| } |
| |
| function takeDamage(amount) { |
| health -= amount; |
| updateHealth(); |
| |
| if (health <= 0) { |
| health = 0; |
| updateHealth(); |
| gameOver(); |
| } |
| } |
| |
| function updateHealth() { |
| healthFill.style.width = `${health}%`; |
| |
| |
| if (health > 60) { |
| healthFill.style.backgroundColor = '#0f0'; |
| } else if (health > 30) { |
| healthFill.style.backgroundColor = '#ff0'; |
| } else { |
| healthFill.style.backgroundColor = '#f00'; |
| } |
| } |
| |
| function updateAmmo() { |
| ammoCount.textContent = `${weapons[currentWeapon].ammo}/${weapons[currentWeapon].maxAmmo}`; |
| } |
| |
| function updateScore() { |
| scoreDisplay.textContent = `SCORE: ${score}`; |
| } |
| |
| function updateWeapon() { |
| weaponDisplay.textContent = weapons[currentWeapon].name; |
| } |
| |
| function gameOver() { |
| gameRunning = false; |
| finalScore.textContent = score; |
| gameOverScreen.style.display = 'flex'; |
| } |
| |
| function checkWallCollision(x, y) { |
| const mapX = Math.floor(x + 0.5); |
| const mapY = Math.floor(y + 0.5); |
| |
| return walls.some(wall => wall.x === mapX && wall.y === mapY); |
| } |
| |
| function normalizeAngle(angle) { |
| while (angle < -Math.PI) angle += Math.PI * 2; |
| while (angle > Math.PI) angle -= Math.PI * 2; |
| return angle; |
| } |
| |
| |
| window.addEventListener('resize', () => { |
| gameWidth = window.innerWidth; |
| gameHeight = window.innerHeight; |
| canvas.width = gameWidth; |
| canvas.height = gameHeight; |
| }); |
| </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=Davi111/games" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |