Create a fully responsive HTML5, CSS, and JavaScript FPS game optimized for high frame rates (60fps+), with the visual and gameplay style inspired by classic Doom (retro pixelated graphics, fast-paced movement, tight corridors, and enemies). Core Requirements: Rendering & Performance: Use WebGL or Canvas 2D for rendering. Optimize for low-latency input and smooth animations. Implement efficient sprite batching and level culling to avoid frame drops. Game Mechanics: Player can move (WASD), strafe, jump, and shoot with mouse or keyboard controls. Include hit detection and enemy AI (simple chase and attack behavior). Add health, ammo, and armor pickups. Basic HUD showing health, ammo, and minimap. Level Design: Generate maze-like maps using procedural generation or tiled layouts. Include doors, keys, and trigger zones for gameplay progression. Walls and floors use pixelated textures for a retro feel. Visual & Audio Style: Retro textures and low-res sprite enemies (pixel-art style). Add dynamic lighting (fake light cones or simple shading). Include looping heavy metal soundtrack and retro weapon sound effects. Weapons: Start with a pistol and shotgun, each with distinct fire rates and damage. Animated muzzle flash and reload animations. Optimizations: Ensure mobile and desktop compatibility. Use requestAnimationFrame for smooth rendering. Preload textures, sounds, and maps to minimize lag. Scalability: Code should be modular (separate files: index.html, style.css, main.js, enemies.js, weapons.js, levels.js). Include comments explaining performance tricks and architecture. Allow future expansions (more weapons, multiplayer). Styling & Immersion: Fullscreen support with responsive UI scaling. Retro CRT shader/filter (optional) for authentic Doom-like visuals. Smooth transition effects when changing levels or dying. Deliverables: Provide all files separately (HTML, CSS, JS, assets folder). Include a readme.md with instructions to run locally or deploy. Ensure clean, commented, and optimized code for production use. - Follow Up Deployment
d663d1e verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Retro FPS - Doom Style</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom CSS for CRT effect and game-specific styling */ | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| font-family: 'Courier New', monospace; | |
| touch-action: none; | |
| user-select: none; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| #game-canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| image-rendering: pixelated; | |
| } | |
| #hud { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 1rem; | |
| color: #ff0; | |
| text-shadow: 2px 2px 0 #f00; | |
| font-size: 1.5rem; | |
| display: flex; | |
| justify-content: space-between; | |
| pointer-events: none; | |
| } | |
| #weapon-display { | |
| position: absolute; | |
| bottom: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 300px; | |
| height: 200px; | |
| pointer-events: none; | |
| } | |
| #crosshair { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 20px; | |
| height: 20px; | |
| pointer-events: none; | |
| } | |
| #crosshair::before, #crosshair::after { | |
| content: ''; | |
| position: absolute; | |
| background-color: #ff0; | |
| } | |
| #crosshair::before { | |
| width: 2px; | |
| height: 10px; | |
| left: 9px; | |
| top: 5px; | |
| } | |
| #crosshair::after { | |
| width: 10px; | |
| height: 2px; | |
| left: 5px; | |
| top: 9px; | |
| } | |
| #start-screen, #game-over-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: #ff0; | |
| z-index: 100; | |
| } | |
| .title { | |
| font-size: 4rem; | |
| margin-bottom: 2rem; | |
| text-shadow: 4px 4px 0 #f00; | |
| letter-spacing: 2px; | |
| } | |
| .btn { | |
| padding: 1rem 2rem; | |
| background-color: #333; | |
| color: #ff0; | |
| border: 2px solid #ff0; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| margin: 0.5rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .btn:hover { | |
| background-color: #ff0; | |
| color: #000; | |
| } | |
| /* CRT effect */ | |
| .crt::after { | |
| content: " "; | |
| display: block; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| right: 0; | |
| background: rgba(18, 16, 16, 0.1); | |
| opacity: 0.15; | |
| z-index: 2; | |
| pointer-events: none; | |
| } | |
| .crt::before { | |
| content: " "; | |
| display: block; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| right: 0; | |
| background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); | |
| z-index: 2; | |
| background-size: 100% 2px, 3px 100%; | |
| pointer-events: none; | |
| } | |
| @media (max-width: 768px) { | |
| #hud { | |
| font-size: 1rem; | |
| padding: 0.5rem; | |
| } | |
| .title { | |
| font-size: 2rem; | |
| } | |
| .btn { | |
| padding: 0.5rem 1rem; | |
| font-size: 1rem; | |
| } | |
| #mobile-controls { | |
| position: absolute; | |
| bottom: 100px; | |
| width: 100%; | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 1rem; | |
| z-index: 10; | |
| } | |
| .mobile-btn { | |
| width: 60px; | |
| height: 60px; | |
| background-color: rgba(255, 255, 0, 0.3); | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-size: 1.5rem; | |
| user-select: none; | |
| } | |
| #joystick-area { | |
| position: absolute; | |
| left: 20px; | |
| bottom: 20px; | |
| width: 120px; | |
| height: 120px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| border-radius: 50%; | |
| } | |
| #joystick { | |
| position: absolute; | |
| width: 50px; | |
| height: 50px; | |
| background-color: rgba(255, 255, 0, 0.5); | |
| border-radius: 50%; | |
| top: 35px; | |
| left: 35px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="crt"> | |
| <div id="game-container"> | |
| <canvas id="game-canvas"></canvas> | |
| <div id="hud"> | |
| <div id="health">HEALTH: 100</div> | |
| <div id="ammo">AMMO: 50</div> | |
| <div id="armor">ARMOR: 0</div> | |
| <div id="score">SCORE: 0</div> | |
| </div> | |
| <div id="weapon-display"> | |
| <canvas id="weapon-canvas" width="300" height="200"></canvas> | |
| </div> | |
| <div id="crosshair"></div> | |
| <div id="start-screen"> | |
| <h1 class="title">RETRO FPS</h1> | |
| <button id="start-btn" class="btn">START GAME</button> | |
| <button id="fullscreen-btn" class="btn">FULLSCREEN</button> | |
| <div class="mt-8 text-center"> | |
| <p>WASD to move</p> | |
| <p>Mouse to aim and shoot</p> | |
| <p>Find the exit to win!</p> | |
| </div> | |
| </div> | |
| <div id="game-over-screen" style="display: none;"> | |
| <h1 class="title" id="game-over-title">GAME OVER</h1> | |
| <div id="final-score" class="text-2xl mb-8">SCORE: 0</div> | |
| <button id="restart-btn" class="btn">PLAY AGAIN</button> | |
| </div> | |
| <!-- Mobile controls (hidden on desktop) --> | |
| <div id="mobile-controls" style="display: none;"> | |
| <div id="joystick-area"> | |
| <div id="joystick"></div> | |
| </div> | |
| <div class="mobile-btn" id="shoot-btn">FIRE</div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game constants | |
| const CELL_SIZE = 64; | |
| const PLAYER_HEIGHT = 32; | |
| const PLAYER_SPEED = 5; | |
| const ROTATION_SPEED = 0.05; | |
| const FOV = Math.PI / 3; // 60 degrees | |
| const HALF_FOV = FOV / 2; | |
| const NUM_RAYS = 320; | |
| const MAX_DEPTH = 16; | |
| const WALL_HEIGHT = 100; | |
| // Game state | |
| const gameState = { | |
| player: { | |
| x: CELL_SIZE * 1.5, | |
| y: CELL_SIZE * 1.5, | |
| angle: 0, | |
| health: 100, | |
| armor: 0, | |
| ammo: 50, | |
| score: 0, | |
| weapons: ['pistol'], | |
| currentWeapon: 0, | |
| isShooting: false, | |
| lastShot: 0, | |
| reloading: false | |
| }, | |
| enemies: [], | |
| projectiles: [], | |
| map: [], | |
| level: 1, | |
| gameOver: false, | |
| victory: false, | |
| started: false, | |
| keys: { | |
| w: false, | |
| a: false, | |
| s: false, | |
| d: false, | |
| space: false | |
| }, | |
| mouse: { | |
| x: 0, | |
| y: 0, | |
| down: false | |
| }, | |
| touch: { | |
| x: 0, | |
| y: 0, | |
| active: false, | |
| startX: 0, | |
| startY: 0 | |
| }, | |
| lastTime: 0, | |
| fps: 0, | |
| frameCount: 0, | |
| lastFpsUpdate: 0, | |
| screenShake: 0, | |
| damageFlash: 0 | |
| }; | |
| // Textures and sprites | |
| const textures = { | |
| wall1: createTexture('#8B0000', '#800000'), // Dark red brick | |
| wall2: createTexture('#006400', '#004d00'), // Dark green brick | |
| wall3: createTexture('#00008B', '#000080'), // Dark blue brick | |
| wall4: createTexture('#4B0082', '#3d0066'), // Indigo brick | |
| floor: createTexture('#333333', '#222222'), // Dark floor | |
| ceiling: createTexture('#111111', '#000000'), // Black ceiling | |
| enemy: createEnemySprite(), | |
| pistol: createPistolSprite(), | |
| shotgun: createShotgunSprite(), | |
| bulletHole: createBulletHoleSprite(), | |
| health: createPickupSprite('#FF0000'), | |
| ammo: createPickupSprite('#FFFF00'), | |
| armor: createPickupSprite('#00FFFF'), | |
| exit: createPickupSprite('#00FF00') | |
| }; | |
| // Audio | |
| const audio = { | |
| pistolShot: createSound(800, 0.2, 0.02, 'square'), | |
| shotgunShot: createSound([400, 300, 200], 0.3, 0.1, 'sawtooth'), | |
| enemyHit: createSound(200, 0.1, 0.01, 'square'), | |
| playerHit: createSound(100, 0.2, 0.05, 'sine'), | |
| pickup: createSound(600, 0.1, 0.01, 'sine'), | |
| emptyGun: createSound(100, 0.1, 0.1, 'sine'), | |
| reload: createSound([300, 400], 0.2, 0.1, 'sine'), | |
| metalTrack: null // Would be loaded from file in production | |
| }; | |
| // Canvas setup | |
| const canvas = document.getElementById('game-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const weaponCanvas = document.getElementById('weapon-canvas'); | |
| const weaponCtx = weaponCanvas.getContext('2d'); | |
| // UI elements | |
| const healthDisplay = document.getElementById('health'); | |
| const ammoDisplay = document.getElementById('ammo'); | |
| const armorDisplay = document.getElementById('armor'); | |
| const scoreDisplay = document.getElementById('score'); | |
| const startScreen = document.getElementById('start-screen'); | |
| const gameOverScreen = document.getElementById('game-over-screen'); | |
| const gameOverTitle = document.getElementById('game-over-title'); | |
| const finalScoreDisplay = document.getElementById('final-score'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| const fullscreenBtn = document.getElementById('fullscreen-btn'); | |
| const mobileControls = document.getElementById('mobile-controls'); | |
| const joystick = document.getElementById('joystick'); | |
| const shootBtn = document.getElementById('shoot-btn'); | |
| // Event listeners | |
| startBtn.addEventListener('click', startGame); | |
| restartBtn.addEventListener('click', startGame); | |
| fullscreenBtn.addEventListener('click', toggleFullscreen); | |
| document.addEventListener('keydown', handleKeyDown); | |
| document.addEventListener('keyup', handleKeyUp); | |
| document.addEventListener('mousemove', handleMouseMove); | |
| document.addEventListener('mousedown', handleMouseDown); | |
| document.addEventListener('mouseup', handleMouseUp); | |
| document.addEventListener('touchstart', handleTouchStart, { passive: false }); | |
| document.addEventListener('touchmove', handleTouchMove, { passive: false }); | |
| document.addEventListener('touchend', handleTouchEnd); | |
| shootBtn.addEventListener('touchstart', handleShootTouch); | |
| // Check if mobile | |
| if (/Mobi|Android/i.test(navigator.userAgent)) { | |
| mobileControls.style.display = 'flex'; | |
| } | |
| // Initialize game | |
| initGame(); | |
| // Main game loop | |
| function gameLoop(timestamp) { | |
| // Calculate delta time for smooth movement | |
| const deltaTime = (timestamp - gameState.lastTime) / 1000; | |
| gameState.lastTime = timestamp; | |
| // Update FPS counter | |
| updateFpsCounter(timestamp); | |
| // Only update if game is running | |
| if (gameState.started && !gameState.gameOver && !gameState.victory) { | |
| update(deltaTime); | |
| } | |
| render(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Start the game loop | |
| requestAnimationFrame(gameLoop); | |
| // Game functions | |
| function initGame() { | |
| // Set canvas size | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Generate level | |
| generateLevel(); | |
| // Create some enemies | |
| spawnEnemies(3 + gameState.level); | |
| // Set initial weapon | |
| gameState.player.currentWeapon = 0; | |
| // Update HUD | |
| updateHud(); | |
| } | |
| function startGame() { | |
| // Reset game state | |
| gameState.player = { | |
| x: CELL_SIZE * 1.5, | |
| y: CELL_SIZE * 1.5, | |
| angle: 0, | |
| health: 100, | |
| armor: 0, | |
| ammo: 50, | |
| score: 0, | |
| weapons: ['pistol'], | |
| currentWeapon: 0, | |
| isShooting: false, | |
| lastShot: 0, | |
| reloading: false | |
| }; | |
| gameState.enemies = []; | |
| gameState.projectiles = []; | |
| gameState.level = 1; | |
| gameState.gameOver = false; | |
| gameState.victory = false; | |
| gameState.started = true; | |
| gameState.screenShake = 0; | |
| gameState.damageFlash = 0; | |
| // Hide screens | |
| startScreen.style.display = 'none'; | |
| gameOverScreen.style.display = 'none'; | |
| // Generate level | |
| generateLevel(); | |
| // Spawn enemies | |
| spawnEnemies(3 + gameState.level); | |
| // Update HUD | |
| updateHud(); | |
| // Play music (would be loaded from file in production) | |
| // audio.metalTrack.play(); | |
| } | |
| function generateLevel() { | |
| // Simple maze generation | |
| const size = 10 + gameState.level; | |
| gameState.map = []; | |
| // Create empty map | |
| for (let y = 0; y < size; y++) { | |
| gameState.map[y] = []; | |
| for (let x = 0; x < size; x++) { | |
| // Border walls | |
| if (x === 0 || y === 0 || x === size - 1 || y === size - 1) { | |
| gameState.map[y][x] = 1; | |
| } else { | |
| // Random walls (25% chance) | |
| gameState.map[y][x] = Math.random() < 0.25 ? 1 : 0; | |
| } | |
| } | |
| } | |
| // Ensure player start is clear | |
| const startX = Math.floor(gameState.player.x / CELL_SIZE); | |
| const startY = Math.floor(gameState.player.y / CELL_SIZE); | |
| gameState.map[startY][startX] = 0; | |
| gameState.map[startY][startX + 1] = 0; | |
| gameState.map[startY + 1][startX] = 0; | |
| gameState.map[startY + 1][startX + 1] = 0; | |
| // Place exit | |
| let exitX, exitY; | |
| do { | |
| exitX = Math.floor(Math.random() * (size - 2)) + 1; | |
| exitY = Math.floor(Math.random() * (size - 2)) + 1; | |
| } while (Math.abs(exitX - startX) < 3 || Math.abs(exitY - startY) < 3); | |
| gameState.map[exitY][exitX] = 5; // Exit | |
| // Place pickups | |
| placePickups(size, 3); | |
| } | |
| function placePickups(size, count) { | |
| for (let i = 0; i < count; i++) { | |
| let x, y; | |
| do { | |
| x = Math.floor(Math.random() * (size - 2)) + 1; | |
| y = Math.floor(Math.random() * (size - 2)) + 1; | |
| } while (gameState.map[y][x] !== 0); | |
| // Random pickup type (2: health, 3: ammo, 4: armor) | |
| gameState.map[y][x] = Math.floor(Math.random() * 3) + 2; | |
| } | |
| } | |
| function spawnEnemies(count) { | |
| for (let i = 0; i < count; i++) { | |
| let x, y; | |
| do { | |
| x = Math.floor(Math.random() * (gameState.map[0].length - 2)) + 1; | |
| y = Math.floor(Math.random() * (gameState.map.length - 2)) + 1; | |
| } while ( | |
| gameState.map[y][x] !== 0 || | |
| distance(gameState.player.x, gameState.player.y, x * CELL_SIZE, y * CELL_SIZE) < CELL_SIZE * 3 | |
| ); | |
| gameState.enemies.push({ | |
| x: x * CELL_SIZE + CELL_SIZE / 2, | |
| y: y * CELL_SIZE + CELL_SIZE / 2, | |
| health: 30, | |
| speed: 1 + Math.random() * 0.5, | |
| damage: 10, | |
| attackCooldown: 0, | |
| size: 20, | |
| type: 'demon' | |
| }); | |
| } | |
| } | |
| function update(deltaTime) { | |
| // Player movement | |
| handleMovement(deltaTime); | |
| // Handle shooting | |
| handleShooting(deltaTime); | |
| // Update enemies | |
| updateEnemies(deltaTime); | |
| // Update projectiles | |
| updateProjectiles(deltaTime); | |
| // Check for pickups | |
| checkPickups(); | |
| // Update screen shake | |
| if (gameState.screenShake > 0) { | |
| gameState.screenShake -= deltaTime * 10; | |
| if (gameState.screenShake < 0) gameState.screenShake = 0; | |
| } | |
| // Update damage flash | |
| if (gameState.damageFlash > 0) { | |
| gameState.damageFlash -= deltaTime * 10; | |
| if (gameState.damageFlash < 0) gameState.damageFlash = 0; | |
| } | |
| // Check for game over | |
| if (gameState.player.health <= 0) { | |
| gameState.gameOver = true; | |
| gameOverTitle.textContent = 'GAME OVER'; | |
| finalScoreDisplay.textContent = `SCORE: ${gameState.player.score}`; | |
| gameOverScreen.style.display = 'flex'; | |
| } | |
| } | |
| function handleMovement(deltaTime) { | |
| const moveSpeed = PLAYER_SPEED * deltaTime; | |
| const rotSpeed = ROTATION_SPEED * deltaTime; | |
| // Rotation | |
| if (gameState.touch.active) { | |
| // Mobile joystick rotation | |
| const dx = gameState.touch.x - gameState.touch.startX; | |
| gameState.player.angle += dx * 0.01; | |
| } else if (gameState.mouse.down) { | |
| // Mouse look | |
| gameState.player.angle += gameState.mouse.x * 0.005; | |
| } | |
| // Forward/backward movement | |
| let moveX = 0; | |
| let moveY = 0; | |
| if (gameState.keys.w) { | |
| moveX += Math.cos(gameState.player.angle) * moveSpeed; | |
| moveY += Math.sin(gameState.player.angle) * moveSpeed; | |
| } | |
| if (gameState.keys.s) { | |
| moveX -= Math.cos(gameState.player.angle) * moveSpeed; | |
| moveY -= Math.sin(gameState.player.angle) * moveSpeed; | |
| } | |
| // Strafe movement | |
| if (gameState.keys.a) { | |
| moveX += Math.cos(gameState.player.angle - Math.PI / 2) * moveSpeed; | |
| moveY += Math.sin(gameState.player.angle - Math.PI / 2) * moveSpeed; | |
| } | |
| if (gameState.keys.d) { | |
| moveX += Math.cos(gameState.player.angle + Math.PI / 2) * moveSpeed; | |
| moveY += Math.sin(gameState.player.angle + Math.PI / 2) * moveSpeed; | |
| } | |
| // Jump (space) | |
| if (gameState.keys.space) { | |
| // Simple jump effect (would be more complex in a full game) | |
| gameState.player.z = Math.sin(Date.now() * 0.01) * 5; | |
| } | |
| // Collision detection | |
| const newX = gameState.player.x + moveX; | |
| const newY = gameState.player.y + moveY; | |
| const cellX = Math.floor(newX / CELL_SIZE); | |
| const cellY = Math.floor(newY / CELL_SIZE); | |
| if (cellX >= 0 && cellX < gameState.map[0].length && | |
| cellY >= 0 && cellY < gameState.map.length) { | |
| if (gameState.map[cellY][cellX] === 0 || gameState.map[cellY][cellX] >= 2) { | |
| gameState.player.x = newX; | |
| gameState.player.y = newY; | |
| } | |
| } | |
| } | |
| function handleShooting(deltaTime) { | |
| const now = Date.now(); | |
| // Check if player is shooting | |
| if ((gameState.mouse.down || gameState.touch.shooting) && !gameState.player.reloading) { | |
| const weapon = gameState.player.weapons[gameState.player.currentWeapon]; | |
| const fireRate = weapon === 'pistol' ? 500 : 1000; // ms between shots | |
| if (now - gameState.player.lastShot > fireRate) { | |
| if (gameState.player.ammo > 0) { | |
| // Fire weapon | |
| gameState.player.lastShot = now; | |
| gameState.player.ammo--; | |
| gameState.screenShake = weapon === 'shotgun' ? 5 : 2; | |
| // Play sound | |
| if (weapon === 'pistol') { | |
| audio.pistolShot.play(); | |
| } else { | |
| audio.shotgunShot.play(); | |
| } | |
| // Create projectile | |
| const spread = weapon === 'pistol' ? 0.02 : 0.1; | |
| const numProjectiles = weapon === 'pistol' ? 1 : 5; | |
| for (let i = 0; i < numProjectiles; i++) { | |
| const angle = gameState.player.angle + (Math.random() * spread - spread / 2); | |
| gameState.projectiles.push({ | |
| x: gameState.player.x, | |
| y: gameState.player.y, | |
| angle: angle, | |
| speed: 20, | |
| range: 500, | |
| damage: weapon === 'pistol' ? 15 : 10, | |
| owner: 'player' | |
| }); | |
| } | |
| // Check for hits | |
| checkHits(weapon === 'pistol' ? 1 : 5); | |
| // Update HUD | |
| updateHud(); | |
| } else { | |
| // Out of ammo | |
| if (now - gameState.player.lastShot > 1000) { | |
| audio.emptyGun.play(); | |
| gameState.player.lastShot = now; | |
| } | |
| } | |
| } | |
| } | |
| // Reload if out of ammo | |
| if (gameState.player.ammo <= 0 && !gameState.player.reloading) { | |
| gameState.player.reloading = true; | |
| setTimeout(() => { | |
| gameState.player.ammo = gameState.player.weapons[gameState.player.currentWeapon] === 'pistol' ? 12 : 6; | |
| gameState.player.reloading = false; | |
| audio.reload.play(); | |
| updateHud(); | |
| }, 1000); | |
| } | |
| } | |
| function checkHits(numRays) { | |
| const hitEnemies = new Set(); | |
| for (let i = 0; i < numRays; i++) { | |
| const spread = 0.05; | |
| const angle = gameState.player.angle + (Math.random() * spread - spread / 2); | |
| // Check for enemy hits | |
| for (const enemy of gameState.enemies) { | |
| if (hitEnemies.has(enemy)) continue; | |
| const dx = enemy.x - gameState.player.x; | |
| const dy = enemy.y - gameState.player.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| const enemyAngle = Math.atan2(dy, dx); | |
| // Normalize angles | |
| const angleDiff = normalizeAngle(enemyAngle - angle); | |
| // Check if enemy is in front of player and within FOV | |
| if (Math.abs(angleDiff) < HALF_FOV && dist < 300) { | |
| // Check if there's a wall between player and enemy | |
| const steps = dist / 5; | |
| let hitWall = false; | |
| for (let j = 1; j <= steps; j++) { | |
| const checkX = gameState.player.x + (dx / steps) * j; | |
| const checkY = gameState.player.y + (dy / steps) * j; | |
| const cellX = Math.floor(checkX / CELL_SIZE); | |
| const cellY = Math.floor(checkY / CELL_SIZE); | |
| if (cellX >= 0 && cellX < gameState.map[0].length && | |
| cellY >= 0 && cellY < gameState.map.length) { | |
| if (gameState.map[cellY][cellX] === 1) { | |
| hitWall = true; | |
| break; | |
| } | |
| } | |
| } | |
| if (!hitWall) { | |
| // Hit the enemy | |
| enemy.health -= gameState.player.weapons[gameState.player.currentWeapon] === 'pistol' ? 15 : 10; | |
| audio.enemyHit.play(); | |
| hitEnemies.add(enemy); | |
| if (enemy.health <= 0) { | |
| // Enemy died | |
| gameState.player.score += 100; | |
| updateHud(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function updateEnemies(deltaTime) { | |
| for (let i = gameState.enemies.length - 1; i >= 0; i--) { | |
| const enemy = gameState.enemies[i]; | |
| // Remove dead enemies | |
| if (enemy.health <= 0) { | |
| gameState.enemies.splice(i, 1); | |
| continue; | |
| } | |
| // Move toward player | |
| const dx = gameState.player.x - enemy.x; | |
| const dy = gameState.player.y - enemy.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist > 0) { | |
| enemy.x += (dx / dist) * enemy.speed * deltaTime; | |
| enemy.y += (dy / dist) * enemy.speed * deltaTime; | |
| } | |
| // Attack if close enough | |
| if (dist < 40 && enemy.attackCooldown <= 0) { | |
| gameState.player.health -= enemy.damage; | |
| gameState.damageFlash = 1; | |
| audio.playerHit.play(); | |
| enemy.attackCooldown = 1; | |
| updateHud(); | |
| } | |
| if (enemy.attackCooldown > 0) { | |
| enemy.attackCooldown -= deltaTime; | |
| } | |
| } | |
| } | |
| function updateProjectiles(deltaTime) { | |
| for (let i = gameState.projectiles.length - 1; i >= 0; i--) { | |
| const proj = gameState.projectiles[i]; | |
| // Move projectile | |
| proj.x += Math.cos(proj.angle) * proj.speed; | |
| proj.y += Math.sin(proj.angle) * proj.speed; | |
| proj.range -= proj.speed; | |
| // Check if projectile is out of range | |
| if (proj.range <= 0) { | |
| gameState.projectiles.splice(i, 1); | |
| continue; | |
| } | |
| // Check for wall collision | |
| const cellX = Math.floor(proj.x / CELL_SIZE); | |
| const cellY = Math.floor(proj.y / CELL_SIZE); | |
| if (cellX >= 0 && cellX < gameState.map[0].length && | |
| cellY >= 0 && cellY < gameState.map.length) { | |
| if (gameState.map[cellY][cellX] === 1) { | |
| gameState.projectiles.splice(i, 1); | |
| continue; | |
| } | |
| } | |
| // Check for enemy hits (if projectile is from player) | |
| if (proj.owner === 'player') { | |
| for (const enemy of gameState.enemies) { | |
| const dist = distance(proj.x, proj.y, enemy.x, enemy.y); | |
| if (dist < enemy.size) { | |
| enemy.health -= proj.damage; | |
| audio.enemyHit.play(); | |
| gameState.projectiles.splice(i, 1); | |
| if (enemy.health <= 0) { | |
| gameState.player.score += 100; | |
| updateHud(); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function checkPickups() { | |
| const cellX = Math.floor(gameState.player.x / CELL_SIZE); | |
| const cellY = Math.floor(gameState.player.y / CELL_SIZE); | |
| if (cellX >= 0 && cellX < gameState.map[0].length && | |
| cellY >= 0 && cellY < gameState.map.length) { | |
| const cellValue = gameState.map[cellY][cellX]; | |
| if (cellValue >= 2) { | |
| // Pickup item | |
| audio.pickup.play(); | |
| switch (cellValue) { | |
| case 2: // Health | |
| gameState.player.health = Math.min(100, gameState.player.health + 25); | |
| break; | |
| case 3: // Ammo | |
| gameState.player.ammo += gameState.player.weapons[gameState.player.currentWeapon] === 'pistol' ? 12 : 6; | |
| break; | |
| case 4: // Armor | |
| gameState.player.armor = Math.min(100, gameState.player.armor + 25); | |
| break; | |
| case 5: // Exit | |
| nextLevel(); | |
| return; | |
| } | |
| // Remove pickup from map | |
| gameState.map[cellY][cellX] = 0; | |
| updateHud(); | |
| } | |
| } | |
| } | |
| function nextLevel() { | |
| gameState.level++; | |
| gameState.player.score += 1000; | |
| if (gameState.level > 5) { | |
| // Victory! | |
| gameState.victory = true; | |
| gameOverTitle.textContent = 'VICTORY!'; | |
| finalScoreDisplay.textContent = `FINAL SCORE: ${gameState.player.score}`; | |
| gameOverScreen.style.display = 'flex'; | |
| } else { | |
| // Generate next level | |
| generateLevel(); | |
| // Spawn more enemies | |
| spawnEnemies(3 + gameState.level); | |
| // Add shotgun on level 2 | |
| if (gameState.level === 2 && !gameState.player.weapons.includes('shotgun')) { | |
| gameState.player.weapons.push('shotgun'); | |
| } | |
| // Reset player position | |
| gameState.player.x = CELL_SIZE * 1.5; | |
| gameState.player.y = CELL_SIZE * 1.5; | |
| gameState.player.angle = 0; | |
| // Update HUD | |
| updateHud(); | |
| } | |
| } | |
| function render() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Apply screen shake effect | |
| const shakeX = gameState.screenShake > 0 ? (Math.random() * gameState.screenShake - gameState.screenShake / 2) : 0; | |
| const shakeY = gameState.screenShake > 0 ? (Math.random() * gameState.screenShake - gameState.screenShake / 2) : 0; | |
| ctx.save(); | |
| ctx.translate(shakeX, shakeY); | |
| // Draw sky (top half) | |
| ctx.fillStyle = '#1a1a2e'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height / 2); | |
| // Draw floor (bottom half) | |
| ctx.fillStyle = '#333333'; | |
| ctx.fillRect(0, canvas.height / 2, canvas.width, canvas.height / 2); | |
| // Raycasting to draw walls | |
| castRays(); | |
| // Draw enemies | |
| drawEnemies(); | |
| // Draw weapon | |
| drawWeapon(); | |
| // Apply damage flash if player was hit | |
| if (gameState.damageFlash > 0) { | |
| ctx.fillStyle = `rgba(255, 0, 0, ${gameState.damageFlash * 0.3})`; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| ctx.restore(); | |
| } | |
| function castRays() { | |
| const halfHeight = canvas.height / 2; | |
| for (let i = 0; i < NUM_RAYS; i++) { | |
| const rayAngle = gameState.player.angle - HALF_FOV + (i / NUM_RAYS) * FOV; | |
| const rayCos = Math.cos(rayAngle); | |
| const raySin = Math.sin(rayAngle); | |
| let distToWall = 0; | |
| let hitWall = false; | |
| let wallX, wallY; | |
| let wallType; | |
| // Incrementally check for wall hits | |
| while (!hitWall && distToWall < MAX_DEPTH * CELL_SIZE) { | |
| distToWall += 2; | |
| wallX = gameState.player.x + rayCos * distToWall; | |
| wallY = gameState.player.y + raySin * distToWall; | |
| const mapX = Math.floor(wallX / CELL_SIZE); | |
| const mapY = Math.floor(wallY / CELL_SIZE); | |
| // Check if ray is out of bounds | |
| if (mapX < 0 || mapX >= gameState.map[0].length || | |
| mapY < 0 || mapY >= gameState.map.length) { | |
| hitWall = true; | |
| distToWall = MAX_DEPTH * CELL_SIZE; | |
| wallType = 1; | |
| } | |
| // Check if ray hit a wall | |
| else if (gameState.map[mapY][mapX] > 0) { | |
| hitWall = true; | |
| wallType = gameState.map[mapY][mapX]; | |
| } | |
| } | |
| // Calculate distance to wall (correcting for fish-eye effect) | |
| const correctedDist = distToWall * Math.cos(rayAngle - gameState.player.angle); | |
| // Calculate wall height | |
| const wallHeight = (CELL_SIZE / correctedDist) * ((canvas.width / 2) / Math.tan(HALF_FOV)); | |
| // Draw wall slice | |
| if (hitWall && wallType !== undefined) { | |
| const wallTop = Math.max(0, halfHeight{"ok":false,"message":"terminated"} | |
| <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=Batfly/girlyx" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |