about summary refs log tree commit diff stats
path: root/html/fps/game.js
diff options
context:
space:
mode:
Diffstat (limited to 'html/fps/game.js')
-rw-r--r--html/fps/game.js992
1 files changed, 992 insertions, 0 deletions
diff --git a/html/fps/game.js b/html/fps/game.js
new file mode 100644
index 0000000..e157ab1
--- /dev/null
+++ b/html/fps/game.js
@@ -0,0 +1,992 @@
+// Game state management
+const GameState = {
+    player: {
+        x: 0,
+        y: 0,
+        angle: 0,
+        health: 100,
+        ammo: 10,
+        score: 0
+    },
+    level: {
+        width: 32,
+        height: 32,
+        map: [],
+        flag: { x: 0, y: 0 }
+    },
+    enemies: [],
+    items: [],
+    isGameOver: false,
+    gun: {
+        recoil: 0,
+        lastShot: 0,
+        muzzleFlash: 0,
+        slidePosition: 0,  // 0 = forward, 1 = back
+        tilt: 0  // 0 = normal, 1 = tilted
+    },
+    shots: [],
+    isStarted: false,
+    gradients: {}, // Cache for wall gradients
+    lastGradientUpdate: 0,
+    particles: [],
+    damageFlash: 0
+};
+
+// Level generation using a simple maze algorithm
+const generateLevel = () => {
+    const map = Array(GameState.level.height).fill().map(() => 
+        Array(GameState.level.width).fill(1)
+    );
+    
+    // Create starting room
+    const startRoomSize = 5;
+    for (let y = 1; y < startRoomSize; y++) {
+        for (let x = 1; x < startRoomSize; x++) {
+            map[y][x] = 0;
+        }
+    }
+    
+    // Add fewer random larger rooms
+    const numLargeRooms = 2; // Reduced from 3
+    for (let i = 0; i < numLargeRooms; i++) {
+        const roomWidth = Math.floor(Math.random() * 3) + 4; // 4-6 (reduced from 4-7)
+        const roomHeight = Math.floor(Math.random() * 3) + 4; // 4-6 (reduced from 4-7)
+        const roomX = Math.floor(Math.random() * (GameState.level.width - roomWidth - 2)) + 1;
+        const roomY = Math.floor(Math.random() * (GameState.level.height - roomHeight - 2)) + 1;
+        
+        // Create room
+        for (let y = roomY; y < roomY + roomHeight; y++) {
+            for (let x = roomX; x < roomX + roomWidth; x++) {
+                map[y][x] = 0;
+            }
+        }
+        
+        // Connect to maze
+        const connectX = roomX + Math.floor(roomWidth/2);
+        const connectY = roomY + Math.floor(roomHeight/2);
+        map[connectY][connectX] = 0;
+    }
+    
+    // Simple maze generation using depth-first search with single-cell hallways
+    const carveMaze = (x, y) => {
+        // Ensure we're within bounds
+        if (x <= 0 || x >= GameState.level.width - 1 || y <= 0 || y >= GameState.level.height - 1) {
+            return;
+        }
+        
+        map[y][x] = 0;
+        
+        const directions = [
+            [0, -2], [2, 0], [0, 2], [-2, 0]
+        ].sort(() => Math.random() - 0.5);
+        
+        for (const [dx, dy] of directions) {
+            const nx = x + dx;
+            const ny = y + dy;
+            if (nx > 0 && nx < GameState.level.width - 1 &&
+                ny > 0 && ny < GameState.level.height - 1 &&
+                map[ny][nx] === 1) {
+                // Carve single-cell paths
+                const midX = x + Math.floor(dx/2);
+                const midY = y + Math.floor(dy/2);
+                if (midX >= 0 && midX < GameState.level.width &&
+                    midY >= 0 && midY < GameState.level.height) {
+                    map[midY][midX] = 0;
+                }
+                carveMaze(nx, ny);
+            }
+        }
+    };
+    
+    // Start maze generation from the edge of the starting room
+    carveMaze(startRoomSize, startRoomSize);
+    
+    // Place flag in a random open space (not in starting room)
+    const openSpaces = [];
+    for (let y = 0; y < GameState.level.height; y++) {
+        for (let x = 0; x < GameState.level.width; x++) {
+            if (map[y][x] === 0 && (x >= startRoomSize || y >= startRoomSize)) {
+                openSpaces.push({x, y});
+            }
+        }
+    }
+    const flagPos = openSpaces[Math.floor(Math.random() * openSpaces.length)];
+    GameState.level.flag = flagPos;
+    
+    // Place enemies in open spaces (not in starting room)
+    GameState.enemies = [];
+    for (let i = 0; i < 5; i++) {
+        const pos = openSpaces[Math.floor(Math.random() * openSpaces.length)];
+        GameState.enemies.push({
+            x: pos.x,
+            y: pos.y,
+            health: Math.floor(Math.random() * 4) + 2
+        });
+    }
+    
+    // Place items in open spaces (not in starting room)
+    GameState.items = [];
+    // Add health packs
+    for (let i = 0; i < 8; i++) {
+        const pos = openSpaces[Math.floor(Math.random() * openSpaces.length)];
+        GameState.items.push({
+            x: pos.x,
+            y: pos.y,
+            type: 'health',
+            value: Math.floor(Math.random() * 5) + 1
+        });
+    }
+    // Add ammo packs
+    for (let i = 0; i < 6; i++) {
+        const pos = openSpaces[Math.floor(Math.random() * openSpaces.length)];
+        GameState.items.push({
+            x: pos.x,
+            y: pos.y,
+            type: 'ammo',
+            value: 5
+        });
+    }
+    
+    GameState.level.map = map;
+    GameState.player.x = startRoomSize/2;
+    GameState.player.y = startRoomSize/2;
+    GameState.player.angle = 0;
+};
+
+// Player movement and controls
+const handlePlayerMovement = (keys) => {
+    const moveSpeed = 0.05;
+    const rotateSpeed = 0.05;
+    
+    if (keys.w) {
+        // Move forward in the direction the player is facing
+        GameState.player.x += Math.sin(GameState.player.angle) * moveSpeed;
+        GameState.player.y -= Math.cos(GameState.player.angle) * moveSpeed;
+    }
+    if (keys.s) {
+        // Move backward
+        GameState.player.x -= Math.sin(GameState.player.angle) * moveSpeed;
+        GameState.player.y += Math.cos(GameState.player.angle) * moveSpeed;
+    }
+    if (keys.a) {
+        // Strafe left
+        GameState.player.x -= Math.cos(GameState.player.angle) * moveSpeed;
+        GameState.player.y -= Math.sin(GameState.player.angle) * moveSpeed;
+    }
+    if (keys.d) {
+        // Strafe right
+        GameState.player.x += Math.cos(GameState.player.angle) * moveSpeed;
+        GameState.player.y += Math.sin(GameState.player.angle) * moveSpeed;
+    }
+    
+    // Add arrow key rotation
+    if (keys.ArrowLeft) {
+        GameState.player.angle -= rotateSpeed;
+    }
+    if (keys.ArrowRight) {
+        GameState.player.angle += rotateSpeed;
+    }
+};
+
+// Enemy AI
+const updateEnemies = () => {
+    GameState.enemies = GameState.enemies.map(enemy => {
+        if (enemy.health <= 0) return null;
+        
+        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.5) {
+            GameState.player.health -= 10;
+            GameState.damageFlash = 1.0; // Trigger full flash
+            if (GameState.player.health <= 0) {
+                GameState.isGameOver = true;
+            }
+        } else if (dist < 5) {
+            // Check if path to player is clear
+            const steps = 10;
+            let canMove = true;
+            for (let i = 1; i <= steps; i++) {
+                const testX = enemy.x + (dx / dist) * (i / steps);
+                const testY = enemy.y + (dy / dist) * (i / steps);
+                if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) {
+                    canMove = false;
+                    break;
+                }
+            }
+            
+            if (canMove) {
+                enemy.x += dx / dist * 0.05;
+                enemy.y += dy / dist * 0.05;
+            }
+        }
+        
+        return enemy;
+    }).filter(Boolean);
+};
+
+// Collision detection
+const checkCollisions = () => {
+    // Wall collisions with improved precision and bounds checking
+    const playerX = GameState.player.x;
+    const playerY = GameState.player.y;
+    
+    // Check if player is within map bounds
+    if (playerX < 0 || playerX >= GameState.level.width ||
+        playerY < 0 || playerY >= GameState.level.height) {
+        // Push player back to last valid position
+        GameState.player.x = Math.max(0, Math.min(GameState.player.x, GameState.level.width - 1));
+        GameState.player.y = Math.max(0, Math.min(GameState.player.y, GameState.level.height - 1));
+        return;
+    }
+    
+    const nextX = playerX + Math.sin(GameState.player.angle) * 0.1;
+    const nextY = playerY + Math.cos(GameState.player.angle) * 0.1;
+    
+    // Check all four corners of the player's collision box
+    const checkPoint = (x, y) => {
+        const gridX = Math.floor(x);
+        const gridY = Math.floor(y);
+        // Ensure we're within bounds
+        if (gridX < 0 || gridX >= GameState.level.width ||
+            gridY < 0 || gridY >= GameState.level.height) {
+            return true; // Treat out of bounds as a wall
+        }
+        return GameState.level.map[gridY][gridX] === 1;
+    };
+    
+    const collisionBox = 0.2; // Player collision box size
+    
+    if (checkPoint(playerX + collisionBox, playerY + collisionBox) ||
+        checkPoint(playerX + collisionBox, playerY - collisionBox) ||
+        checkPoint(playerX - collisionBox, playerY + collisionBox) ||
+        checkPoint(playerX - collisionBox, playerY - collisionBox)) {
+        // Push player back to last valid position
+        GameState.player.x = Math.floor(GameState.player.x) + 0.5;
+        GameState.player.y = Math.floor(GameState.player.y) + 0.5;
+    }
+    
+    // Item collection
+    GameState.items = GameState.items.filter(item => {
+        const dx = GameState.player.x - item.x;
+        const dy = GameState.player.y - item.y;
+        if (Math.sqrt(dx * dx + dy * dy) < 0.5) {
+            if (item.type === 'ammo') {
+                GameState.player.ammo += item.value;
+            } else {
+                GameState.player.health = Math.min(100, GameState.player.health + item.value);
+            }
+            return false;
+        }
+        return true;
+    });
+    
+    // Flag collection
+    const dx = GameState.player.x - GameState.level.flag.x;
+    const dy = GameState.player.y - GameState.level.flag.y;
+    if (Math.sqrt(dx * dx + dy * dy) < 0.5) {
+        GameState.player.score++;
+        generateLevel();
+    }
+};
+
+// Rendering system
+const render = (ctx) => {
+    const canvas = ctx.canvas;
+    const width = canvas.width;
+    const height = canvas.height;
+    
+    // Clear screen
+    ctx.fillStyle = '#000';
+    ctx.fillRect(0, 0, width, height);
+    
+    if (!GameState.isStarted) {
+        // Draw start screen
+        ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+        ctx.fillRect(0, 0, width, height);
+        
+        // Draw title
+        ctx.fillStyle = '#fff';
+        ctx.font = '48px monospace';
+        ctx.textAlign = 'center';
+        ctx.fillText('Hedge Maze', width/2, height/2 - 50);
+        
+        // Draw start button
+        const buttonWidth = 200;
+        const buttonHeight = 60;
+        const buttonX = width/2 - buttonWidth/2;
+        const buttonY = height/2;
+        
+        ctx.fillStyle = '#333';
+        ctx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight);
+        
+        ctx.fillStyle = '#fff';
+        ctx.font = '24px monospace';
+        ctx.fillText('START GAME', width/2, buttonY + buttonHeight/2 + 8);
+        
+        return;
+    }
+    
+    // Draw ceiling
+    ctx.fillStyle = '#1a1a4a';
+    ctx.fillRect(0, 0, width, height/2);
+    
+    // Draw floor
+    ctx.fillStyle = '#4a2a00';
+    ctx.fillRect(0, height/2, width, height/2);
+    
+    // Draw walls and sprites using ray casting
+    const fov = Math.PI / 3;
+    const numRays = Math.floor(width / 2); // Reduce ray count by half
+    const rayResults = [];
+    
+    // Pre-calculate common values
+    const halfHeight = height / 2;
+    const brightnessSteps = 20; // Number of brightness levels to cache
+    const gradientCache = {};
+    
+    // Create cached gradients for different brightness levels
+    for (let b = 0; b <= brightnessSteps; b++) {
+        const brightness = b / brightnessSteps;
+        const baseColor = Math.floor(brightness * 100);
+        const distanceColor = Math.floor(brightness * 50);
+        
+        gradientCache[b] = {
+            top: `rgb(${distanceColor}, ${baseColor + 20}, ${distanceColor})`,
+            middle: `rgb(${distanceColor}, ${baseColor}, ${distanceColor})`,
+            bottom: `rgb(${distanceColor}, ${baseColor - 20}, ${distanceColor})`,
+            line: `rgba(0, ${baseColor - 30}, 0, 0.3)`
+        };
+    }
+    
+    for (let i = 0; i < width; i += 2) {
+        const rayIndex = Math.floor(i / 2);
+        const rayAngle = GameState.player.angle - fov/2 + fov * rayIndex / numRays;
+        let distance = 0;
+        let hit = false;
+        
+        while (!hit && distance < 20) {
+            distance += 0.2;
+            const testX = GameState.player.x + Math.sin(rayAngle) * distance;
+            const testY = GameState.player.y + Math.cos(rayAngle) * distance;
+            
+            // Check for wall hits
+            if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) {
+                hit = true;
+            }
+        }
+        
+        rayResults[rayIndex] = {
+            distance,
+            angle: rayAngle
+        };
+        
+        const wallHeight = height / (distance * Math.cos(rayAngle - GameState.player.angle));
+        const brightness = Math.max(0, 1 - distance / 20);
+        const brightnessIndex = Math.floor(brightness * brightnessSteps);
+        const colors = gradientCache[brightnessIndex];
+        
+        const wallTop = halfHeight - wallHeight/2;
+        const wallBottom = halfHeight + wallHeight/2;
+        
+        // Draw wall with cached colors
+        ctx.fillStyle = colors.middle;
+        ctx.fillRect(i, wallTop, 2, wallHeight);
+        
+        // Draw top and bottom highlights
+        ctx.fillStyle = colors.top;
+        ctx.fillRect(i, wallTop, 2, 1);
+        ctx.fillStyle = colors.bottom;
+        ctx.fillRect(i, wallBottom - 1, 2, 1);
+        
+        // Draw vertical lines less frequently
+        if (i % 8 === 0) {
+            ctx.fillStyle = colors.line;
+            ctx.fillRect(i, wallTop, 2, wallHeight);
+        }
+    }
+    
+    // Draw shots
+    const now = Date.now();
+    GameState.shots = GameState.shots.filter(shot => {
+        const age = now - shot.time;
+        if (age > 200) return false; // Remove shots older than 200ms
+        
+        const progress = age / 200;
+        const distance = progress * 20;
+        const x = GameState.player.x + Math.sin(shot.angle) * distance;
+        const y = GameState.player.y + Math.cos(shot.angle) * distance;
+        
+        // Convert world position to screen position
+        const dx = x - GameState.player.x;
+        const dy = y - GameState.player.y;
+        const angle = Math.atan2(dx, dy) - GameState.player.angle;
+        const screenX = (angle / fov + 0.5) * width;
+        
+        if (screenX >= 0 && screenX < width) {
+            ctx.fillStyle = '#ffff00';
+            ctx.fillRect(screenX, halfHeight, 2, 2);
+        }
+        
+        return true;
+    });
+    
+    // Draw gun
+    const gunY = height - 150;
+    const gunX = width/2;
+    const recoilOffset = Math.sin(GameState.gun.recoil * Math.PI) * 50;
+    const tiltAngle = GameState.gun.tilt * Math.PI / 6;
+    
+    // Only draw gun if it's not in full recoil
+    if (GameState.gun.recoil < 0.8) {
+        ctx.save();
+        ctx.translate(gunX, gunY + recoilOffset);
+        ctx.rotate(tiltAngle);
+        
+        // Gun body (larger rectangle) extending below screen
+        ctx.fillStyle = '#333';
+        ctx.fillRect(-30, 0, 60, height);
+        
+        // Gun slide (smaller rectangle) with sliding animation
+        const slideOffset = GameState.gun.slidePosition * 20;
+        ctx.fillStyle = '#222';
+        // Adjusted slide dimensions: shorter above, longer below
+        ctx.fillRect(-8, -15 - slideOffset, 16, 90); // Changed from -30 to -15 for top, and 60 to 90 for height
+        
+        // Muzzle flash
+        if (GameState.gun.muzzleFlash > 0) {
+            const flashSize = GameState.gun.muzzleFlash * 30;
+            ctx.fillStyle = `rgba(255, 255, 0, ${GameState.gun.muzzleFlash})`;
+            ctx.beginPath();
+            ctx.arc(0, -15 - slideOffset, flashSize, 0, Math.PI * 2); // Adjusted to match new slide position
+            ctx.fill();
+        }
+        
+        ctx.restore();
+    }
+    
+    // Draw crosshair
+    ctx.fillStyle = '#fff';
+    ctx.fillRect(width/2 - 5, halfHeight, 10, 1);
+    ctx.fillRect(width/2, halfHeight - 5, 1, 10);
+    
+    // Draw HUD - only canvas-based
+    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+    ctx.fillRect(10, 10, 200, 120);
+    
+    ctx.fillStyle = '#fff';
+    ctx.font = '24px monospace';
+    ctx.fillText(`Health: ${GameState.player.health}`, 100, 45);
+    ctx.fillText(`Ammo: ${GameState.player.ammo}`, 80, 80);
+    ctx.fillText(`Score: ${GameState.player.score}`, 80, 115);
+    
+    // Draw compass
+    const compassSize = 100;
+    const compassX = width/2 - compassSize/2;
+    const compassY = 20;
+    
+    // Draw compass background
+    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+    ctx.fillRect(compassX, compassY, compassSize, compassSize); // Make it square
+    ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+    ctx.lineWidth = 2;
+    ctx.strokeRect(compassX, compassY, compassSize, compassSize);
+    
+    // Draw compass directions
+    ctx.fillStyle = '#fff';
+    ctx.font = '16px monospace';
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'middle';
+    
+    // Draw N, E, S, W in traditional compass layout
+    ctx.fillText('N', compassX + compassSize/2, compassY + 15);
+    ctx.fillText('E', compassX + compassSize - 15, compassY + compassSize/2);
+    ctx.fillText('S', compassX + compassSize/2, compassY + compassSize - 15);
+    ctx.fillText('W', compassX + 15, compassY + compassSize/2);
+    
+    // Draw direction indicator - ensure it's always visible within the compass
+    const angle = GameState.player.angle;
+    const radius = compassSize/2 - 15; // Keep indicator within compass bounds
+    const indicatorX = compassX + compassSize/2 + Math.sin(angle) * radius;
+    const indicatorY = compassY + compassSize/2 - Math.cos(angle) * radius;
+    
+    ctx.fillStyle = '#0f0';
+    ctx.beginPath();
+    ctx.arc(indicatorX, indicatorY, 4, 0, Math.PI * 2);
+    ctx.fill();
+    
+    // Draw sprites
+    const drawSprite = (x, y, distance, type) => {
+        // Calculate screen position
+        const dx = x - GameState.player.x;
+        const dy = y - GameState.player.y;
+        const angle = Math.atan2(dx, dy) - GameState.player.angle;
+        
+        // Convert to screen coordinates
+        const screenX = (angle / (Math.PI / 3) + 0.5) * width;
+        const screenY = height / 2;
+        
+        // Calculate size based on distance
+        const baseSize = 0.5;
+        const size = (baseSize / distance) * height;
+        
+        // Only draw if on screen and not behind a wall
+        if (screenX >= 0 && screenX < width) {
+            const wallDistance = rayResults[Math.floor(screenX / 2)].distance;
+            if (distance < wallDistance) {
+                // Set color based on type
+                if (type === 'enemy') {
+                    ctx.fillStyle = '#f00';
+                } else if (type === 'health') {
+                    ctx.fillStyle = '#0f0';
+                } else if (type === 'ammo') {
+                    ctx.fillStyle = '#0ff';
+                } else if (type === 'flag') {
+                    ctx.fillStyle = '#ff0';
+                }
+                
+                // Draw single square
+                ctx.fillRect(screenX - size/2, screenY - size/2, size, size);
+            }
+        }
+    };
+
+    // Draw all sprites
+    GameState.enemies.forEach(enemy => {
+        const dx = enemy.x - GameState.player.x;
+        const dy = enemy.y - GameState.player.y;
+        const distance = Math.sqrt(dx * dx + dy * dy);
+        drawSprite(enemy.x, enemy.y, distance, 'enemy');
+    });
+
+    GameState.items.forEach(item => {
+        const dx = item.x - GameState.player.x;
+        const dy = item.y - GameState.player.y;
+        const distance = Math.sqrt(dx * dx + dy * dy);
+        drawSprite(item.x, item.y, distance, item.type);
+    });
+
+    // Draw flag
+    const dx = GameState.level.flag.x - GameState.player.x;
+    const dy = GameState.level.flag.y - GameState.player.y;
+    const flagDistance = Math.sqrt(dx * dx + dy * dy);
+    drawSprite(GameState.level.flag.x, GameState.level.flag.y, flagDistance, 'flag');
+    
+    // Draw particles
+    GameState.particles.forEach(particle => {
+        const dx = particle.x - GameState.player.x;
+        const dy = particle.y - GameState.player.y;
+        const distance = Math.sqrt(dx * dx + dy * dy);
+        const angle = Math.atan2(dx, dy) - GameState.player.angle;
+        
+        // Convert world position to screen position
+        const screenX = (angle / (Math.PI / 3) + 0.5) * width;
+        const screenY = height / 2;
+        
+        // Ensure size is always positive and within reasonable bounds
+        const size = Math.max(1, Math.min(particle.size, (1 - distance / 20) * particle.size));
+        
+        if (screenX >= 0 && screenX < width && size > 0) {
+            ctx.save();
+            ctx.translate(screenX, screenY);
+            ctx.rotate(particle.rotation);
+            
+            ctx.fillStyle = particle.color;
+            ctx.globalAlpha = particle.life;
+            
+            // Draw a more interesting particle shape
+            ctx.beginPath();
+            ctx.moveTo(0, -size);
+            ctx.lineTo(size, size);
+            ctx.lineTo(-size, size);
+            ctx.closePath();
+            ctx.fill();
+            
+            ctx.restore();
+            ctx.globalAlpha = 1;
+        }
+    });
+    
+    // Draw mini-map
+    const miniMapSize = 200;
+    const miniMapX = width - miniMapSize - 10;
+    const miniMapY = 10;
+    const cellSize = miniMapSize / GameState.level.width;
+    
+    // Draw mini-map background with semi-transparent border
+    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+    ctx.fillRect(miniMapX, miniMapY, miniMapSize, miniMapSize);
+    ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+    ctx.lineWidth = 2;
+    ctx.strokeRect(miniMapX, miniMapY, miniMapSize, miniMapSize);
+    
+    // Draw walls
+    ctx.fillStyle = '#666';
+    for (let y = 0; y < GameState.level.height; y++) {
+        for (let x = 0; x < GameState.level.width; x++) {
+            if (GameState.level.map[y][x] === 1) {
+                ctx.fillRect(
+                    miniMapX + x * cellSize,
+                    miniMapY + y * cellSize,
+                    cellSize,
+                    cellSize
+                );
+            }
+        }
+    }
+    
+    // Draw flag
+    ctx.fillStyle = '#ff0';
+    ctx.beginPath();
+    ctx.arc(
+        miniMapX + GameState.level.flag.x * cellSize + cellSize/2,
+        miniMapY + GameState.level.flag.y * cellSize + cellSize/2,
+        cellSize/2,
+        0,
+        Math.PI * 2
+    );
+    ctx.fill();
+    
+    // Draw items
+    GameState.items.forEach(item => {
+        if (item.type === 'health') {
+            ctx.fillStyle = '#0f0';
+        } else {
+            ctx.fillStyle = '#0ff';
+        }
+        ctx.beginPath();
+        ctx.arc(
+            miniMapX + item.x * cellSize + cellSize/2,
+            miniMapY + item.y * cellSize + cellSize/2,
+            cellSize/3,
+            0,
+            Math.PI * 2
+        );
+        ctx.fill();
+    });
+    
+    // Draw enemies
+    ctx.fillStyle = '#f00';
+    GameState.enemies.forEach(enemy => {
+        ctx.beginPath();
+        ctx.arc(
+            miniMapX + enemy.x * cellSize + cellSize/2,
+            miniMapY + enemy.y * cellSize + cellSize/2,
+            cellSize/2,
+            0,
+            Math.PI * 2
+        );
+        ctx.fill();
+    });
+    
+    // Draw player with enhanced direction indicator
+    const playerX = miniMapX + GameState.player.x * cellSize;
+    const playerY = miniMapY + GameState.player.y * cellSize;
+    const size = cellSize * 1.5;
+    
+    // Draw player outline
+    ctx.strokeStyle = '#fff';
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.arc(playerX + cellSize/2, playerY + cellSize/2, size/2 + 2, 0, Math.PI * 2);
+    ctx.stroke();
+    
+    // Draw player triangle with direction indicator - use the angle directly
+    ctx.fillStyle = '#00f';
+    ctx.save();
+    ctx.translate(playerX + cellSize/2, playerY + cellSize/2);
+    ctx.rotate(GameState.player.angle);  // Use the angle directly
+    
+    // Draw main triangle
+    ctx.beginPath();
+    ctx.moveTo(0, -size/2);
+    ctx.lineTo(size/2, size/2);
+    ctx.lineTo(-size/2, size/2);
+    ctx.closePath();
+    ctx.fill();
+    
+    // Draw direction indicator line
+    ctx.strokeStyle = '#fff';
+    ctx.lineWidth = 1;
+    ctx.beginPath();
+    ctx.moveTo(0, -size/2);
+    ctx.lineTo(0, -size);
+    ctx.stroke();
+    
+    // Draw small circle at the end of the direction line
+    ctx.fillStyle = '#fff';
+    ctx.beginPath();
+    ctx.arc(0, -size, size/4, 0, Math.PI * 2);
+    ctx.fill();
+    
+    ctx.restore();
+
+    // Draw damage flash effect
+    if (GameState.damageFlash > 0) {
+        // Create a radial gradient that's transparent in the center and red at the edges
+        const gradient = ctx.createRadialGradient(
+            width/2, height/2, 0,  // Inner circle (center)
+            width/2, height/2, Math.max(width, height)/2  // Outer circle (edges)
+        );
+        gradient.addColorStop(0, `rgba(255, 0, 0, 0)`);  // Transparent center
+        gradient.addColorStop(0.7, `rgba(255, 0, 0, 0)`);  // Start red at 70% of radius
+        gradient.addColorStop(1, `rgba(255, 0, 0, ${GameState.damageFlash})`);  // Full red at edges
+        
+        ctx.fillStyle = gradient;
+        ctx.fillRect(0, 0, width, height);
+        GameState.damageFlash -= 0.05; // Fade out over time
+    }
+};
+
+// Game loop
+const gameLoop = (ctx) => {
+    if (!GameState.isStarted) {
+        render(ctx);
+        requestAnimationFrame(() => gameLoop(ctx));
+        return;
+    }
+    
+    if (GameState.isGameOver) {
+        ctx.fillStyle = '#fff';
+        ctx.font = '48px monospace';
+        ctx.fillText('GAME OVER', ctx.canvas.width/2 - 100, ctx.canvas.height/2);
+        return;
+    }
+    
+    // Check for firing input
+    if ((keys[' '] || keys.e) && GameState.player.ammo > 0 && 
+        Date.now() - GameState.gun.lastShot > 200) {
+        GameState.player.ammo--;
+        GameState.gun.recoil = 1;
+        GameState.gun.muzzleFlash = 1;
+        GameState.gun.lastShot = Date.now();
+        GameState.shots.push({
+            time: Date.now(),
+            angle: GameState.player.angle
+        });
+        
+        // Check for enemy hits
+        const fov = Math.PI / 3;
+        const rayAngle = GameState.player.angle;
+        let distance = 0;
+        let hitEnemy = null;
+        let hitWall = false;
+        
+        while (!hitEnemy && !hitWall && distance < 20) {
+            distance += 0.1;
+            const testX = GameState.player.x + Math.sin(rayAngle) * distance;
+            const testY = GameState.player.y + Math.cos(rayAngle) * distance;
+            
+            if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) {
+                hitWall = true;
+                break;
+            }
+            
+            hitEnemy = GameState.enemies.find(enemy => {
+                const dx = testX - enemy.x;
+                const dy = testY - enemy.y;
+                return Math.sqrt(dx * dx + dy * dy) < 0.5;
+            });
+        }
+        
+        if (hitEnemy) {
+            hitEnemy.health--;
+            if (hitEnemy.health <= 0) {
+                // Create more particles with more colors
+                const colors = [
+                    '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff',
+                    '#ff8800', '#88ff00', '#00ff88', '#0088ff', '#8800ff', '#ff0088',
+                    '#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff'
+                ];
+                for (let i = 0; i < 80; i++) { // More particles
+                    const color = colors[Math.floor(Math.random() * colors.length)];
+                    GameState.particles.push(new Particle(hitEnemy.x, hitEnemy.y, color));
+                }
+            }
+        }
+    }
+    
+    // Update gun recoil, muzzle flash, and slide position
+    if (GameState.gun.recoil > 0) {
+        GameState.gun.recoil -= 0.1;
+        // Animate slide moving back
+        GameState.gun.slidePosition = Math.min(1, GameState.gun.slidePosition + 0.2);
+    } else if (GameState.gun.slidePosition > 0) {
+        // Animate slide moving forward
+        GameState.gun.slidePosition = Math.max(0, GameState.gun.slidePosition - 0.1);
+    }
+    if (GameState.gun.muzzleFlash > 0) {
+        GameState.gun.muzzleFlash -= 0.2;
+    }
+
+    // Update gun tilt based on ammo
+    if (GameState.player.ammo === 0) {
+        GameState.gun.tilt = Math.min(1, GameState.gun.tilt + 0.1);
+    } else {
+        GameState.gun.tilt = Math.max(0, GameState.gun.tilt - 0.1);
+    }
+    
+    // Update particles
+    GameState.particles = GameState.particles.filter(particle => particle.update());
+    
+    handlePlayerMovement(keys);
+    updateEnemies();
+    checkCollisions();
+    render(ctx);
+    
+    requestAnimationFrame(() => gameLoop(ctx));
+};
+
+// Input handling
+const keys = { 
+    w: false, 
+    a: false, 
+    s: false, 
+    d: false,
+    ArrowLeft: false,
+    ArrowRight: false,
+    ' ': false,  // Space bar
+    e: false     // E key
+};
+
+document.addEventListener('keydown', e => {
+    if (!GameState.isStarted && (e.key === ' ' || e.key === 'Enter')) {
+        GameState.isStarted = true;
+        generateLevel();
+        return;
+    }
+    
+    if (e.key.toLowerCase() in keys) keys[e.key.toLowerCase()] = true;
+    if (e.key in keys) keys[e.key] = true;
+});
+
+document.addEventListener('keyup', e => {
+    if (e.key.toLowerCase() in keys) keys[e.key.toLowerCase()] = false;
+    if (e.key in keys) keys[e.key] = false;
+});
+
+document.addEventListener('mousemove', e => {
+    if (GameState.isStarted) {
+        GameState.player.angle += e.movementX * 0.01;
+    }
+});
+
+// Update click handler to handle both start screen and firing
+document.addEventListener('click', (e) => {
+    if (!GameState.isStarted) {
+        const canvas = document.getElementById('gameCanvas');
+        const rect = canvas.getBoundingClientRect();
+        const x = e.clientX - rect.left;
+        const y = e.clientY - rect.top;
+        
+        const buttonWidth = 200;
+        const buttonHeight = 60;
+        const buttonX = canvas.width/2 - buttonWidth/2;
+        const buttonY = canvas.height/2;
+        
+        if (x >= buttonX && x <= buttonX + buttonWidth &&
+            y >= buttonY && y <= buttonY + buttonHeight) {
+            GameState.isStarted = true;
+            generateLevel();
+        }
+        return;
+    }
+    
+    // Handle firing on click during gameplay
+    if (GameState.player.ammo > 0 && Date.now() - GameState.gun.lastShot > 200) {
+        GameState.player.ammo--;
+        GameState.gun.recoil = 1;
+        GameState.gun.muzzleFlash = 1;
+        GameState.gun.lastShot = Date.now();
+        GameState.shots.push({
+            time: Date.now(),
+            angle: GameState.player.angle
+        });
+        
+        // Check for enemy hits
+        const fov = Math.PI / 3;
+        const rayAngle = GameState.player.angle;
+        let distance = 0;
+        let hitEnemy = null;
+        let hitWall = false;
+        
+        while (!hitEnemy && !hitWall && distance < 20) {
+            distance += 0.1;
+            const testX = GameState.player.x + Math.sin(rayAngle) * distance;
+            const testY = GameState.player.y + Math.cos(rayAngle) * distance;
+            
+            if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) {
+                hitWall = true;
+                break;
+            }
+            
+            hitEnemy = GameState.enemies.find(enemy => {
+                const dx = testX - enemy.x;
+                const dy = testY - enemy.y;
+                return Math.sqrt(dx * dx + dy * dy) < 0.5;
+            });
+        }
+        
+        if (hitEnemy) {
+            hitEnemy.health--;
+            if (hitEnemy.health <= 0) {
+                // Create more particles with more colors
+                const colors = [
+                    '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff',
+                    '#ff8800', '#88ff00', '#00ff88', '#0088ff', '#8800ff', '#ff0088',
+                    '#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff'
+                ];
+                for (let i = 0; i < 80; i++) { // More particles
+                    const color = colors[Math.floor(Math.random() * colors.length)];
+                    GameState.particles.push(new Particle(hitEnemy.x, hitEnemy.y, color));
+                }
+            }
+        }
+    }
+});
+
+// Initialize game
+const init = () => {
+    const canvas = document.getElementById('gameCanvas');
+    canvas.width = window.innerWidth;
+    canvas.height = window.innerHeight;
+    const ctx = canvas.getContext('2d');
+    
+    // Don't generate level or start game loop until start button is clicked
+    render(ctx);
+    gameLoop(ctx);
+};
+
+// Add particle class
+class Particle {
+    constructor(x, y, color) {
+        this.x = x;
+        this.y = y;
+        this.color = color;
+        this.size = Math.random() * 8 + 4; // Larger size range (4-12)
+        
+        // Random direction in all directions (360 degrees)
+        const angle = Math.random() * Math.PI * 2;
+        const speed = Math.random() * 15 + 5; // Faster spread (5-20)
+        this.speedX = Math.sin(angle) * speed;
+        this.speedY = Math.cos(angle) * speed;
+        
+        this.life = 1.0;
+        this.decay = Math.random() * 0.005 + 0.002; // Slower decay
+        this.rotation = Math.random() * Math.PI * 2;
+        this.rotationSpeed = (Math.random() - 0.5) * 0.2;
+        this.gravity = Math.random() * 0.15 + 0.05;
+    }
+
+    update() {
+        this.x += this.speedX;
+        this.y += this.speedY;
+        this.speedY += this.gravity;
+        this.life -= this.decay;
+        this.rotation += this.rotationSpeed;
+        return this.life > 0;
+    }
+}
+
+init();