girlyx / index.html
Batfly's picture
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
Raw
History Blame Contribute Delete
38 kB
<!DOCTYPE html>
<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>