oneshotlm
Compare all models on this prompt →

Wolfenstein raycaster · ring-2.6-1t

Build a first-person raycasting maze shooter on a canvas in the Wolfenstein 3D style: WASD movement and arrow or mouse turning through a textured grid maze with enemies you can shoot. Procedural textures only, no external assets.

⛨ sandboxed Open in new tab ↗
transcript · 3 messages
user

Build a first-person raycasting maze shooter on a canvas in the Wolfenstein 3D style: WASD movement and arrow or mouse turning through a textured grid maze with enemies you can shoot. Procedural textures only, no external assets.

assistant
thinking
The user wants a first-person raycasting maze shooter similar to Wolfenstein 3D. Let me plan out all the features needed:1. **Raycasting engine** - Cast rays from player position to render textured walls
2. **Textured walls** - Procedural textures (no external assets)
3. **WASD movement** - Forward/backward/strafe
4. **Turning** - Arrow keys and mouse look
5. **Grid maze** - Procedurally generated maze
6. **Enemies** - That can shoot at the player and be shot
7. **Shooting** - Player can shoot enemies
8. **HUD** - Health, ammo, minimap
9. **Floor/ceiling** - Colored or textured
10. **Sprites for enemies** - Rendered as billboards

This is a substantial project. Let me build it all in one HTML file.

Key technical decisions:
- Canvas-based rendering
- Raycasting for walls (DDA algorithm)
- Sprite rendering for enemies (projected 2D sprites sorted by distance)
- Procedural texture generation using canvas/pixel manipulation
- Simple AI for enemies
- Collision detection with the grid
- Minimap overlay

Let me write this step by step:

### Raycasting Engine
- Standard DDA raycasting algorithm
- Textured walls with distance correction
- Floor/ceiling coloring

### Procedural Textures
- Generate textures using mathematical functions (stripes, bricks, stone, etc.)
- Store as ImageData arrays

### Maze Generation
- Use recursive backtracking or simple random maze

### Player
- Position (x, y) in grid coordinates
- Direction angle
- FOV
- Health, ammo

### Enemies
- Position in maze
- Simple AI (move toward player when visible)
- Health bars
- Can shoot at player
- Sprite rendering

### Controls
- WASD for movement
- Mouse for turning (pointer lock)
- Arrow keys as alternative
- Click to shoot

Let me code this all up now.

I'll structure it as:
1. HTML skeleton with canvas
2. CSS for full-screen canvas
3. JavaScript with all game logic

Let me think about the texture generation:
- Wall textures: brick pattern, stone pattern, wood panel, etc.
- Each generated as a small texture (64x64 or similar)
- Use pixel manipulation for procedural generation

For enemies:
- Render as sprites (circles or simple shapes with procedural textures)
- Sort by distance for proper rendering order
- Simple state machine (idle, chase, attack, die)

Let me write the complete code now.

For the maze, I'll use a simple recursive backtracking algorithm to generate a random maze.

For textures, I'll generate them procedurally:
1. Red brick wall
2. Grey stone wall  
3. Wood panel wall
4. Blue/grey wall

For enemies, I'll draw them as procedural sprites - maybe humanoid shapes made from basic shapes.

Let me think about the sprite rendering more carefully. In Wolfenstein 3D, enemies are rendered as billboard sprites. I need to:
1. Calculate the angle and distance to each enemy
2. Project the sprite size based on distance
3. Draw columns of the sprite, checking for wall occlusion
4. Apply lighting based on distance

Actually, for a single-file implementation, I'll keep the sprites simple but effective - I'll render them as procedurally generated sprite images and project them onto the canvas.

Let me code this up:

```javascript
// Game constants
const TILE_SIZE = 64;
const MAP_WIDTH = 24;
const MAP_HEIGHT = 24;
const TEX_WIDTH = 64;
const TEX_HEIGHT = 64;
const SCREEN_WIDTH = 960;
const SCREEN_HEIGHT = 600;
const FOV = Math.PI / 3; // 60 degrees
const NUM_RAYS = SCREEN_WIDTH;
const MOVE_SPEED = 3.0;
const ROT_SPEED = 2.0;
```

For the raycaster, I'll use the DDA algorithm which is what Wolfenstein 3D actually used.

For enemy sprites, I need to:
1. Calculate screen position
2. Calculate sprite height/width based on distance
3. For each column of the sprite, check if the ray distance to the wall is greater than the distance to the enemy
4. Draw the sprite column if visible

Let me write the full implementation now.

I need to be careful about performance. The main bottleneck will be the raycasting and sprite rendering. I'll use a single ImageData buffer for the screen and write pixels directly.

Actually, for simplicity and performance, I'll use canvas 2D context for most things:
- Use drawImage for textured wall columns
- Use fillRect/circle for sprites
- Use the canvas as the rendering target

Let me think about this more carefully. Using canvas drawImage for each wall column will be slower than direct pixel manipulation, but it's much simpler to code. For a modern browser, it should be fast enough.

Let me go with a hybrid approach:
- ImageData for the main rendering buffer
- Direct pixel manipulation for textures
- Actually, let me just use fillRect and drawImage - it'll be simpler and fast enough

Actually, for the best performance with pure canvas, I should:
1. Pre-render textures to small offscreen canvases
2. Use drawImage to draw texture slices to the main canvas
3. Use the z-buffer for sprite occlusion

Let me write the full code now. This will be a substantial file.

Let me think about the enemy AI:
- Each enemy has a state: 'idle', 'alert', 'chase', 'attack', 'hurt', 'die'
- Line of sight check using DDA
- Move toward player when chasing
- Periodically shoot at player
- Take damage when hit

For shooting:
- Player clicks to shoot
- Hitscan attack (instant projectile)
- Check if any enemy is in the crosshair and visible
- Enemy shoots back with similar logic

Let me also add:
- A minimap in the corner
- Crosshair
- Health/ammo display
- Damage flash effect
- Enemy death animation

OK let me just start writing the code. I'll make it comprehensive.

Actually let me think about the maze generation. I'll use a simple approach:
- Start with all walls
- Use recursive backtracking to carve paths
- The player starts at (1,1) facing east or south
- Enemies are placed in various positions in the maze

For procedural textures, I'll generate:
1. Stone wall - noise-based
2. Brick wall - grid pattern with mortar
3. Wood panel - horizontal planks with grain
4. Color accent wall - blue/grey concrete

Let me write this now. I'll be comprehensive.

For mouse control, I'll use Pointer Lock API.

Let me code everything now:

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Raycaster Maze Shooter</title>
<style>
* { margin: 0; padding: 0; overflow: hidden; }
body { background: #000; }
canvas { display: block; cursor: crosshair; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// Full game code here
</script>
</body>
</html>
```

Now let me write the complete JavaScript. This is going to be a big file.

Let me think through the texture generation carefully:

For brick texture:
- Iterate over each pixel
- Calculate which row of bricks (offset every other row)
- Draw horizontal mortar lines
- Draw vertical mortar lines
- Color the bricks with slight variation

For stone texture:
- Use a simple noise function (value noise)
- Create blocks of varying gray tones

For wood texture:
- Horizontal bands
- Add vertical grain lines with slight randomness

For a simple noise function, I can use a sine-based approach or a simple hash function.

Let me think about the sprite rendering for enemies more carefully:

For each enemy:
1. Calculate dx, dy from player to enemy
2. Calculate distance
3. Calculate the angle relative to player direction
4. Calculate screen x position
5. Calculate sprite width and height on screen
6. For each column of the sprite:
   a. Check the ray distance to the wall at that column
   b. If enemy is closer, draw that column of the sprite
7. Apply distance-based shading

For the enemy visuals, I'll draw them procedurally:
- Body (rectangle)
- Head (circle)
- Weapon (line)
- Maybe some animation frames

Actually, for a cleaner approach, let me pre-render enemy sprites as small canvases with procedural art. Each enemy type gets a few animation frames.

Let me design the enemy:
- Simple humanoid figure
- Drawn with canvas primitives
- A few frames for walking animation
- Death animation

This is getting complex. Let me simplify and just draw enemies as colored shapes that look like characters. I'll use a top-down view rendered into a small sprite canvas, then project it.

Wait, for Wolfenstein-style sprites, they're pre-rendered images that always face the camera. Since I can't use external assets, I'll need to draw them in a way that works from any angle. The simplest approach is to draw them as simple geometric figures from a fixed perspective.

Let me pre-render sprites for enemies:
- Each sprite is 64x64 pixels
- Draw a simple soldier/character from the front
- Use basic shapes (rectangles for body, circle for head, etc.)
- Have 2-3 animation frames

Actually, the simplest and most visually effective approach for a Wolfenstein-style game is to have the sprites be simple but recognizable shapes. Let me draw them like this:

For each enemy sprite (512x512 source, then scaled down):
- Head: circle at top
- Body: rectangle in middle
- Arms: lines on sides
- Legs: lines at bottom
- Color-coded based on enemy type
- Weapon drawn in hands

I'll render these onto offscreen canvases, then use them as sprites.

OK let me just write the code now. I've planned enough.

Let me also think about the game loop:
1. Handle input
2. Update player position
3. Update enemies (AI, movement)
4. Check combat (player shooting, enemies shooting)
5. Render:
   a. Clear screen (ceiling/floor)
   b. Raycast walls
   c. Render sprites (enemies, pickups)
   d. Render HUD (health, ammo, weapon, minimap)
   e. Render crosshair/damage flash

For performance, I'll use requestAnimationFrame.

Let me also think about the weapon rendering. A simple gun drawn on the canvas at the bottom center.

OK, I'll write the complete code now. Let me be thorough.

I realize I should also think about:
- Collision detection (prevent walking through walls)
- Smooth movement (frame-rate independent)
- Enemy pathfinding (simple: move toward player, check for walls)

For collision detection, I'll check the tile at the player's intended new position and ensure it's not a wall.

For enemy pathfinding, I'll do a simple approach: move toward the player, and if blocked by a wall, try to move along the wall.

Let me code this all up now. It'll be a big file but should work well.

Let me think about what makes this really impressive:
1. Smooth raycasting with textured walls
2. Multiple enemy types
3. Working AI
4. Minimap
5. Weapon bob
6. Damage flash
7. Game over / win condition
8. Multiple levels or procedural generation

I'll aim for a solid implementation of all core features.

Let me write the complete code now. I'll structure it clearly.

Actually, for the textures, I want to make sure they look good. Let me use a simple approach:

For each texture, I'll create an offscreen canvas and draw the texture onto it using canvas 2D operations. This is easier than pixel manipulation and still procedural.

For brick texture:
```javascript
function generateBrickTexture(width, height) {
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    
    // Base color
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(0, 0, width, height);
    
    // Draw bricks
    const brickHeight = 16;
    const brickWidth = 32;
    const mortar = 2;
    
    ctx.fillStyle = '#A0522D';
    for (let y = 0; y < height; y += brickHeight) {
        const offset = (y / brickHeight % 2) * (brickWidth / 2);
        for (let x = -offset; x < width + brickWidth; x += brickWidth) {
            ctx.fillRect(x + 1, y + 1, brickWidth - mortar, brickHeight - mortar);
        }
    }
    
    // Add some noise/variation
    // ...
    
    return canvas;
}
```

This approach is clean and produces good-looking textures.

Let me also think about the floor and ceiling. In Wolfenstein 3D, the floor and ceiling are just flat colors (or later games had textured floors). For simplicity, I'll use solid colors - maybe a dark gray floor and slightly lighter gray ceiling, or add a gradient for atmosphere.

OK, time to write the actual code. Let me be comprehensive but also make sure it works correctly.

One more consideration: the z-buffer. I need to store the perpendicular distance for each ray to properly occlude sprites. The perpendicular distance is distance * cos(rayAngle - playerAngle).

Let me write the complete implementation now.

I'll structure the JavaScript as follows:

```javascript
// Constants
// Texture generation
// Map generation
// Player object
// Enemy system
// Input handling
// Raycasting renderer
// Sprite renderer
// HUD renderer
// Game loop
```

Let me write it all now. This will be a substantial but self-contained implementation.

For the weapon/gun, I'll draw it procedurally on the canvas - a simple rifle shape at the bottom of the screen.

Let me also add a start screen and maybe a simple weapon bob effect.

OK here we go. Let me write the complete file.

Actually, one thing to be careful about: the texture mapping. When casting a ray and hitting a wall, I need to:
1. Determine which wall was hit (horizontal or vertical side)
2. Calculate the exact hit point
3. Calculate the texture X coordinate (0 to TEX_WIDTH)
4. Draw a vertical strip of the texture, scaled to the wall slice height
5. Apply distance shading (darker for farther walls)

For side hits (horizontal wall side vs vertical wall side), I'll shade differently to give a 3D effect.

Let me finalize my approach and write the code.

Here's my full plan:

**Map:** 32x32 grid, procedurally generated maze
**Textures:** 4 procedural wall textures (64x64 each)
**Enemies:** 3-5 enemies, simple AI, sprite-based rendering
**Weapons:** One weapon, drawn procedurally
**HUD:** Health bar, ammo count, minimap, crosshair
**Controls:** WASD + mouse (pointer lock) + click to shoot

Let me write the complete implementation now. I'll try to make the code clean and well-organized.

For the maze generation, I'll use recursive backtracking:
1. Start with all walls
2. Pick a starting cell
3. Carve passages by removing walls
4. Ensure player and enemies can be placed in open areas

Actually, I think I'll use a simpler approach: predefined map layout with some randomization, or a proper maze with some open areas for enemies to roam.

Let me use a recursive division or recursive backtracking maze algorithm, but with 2x2 cells to make the maze bigger and have more open space.

Or even simpler: generate a maze at half resolution, then scale it up. This gives wider corridors.

Let me write the code now. I'll use a 32x32 map with recursive backtracking.

Let me finalize and code this up properly. Here goes:

```javascript
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

// Screen dimensions
const W = 960;
const H = 600;
canvas.width = W;
canvas.height = H;

// Map
const MAP_W = 32;
const MAP_H = 32;
const map = new Array(MAP_W * MAP_H);

// Player
const player = {
    x: 3.5, y: 3.5,
    angle: 0,
    speed: 0,
    turnSpeed: 0,
    health: 100,
    ammo: 50,
    score: 0
};

// Textures (offscreen canvases)
const textures = [];

// Enemies
const enemies = [];

// Z-buffer
const zBuffer = new Float32Array(W);

// Input state
const keys = {};
let mouseX = 0;
let mouseY = 0;
let mouseDown = false;
let locked = false;
```

OK I think I have enough of a plan. Let me write the full code. It's going to be long but I'll make it work well.

Let me think about one more thing: for enemy sprites, I need to render them from a front-facing perspective. The sprite should look like a person facing the camera. Then when rendering, I project the sprite based on the angle between the player's view direction and the direction to the enemy.

For a proper billboard sprite in a raycaster:
1. Calculate the vector from player to enemy
2. Calculate the angle relative to player direction
3. The sprite's screen X = tan(angle) * screen distance (where screen distance = W/2 / tan(FOV/2))
4. The sprite's screen height = TILE_SIZE / distance * screen distance
5. Draw the sprite columns, using the z-buffer for occlusion

Let me implement this properly.

For the actual sprite art, I'll draw enemies as:
- A soldier with a gun
- Different colored "skins" for variety
- Simple geometric shapes

I'll create sprite images by drawing directly to offscreen canvases.

OK, let me just write the complete game now. I'll be thorough.

Let me also think about frame rate. Target 60fps. The main bottleneck is the raycasting (960 rays per frame) and sprite rendering. With modern browsers, this should be fine.

For the floor casting, I can either do proper floor raycasting (expensive) or just use a gradient/flat color. I'll use a gradient for the floor to keep performance high.

Let me write the full implementation now:

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Raycaster Maze Shooter</title>
<style>
* { margin:0; padding:0; }
html, body { width:100%; height:100%; background:#000; overflow:hidden; }
canvas { display:block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// COMPLETE GAME CODE
</script>
</body>
</html>
```

Now the JavaScript... let me think through each part carefully.

### Maze Generation

I'll use recursive backtracking on a grid, then add some open areas:

```javascript
function generateMaze() {
    // Fill with walls
    for (let i = 0; i < MAP_W * MAP_H; i++) map[i] = 1;
    
    // Carve passages using recursive backtracking
    function carve(x, y) {
        map[y * MAP_W + x] = 0;
        // Random directions
        const dirs = [[0,-2],[0,2],[-2,0],[2,0]];
        shuffle(dirs);
        for (const [dx, dy] of dirs) {
            const nx = x + dx, ny = y + dy;
            if (nx > 0 && nx < MAP_W-1 && ny > 0 && ny < MAP_H-1 && map[ny*MAP_W+nx] === 1) {
                map[(y+dy/2)*MAP_W + (x+dx/2)] = 0; // carve wall between
                carve(nx, ny);
            }
        }
    }
    
    carve(1, 1);
    
    // Open up some areas for enemies
    // ...
}
```

Wait, I should make the maze grid use odd coordinates for the maze cells and even coordinates for walls. This is the standard approach for maze generation.

Actually, let me use a simpler approach: generate the maze at full resolution where each cell is 1x1. This means the player can move through the maze more naturally.

Let me use a grid where odd coordinates are cells and even coordinates are potential walls. The maze will be (MAP_W-1)/2 by (MAP_H-1)/2 cells.

With a 32x32 map, that gives 15x15 cells.

OK let me just write the complete code now. I'll be detailed.

Actually, I just realized I should also add:
- Sound effects? Can't do without external assets. But I can use Web Audio API to generate simple sounds!
- That would be a nice touch

Let me add simple procedural sound effects using the Web Audio API:
- Shooting sound: short noise burst
- Hit sound: quick thud
- Enemy death: falling tone
- Enemy alert: short tone

OK let me write the complete code now. For real this time.

Let me plan the texture generation more carefully:

```javascript
function genTexture(type) {
    const c = document.createElement('canvas');
    c.width = 64; c.height = 64;
    const x = c.getContext('2d');
    
    if (type === 0) {
        // Red brick
        x.fillStyle = '#553322';
        x.fillRect(0, 0, 64, 64);
        for (let row = 0; row < 4; row++) {
            const off = row % 2 ? 32 : 0;
            for (let col = 0; col < 4; col++) {
                const bx = off + col * 16 - (off ? 0 : 0);
                x.fillStyle = `hsl(15, 40%, ${45 + Math.random()*15}%)`;
                x.fillRect(off + col*16 + 1, row*16 + 1, 14, 14);
            }
        }
        // Mortar lines...
    }
}
```

Hmm, actually let me use a more efficient approach. I'll use putImageData with pixel-level control for the textures. That way I can create more interesting procedural textures.

Or even simpler: generate textures using mathematical functions and put them into ImageData.

Let me use a hybrid approach: canvas drawing for geometric patterns (bricks), ImageData for noise-based patterns (stone, etc.).

Actually, you know what, let me just simplify. I'll use canvas 2D drawing for all textures. It'll look great and be simple to code.

Let me write the full code now. No more planning.

Here's my final structure:

```javascript
// ==========================================
// RAYCASTER MAZE SHOOTER
// ==========================================

// --- Configuration ---
// --- Texture Generation ---
// --- Map Generation ---
// --- Entity System (Player + Enemies) ---
// --- Input Handling ---
// --- Raycasting Renderer ---
// --- Sprite Renderer ---
// --- HUD ---
// --- Audio ---
// --- Game Loop ---
```

Let me write it. I need to be thorough but also make sure the code is correct.

For the raycasting, the standard algorithm:
1. For each screen column x (0 to W-1):
   a. Calculate the ray direction
   b. Perform DDA to find which wall was hit
   c. Calculate perpendicular wall distance
   d. Calculate wall height on screen
   e. Get texture column and draw it
2. Use z-buffer for sprite occlusion

The DDA algorithm:
- Start at player position
- Calculate step sizes for x and y
- March along the ray, checking grid cells
- When a wall cell is found, calculate the exact wall position for texture mapping

Let me implement this now.

For sprite rendering:
1. For each enemy, calculate:
   - Sprite vector: enemyPos - playerPos
   - Distance (perpendicular for z-buffer)
   - Screen position
   - Scale based on distance
2. Sort by distance (far to near)
3. For each sprite:
   - For each visible column:
     - Check z-buffer
     - Draw column if closer than wall

Let me write the complete code. I'll aim for roughly 500-700 lines of JavaScript.

Let me code it now. For real.

The key formulas:
- Camera plane for FOV: `planeX = tan(FOV/2) * dirY, planeY = -tan(FOV/2) * dirX` (perpendicular to direction)
  Wait, the camera plane should be perpendicular to the direction vector.
  
  If direction is (dirX, dirY), the camera plane is:
  - If dir is normalized, plane = perpendicular * tan(FOV/2)
  - planeX = dirY * tan(FOV/2) * aspectRatioCorrection? No...
  
  Actually, the standard raycasting setup:
  - Direction vector: (dirX, dirY)
  - Camera plane vector: (planeX, planeY), perpendicular to direction
  
  For each pixel column x from -W/2 to W/2:
  - cameraX = 2 * x / W (ranges from -1 to 1)
  - rayDirX = dirX + planeX * cameraX
  - rayDirY = dirY + planeY * cameraX
  
  The FOV is determined by the length of the camera plane vector relative to the direction vector.

Let me use the standard Lode's raycasting tutorial approach:
- Player position: posX, posY (in grid units)
- Direction: dirX, dirY (unit vector or not)
- Camera plane: planeX, planeY (perpendicular to direction, determines FOV)

The camera plane length determines FOV:
- Length = tan(FOV/2)
- For 66° FOV (Wolfenstein default): length ≈ 1.0 (roughly)
- For 90° FOV: length ≈ tan(45°) = 1.0

Let me use FOV = 66° which is classic.

Player starts facing east: dir = (1, 0), plane = (0, 0.66)

For each pixel x:
- cameraX = 2*x/W - 1 (ranges from -1 to 1)
- rayDirX = dirX + planeX * cameraX
- rayDirY = dirY + planeY * cameraX

DDA:
- Map position: mapX = floor(posX), mapY = floor(posY)
- Delta distance: deltaDistX = |1/rayDirX|, deltaDistY = |1/rayDirY|
- Step direction based on ray direction
- Side distance: how far to next grid line
- March: compare sideDistX and sideDistY, step the smaller one
- When a wall is hit, calculate the perpendicular wall distance:
  - If side == 0 (vertical wall, hit from east/west): perpWallDist = sideDistX - deltaDistX
  - If side == 1 (horizontal wall, hit from north/south): perpWallDist = sideDistY - deltaDistY

Texture mapping:
- Calculate where on the wall the ray hit: wallX = pos + perpWallDist * rayDir
- frac(wallX) gives the position within the wall cell
- texX = floor(wallX * TEX_WIDTH) % TEX_WIDTH
- If side == 0 and rayDirX > 0, or side == 1 and rayDirY < 0: texX = TEX_WIDTH - texX - 1

Then draw a vertical strip of the wall texture at screen column x, from drawStart to drawEnd, with the appropriate texture column.

For drawing the textured strip, I'll use drawImage with source and destination rectangles.

OK this is all very standard. Let me code it up.

For enemies as sprites:
- Each enemy has a position, health, state, etc.
- For rendering, calculate relative position to player
- Transform to camera space
- Render as a scaled sprite

The transform to camera space:
- invDet = 1.0 / (planeX * dirY - dirX * planeY)
- transformX = invDet * (dirY * spriteX - dirX * spriteY)
- transformY = invDet * (-planeY * spriteX + planeX * spriteY)
  Wait, that's for the Doom-style approach. Let me think...

Actually, for Wolfenstein-style sprites, the calculation is:
- spriteX = enemyX - playerX
- spriteY = enemyY - playerY
- invDet = 1.0 / (planeX * dirY - dirX * planeY)
- transformX = invDet * (dirX * spriteY - dirY * spriteX)  -- this is the Y component in camera space (depth)
- transformY = invDet * (-planeX * spriteY + planeY * spriteX) -- wait, I'm getting confused

Let me use the standard formula from Lode's raycasting tutorial (lesson 8):
```
// Translate sprite position to relative to camera
double spriteX = sprite.posX - posX;
double spriteY = sprite.posY - posY;

// Transform sprite with the inverse camera matrix
double invDet = 1.0 / (planeX * dirY - dirX * planeY);
double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY);

int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));
int spriteHeight = abs(int(screenHeight / transformY));
```

Where transformY is the depth (perpendicular distance to the camera plane) and transformX is the horizontal offset.

For each sprite, I need to:
1. Calculate transformX, transformY
2. Calculate screen X position
3. Calculate height
4. Calculate draw start Y and end Y
5. For each column of the sprite, check if it's visible (zBuffer[x] > transformY) and draw

Now let me also think about enemy states:
- IDLE: standing still, not aware of player
- ALERT: noticed the player
- CHASING: moving toward player
- ATTACKING: in range to attack
- HURT: briefly after being hit
- DYING: death animation
- DEAD: removed from game

For line of sight, I'll use a simple ray check - cast a ray from player to enemy and see if it hits a wall first.

For enemy shooting, when an enemy is chasing and close enough, it shoots periodically at the player.

Let me add health pickups too - items on the ground that the player can pick up.

OK, let me write the complete code now. I'll structure it clearly.

Actually, now I should think about what "procedural textures only" means more carefully. Every visual element must be generated procedurally - no images loaded. So:
- Wall textures: generated with code
- Sprite images: drawn with canvas
- UI elements: drawn with canvas
- Everything is code-generated

This is fine. Let me write it.

Here's my final comprehensive plan for the code structure:

```
1. Canvas setup & constants
2. Audio system (Web Audio API)
3. Texture generation functions
4. Sprite drawing functions (for enemies)
5. Maze generation
6. Player state
7. Enemy definition and array
8. Input handling (keyboard + pointer lock)
9. Game update logic
10. Raycasting rendering
11. Sprite rendering
12. HUD rendering
13. Game state management (menu, playing, game over)
14. Main game loop
```

Let me write this now. I'll aim for a polished, complete game.

Let me draft the code mentally:

For textures, I'll generate 4 wall textures:
1. Stone wall (gray with noise)
2. Brick wall (red/orange bricks)
3. Wood panel (brown with grain)
4. Tech wall (blue/grey, sci-fi)

And 2 floor/ceiling colors with simple gradients.

For enemies, I'll create 2 types:
1. Soldier - basic enemy, shoots at player
2. Fast enemy - moves faster, less health

For the weapon, I'll draw a simple gun at the bottom of the screen that flashes when shooting.

Let me code everything now. This will be a single, large HTML file.

I'll write it section by section:

Let me start with the HTML/CSS framework and then the full JS.

Actually, you know what, I think I should use an ImageData-based approach for the main rendering. Drawing individual image elements for each wall strip might be slow. Let me create one ImageData for the entire screen and write pixels directly.

No wait, that would be very slow in JavaScript. Using drawImage with pre-rendered texture canvases is actually faster because the browser can optimize it with GPU acceleration.

Hmm, but for each wall column I'd need to call drawImage, which means 960 drawImage calls per frame. Let me think...

Actually, the standard approach for performant canvas raycasters is:
1. Create one ImageData for the screen
2. For each ray, write the column pixels directly to the ImageData buffer
3. After all rays are done, put the ImageData on the canvas

This avoids the overhead of many drawImage calls. Let me use this approach.

For textures, I'll also store them as ImageData (or as regular images that I can drawImage into an offscreen canvas, then getImageData from).

Let me finalize this approach:
- Main screen buffer: ImageData(W, H)
- Textures: ImageData[4](TEX_W, TEX_H) 
- For each ray: sample texture pixels and write to screen buffer
- For sprites: draw using canvas drawImage (since sprite rendering with pixel-level control is complex)

Actually, for sprites I could also use ImageData, but it's more complex because I need to handle the per-column z-buffer check. Let me use canvas 2D for sprites.

Wait, if I use ImageData for the main buffer, I can't easily mix canvas 2D drawing. The approach would be:
1. Render walls and floor/ceiling to ImageData
2. Put the ImageData on canvas
3. Use canvas 2D context for sprites on top
4. Use canvas 2D for HUD

This should work! The canvas 2D operations for sprites and HUD happen on top of the ImageData-based wall rendering.

Actually, there's a better approach: use an offscreen canvas for the 3D scene and the main canvas for everything. Let me use two canvases layered:
- Bottom canvas: 3D rendering (ImageData-based)
- Top canvas: sprites (2D), HUD, crosshair (2D)

Hmm, that's overcomplicating it. Let me just:
1. Render walls/floor/ceiling to ImageData, putImageData on canvas
2. Draw sprites using canvas drawImage/draw shapes
3. Draw HUD using canvas 2D

This should work because putImageData sets pixels, and then subsequent drawImage/fillRect calls draw on top.

But wait, there's a subtlety: putImageData replaces pixels (no compositing), and then drawImage adds on top. So the order is:
1. putImageData for walls
2. draw sprites on top (with transparency for non-visible parts)
3. draw HUD on top

This should work correctly as long as sprite pixels have proper alpha.

Actually, for sprites I need to respect the z-buffer per-column. So I need to either:
a. Draw sprites directly to the ImageData buffer (pixel by pixel, checking z-buffer)
b. Use canvas drawImage per column of the sprite (slow but correct)
c. Use the z-buffer to clip sprites at the canvas level

Option c isn't natively supported. Option b would work but is slow. Option a gives the best performance but requires pixel-level sprite rendering.

Let me go with option a: render everything to the ImageData buffer, including sprites. This means I need to pre-render enemy sprites as pixel arrays and then project them onto the screen buffer with z-buffer checks.

This is the most correct and potentially fastest approach.

Let me pre-render enemy sprites as small RGBA arrays (like texture arrays), then project them per-column.

OK let me write the complete code now. I'll use the ImageData approach for everything.

```javascript
// Screen buffer
const imageData = ctx.createImageData(W, H);
const pixels = imageData.data;

// For each column, store perpendicular distance
const zBuffer = new Float32Array(W);

// Texture storage as arrays of [r,g,b] per pixel
const wallTextures = [];

// Pre-rendered sprite images as pixel arrays
const spriteImages = [];
```

For texture sampling, I'll store textures as arrays of RGB values (or just keep them as offscreen canvases and use getImageData for sampling).

Actually, the cleanest approach: store textures as offscreen canvases, use getImageData once to extract pixel data, then store as Uint8Array.

Let me write it all now. Here's the complete code:

I'm going to write this carefully. Let me structure the code:

```javascript
// CONSTANTS
const W = 960, H = 600;
const TEX_SIZE = 64;
const MAP_W = 32, MAP_H = 32;
const FOV = Math.PI / 3; // 60 degrees

// TEXTURE GENERATION
function genTextures() {
    // Returns array of arrays, each tex[x + y * 64] = [r, g, b]
    const textures = [];
    
    // 0: Red brick
    // 1: Grey stone
    // 2: Wood panel
    // 3: Blue tech
    
    return textures;
}

// MAZE GENERATION
function genMaze() { ... }

// ENEMY SPRITE GENERATION
function genEnemySprites() { ... }

// RAYCASTING
function castRays() {
    for (let x = 0; x < W; x++) {
        const cameraX = 2 * x / W - 1;
        const rayDirX = player.dirX + player.planeX * cameraX;
        const rayDirY = player.dirY + player.planeY * cameraX;
        
        let mapX = Math.floor(player.x);
        let mapY = Math.floor(player.y);
        
        const deltaDistX = Math.abs(1 / rayDirX);
        const deltaDistY = Math.abs(1 / rayDirY);
        
        let stepX, stepY, sideDistX, sideDistY;
        
        if (rayDirX < 0) {
            stepX = -1;
            sideDistX = (player.x - mapX) * deltaDistX;
        } else {
            stepX = 1;
            sideDistX = (mapX + 1 - player.x) * deltaDistX;
        }
        
        if (rayDirY < 0) {
            stepY = -1;
            sideDistY = (player.y - mapY) * deltaDistY;
        } else {
            stepY = 1;
            sideDistY = (mapY + 1 - player.y) * deltaDistY;
        }
        
        let hit = 0, side = 0;
        while (hit === 0) {
            if (sideDistX < sideDistY) {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            } else {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }
            if (map[mapY * MAP_W + mapX] > 0) hit = map[mapY * MAP_W + mapX];
        }
        
        let perpDist;
        if (side === 0) perpDist = (mapX - player.x + (1 - stepX) / 2) / rayDirX;
        else perpDist = (mapY - player.y + (1 - stepY) / 2) / rayDirY;
        
        zBuffer[x] = perpDist;
        
        // Wall height
        const lineHeight = Math.floor(H / perpDist);
        const drawStart = Math.max(0, Math.floor(-lineHeight / 2 + H / 2));
        const drawEnd = Math.min(H - 1, Math.floor(lineHeight / 2 + H / 2));
        
        // Texture coordinate
        let wallX;
        if (side === 0) wallX = player.y + perpDist * rayDirY;
        else wallX = player.x + perpDist * rayDirX;
        wallX -= Math.floor(wallX);
        
        let texX = Math.floor(wallX * TEX_SIZE);
        if ((side === 0 && rayDirX > 0) || (side === 1 && rayDirY < 0)) texX = TEX_SIZE - texX - 1;
        
        // Draw the column
        const tex = textures[hit - 1];
        const brightness = side === 0 ? 1.0 : 0.7; // Different shading for sides
        const distShading = Math.min(1, 3.0 / (perpDist + 0.1));
        
        for (let y = drawStart; y <= drawEnd; y++) {
            const texY = ((y * 2 - H + lineHeight) * TEX_SIZE / lineHeight) / 2;
            const ti = (texY * TEX_SIZE + texX) * 3;
            const pi = (y * W + x) * 4;
            pixels[pi] = tex[ti] * brightness * distShading;
            pixels[pi + 1] = tex[ti + 1] * brightness * distShading;
            pixels[pi + 2] = tex[ti + 2] * brightness * distShading;
            pixels[pi + 3] = 255;
        }
    }
}

// FLOOR AND CEILING
function renderFloorCeiling() { ... }

// SPRITE RENDERING
function renderSprites() { ... }

// GAME UPDATE
function update() { ... }

// HUD
function renderHUD() { ... }

// MAIN LOOP
function gameLoop() {
    update();
    renderFloorCeiling();
    castRays();
    renderSprites();
    renderHUD();
    putImageData();
    requestAnimationFrame(gameLoop);
}
```

OK let me stop planning and just write the complete code. I'll type it out now into the file.

Let me think about a few more details:

1. For floor/ceiling, I'll just do solid colors with a gradient for atmosphere - much simpler than floor raycasting and fast.

2. For the weapon, I'll draw it directly to the canvas after everything else using canvas 2D.

3. For sprites, I'll pre-render them to small offscreen canvases and use those as image sources. But I need to do per-column z-buffer checks... 

Hmm, the cleanest approach is still the all-pixel approach. Let me do it:

- All 3D rendering goes to the ImageData buffer
- Then I overlay the weapon and HUD using canvas 2D
- The canvas 2D stuff is always on top and doesn't need z-buffer checks

For sprites, I need per-pixel z-buffer checking. I'll iterate through each column of the sprite, check against zBuffer[column], and if the sprite is closer, draw that column of the sprite.

For the sprite pixel data, I'll pre-render each enemy frame to get a pixel array with RGBA values. Empty pixels will have alpha = 0.

Let me write all this now.

One more thing: I need to handle floor/ceiling in the raycaster too. Actually I'll just fill the ImageData first with floor/ceiling colors, then draw walls on top.

Floor/ceiling plan:
- Fill top half with ceiling color (gradient from dark to slightly lighter)
- Fill bottom half with floor color (gradient from dark to slightly lighter)
- This is very fast: just fill the pixel array

OK, final code incoming. I'll write it all at once.

I realize I also need to think about weapon bobbing and a smooth frame rate. Let me use deltaTime for all movement.

Let me also think about the enemy shooting. Enemies should:
- Have a line of sight check
- Shoot periodically when they can see the player
- The shot is a hitscan check

For hitscan from enemy to player:
- Cast a ray from enemy toward player
- If it reaches the player without hitting a wall, the player takes damage

Actually, simpler: just check if the enemy has a direct line of sight (using the DDA raycaster from the enemy's position toward the player's position). If yes and distance is within range, shoot.

OK here's the code. I'm going to write the whole thing now as one big block.

Let me also add:
- Multiple levels (just regenerate the maze when all enemies are killed)
- Score display
- Screen flash on damage
- Enemy variety

Alright, let me finalize the code. I'll write it all into index.html.

For the minimap, I'll draw it directly on the canvas using 2D context operations after the 3D scene is rendered.

Let me also make sure the game has proper game states:
1. MENU - Title screen, click to start
2. PLAYING - Main game
3. GAMEOVER - Death screen
4. LEVELCOMPLETE - Victory screen

OK, here goes the full implementation. I need to be thorough and make sure everything works correctly. Let me write the full code now.

For the procedural textures, I'll create 4 wall textures:
1. Red brick (classic Wolfenstein style)
2. Grey stone
3. Brown wood
4. Blue-ish concrete

Each 64x64 pixels, stored as Int32Array for fast pixel access.

For the enemy sprites, I'll draw:
- A soldier figure
- Different colored variants
- Walking frames (2 frames)
- Death animation (4 frames)
- Shooting frame

Each sprite is 64x64 pixels.

Actually, let me simplify the sprites a bit. I'll draw them directly as simple geometric shapes:
- Circle for head
- Rectangle for body
- Lines for limbs
- This gives them a distinctive look

And for the weapon viewmodel:
- A simple rifle drawn at the bottom of the screen
- Flash effect when shooting
- Sway/bob animation

Let me also consider adding some environmental detail:
- Some walls might have different textures 
- Maybe some decorative elements on the map (represented as different texture IDs)

Map legend:
- 0: empty/walkable
- 1: red brick wall
- 2: grey stone wall
- 3: brown wood wall
- 4: blue tech wall

This gives us variety in the maze.

OK I'm going to write the code. For real this time. Here goes.

Let me think about the sine/cosine based approach to the floor/ceiling. Rather than doing proper floor raycasting, I'll just use a gradient. The ceiling will be dark at the top and slightly lighter near the middle, the floor will be slightly lighter near the middle and dark at the bottom. This creates a nice tunnel effect.

Actually, a better approach: I'll render the ceiling and floor as vertical gradients. For each row y:
- If y < H/2: ceiling, color gets lighter as y approaches H/2
- If y >= H/2: floor, color gets lighter as y approaches H/2

Let me use:
- Ceiling: dark gray to medium gray
- Floor: medium gray to dark brown/dark green

This is atmospheric and fast.

OK. Writing the code now. No more deliberation.

For the enemy sprites, after thinking about it, I'll generate them as small canvases with procedural art and then use drawImage for each visible column. But that requires per-column checks against zBuffer...

The most efficient approach for sprites in a canvas raycaster:
1. For each sprite (sorted back to front):
2. Calculate screen position and size
3. For each column of the sprite:
   a. Check zBuffer[column] > spriteDepth
   b. If so, draw that column using drawImage with source/dest rects

Actually no, I can't draw partial columns with drawImage efficiently. Let me think...

I CAN write sprites directly into the ImageData buffer. Here's how:
1. For each sprite (sorted back to front):
2. Calculate screen X, Y, width, height
3. For each column cx within the sprite's screen bounds:
   a. If zBuffer[cx] > spriteDepth:
      b. For each row cy within the sprite's screen bounds:
         - Get the pixel from the sprite image at the appropriate position
         - If alpha > threshold, write to the screen buffer

This is O(numSprites * spriteWidth * spriteHeight) which is fine for a small number of sprites.

The sprite pixel data will be stored as Uint8Array with 4 channels (RGBA).

OK, now the floor rendering. Actually, let me do proper floor casting for the areas between wall columns. It's not that complex and gives a much better visual. Actually no, the gradient approach is fine for this context. Let me keep it simple.

Let me finalize and write the code. I'll aim for about 600-800 lines of JS.

Here's my complete implementation:

```javascript
// Setup
const canvas = document.getElementById('c');
canvas.width = 960;
canvas.height = 600;
const ctx = canvas.getContext('2d');
const W = 960, H = 600;

// Constants
const TEX = 64; // texture size
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;

// Map
const MW = 32, MH = 32;
let map = new Array(MW * MH);

// Z-buffer
let zbuf = new Float32Array(W);

// Screen buffer
let imgData = ctx.createImageData(W, H);
let pix = imgData.data;

// Player
let px = 3.5, py = 3.5, pa = 0; // x, y, angle
let phealth = 100, pammo = 50, pscore = 0;

// Movement
let moveForward = false, moveBack = false, turnLeft = false, turnRight = false;
let strafeLeft = false, strafeRight = false;

// Mouse
let mouseDX = 0;
let locked = false;

// Enemies
let enemies = [];
let numEnemies = 8;

// Textures
let textures = [];

// Sprite cache (for enemies)
let spriteData = [];

// Timing
let lastTime = 0;
let damageFlash = 0;

// States
const MENU = 0, PLAYING = 1, GAMEOVER = 2, WIN = 3;
let gameState = MENU;

// Audio context
let audioCtx = null;
```

OK I'm going to write the complete file now. Let me do it in one shot.

I'll start with the HTML structure, then write a comprehensive JS implementation.

For the mouse pointer lock, I need to handle the click event to request pointer lock.

For the weapon, I'll draw it with canvas 2D primitives - that way it's easy and looks good.

For the minimap, I'll draw small rectangles for each wall tile on a corner of the screen.

Let me write it all now - the complete file.

I think I'll generate the textures using ImageData and some clever pixel manipulation. Here's my approach for each:

**Red Brick:**
- Dark red mortar lines
- Bricks in alternating pattern (staggered rows)
- Slight color variation per brick
- Some noise

**Grey Stone:**
- Value noise with multiple octaves
- Darker edges for depth effect

**Wood Panel:**
- Horizontal bands (planks)
- Vertical grain lines within each plank
- Slight color variation

**Blue Tech:**
- Blue-grey base
- Grid lines (like panels)
- Small bright dots (like LEDs)

For the noise function, I'll implement a simple value noise using a hash function.

Let me write it.

Actually let me think about performance one more time. For each frame:
1. Fill floor/ceiling gradient: W*H pixels but very simple operations
2. Raycast W columns: each ray does DDA iterations
3. Draw wall columns: for each visible row, sample texture
4. Draw sprites: for each enemy, draw its columns

The most expensive part is step 3 (drawing wall columns). For each screen pixel, I'm doing array indexing and pixel writing. In JavaScript, this can be slow for 960*600 = 576,000 pixels.

But with modern JS engines, iterating through 576K pixels should be fast enough (well under 16ms for the whole frame). The DDA part is also fast since most rays won't iterate many cells.

Sprite rendering is the bottleneck if there are many enemies. But with 8 enemies at maybe 60x100 pixels each, that's only 48K pixels per frame.

So total operations per frame: ~624K pixel writes. This should be fine.

Let me also consider: should I clear the screen buffer or just overwrite? Since I'm filling every pixel (floor, ceiling, walls, and sprites cover the entire screen), I don't need to clear. But to be safe, I'll fill floors/ceiling first, then walls, then sprites.

Actually, walls don't cover all pixels - there might be gaps. But in a raycaster, each column has a wall from drawStart to drawEnd, and floor/ceiling covers the rest. Then sprites are drawn on top. So the rendering order is correct.

OK, NOW I will write the code. For real. Starting now.

Let me also add a restart mechanism and level progression - when all enemies are killed, move to next level (regenerate maze).

I also want to add:
- Ammo pickups
- Health pickups
- Key/door mechanic? Maybe too complex for now.

Let me keep it to the essentials and make sure they work well.

Here's my final feature list:
1. ✓ Raycasting engine with textured walls
2. ✓ 4 procedural wall textures
3. ✓ Procedurally generated maze
4. ✓ WASD movement with collision
5. ✓ Mouse look (pointer lock) + arrow keys
6. ✓ Multiple enemies with AI
7. ✓ Hitscan shooting
8. ✓ Health/ammo system
9. ✓ HUD with health, ammo, minimap, crosshair
10. ✓ Weapon viewmodel
11. ✓ Procedural audio
12. ✓ Floor/ceiling rendering
13. ✓ Damage flash
14. ✓ Game states (menu, playing, game over)
15. ✓ Score
16. ✓ Level progression

OK let me write the complete implementation now. It'll be thorough.

Let me think about one tricky part: floor/ceiling rendering. I'll do a simple gradient:
- Ceiling: dark gray-blue at top, slightly lighter near horizon
- Floor: dark brown at bottom, slightly lighter near horizon

Without floor raycasting, the floor will look flat, which is fine for this style (and is how many classic raycasters did it).

For the wall rendering, I need to be careful about the texture sampling. When the perpendicular distance is computed correctly, the wall heights will be correct.

Let me make sure I understand the DDA formula correctly:

```
// Ray direction
let rdx = Math.sin(pa); // wait, this is wrong for the camera setup
```

Actually, let me think about coordinate system:
- pa = player angle, 0 = facing east, PI/2 = facing south, PI = facing west, -PI/2 = facing north
- Player direction: dirX = cos(pa), dirY = sin(pa)
- Camera plane (perpendicular): planeX = -sin(pa) * scale, planeY = cos(pa) * scale
  where scale determines FOV

Actually, for the standard Lode setup:
- dirX = cos(angle), dirY = sin(angle) = 0 initially (facing east, but I want south in top-down view)

Wait, let me use a simpler convention:
- pa = 0 means facing right (east)
- Increasing angle = counterclockwise? Or clockwise?

In Lode's tutorial:
- dirX = -1, dirY = 0 (facing west initially)
- planeX = 0, planeY = 0.66

But many implementations use:
- pa = 0 facing east (right)
- pa increases counterclockwise (like standard math)

Let me use:
- dir = (cos(pa), sin(pa))
- For pa = 0: facing right (east) → dir = (1, 0)
- For pa = PI/2: facing up (north in screen coordinates because Y increases downward... wait)

Ugh, the Y-axis issue. In the map grid, do we use standard math coordinates (Y up) or screen coordinates (Y down)?

In most raycasting implementations, the map uses standard grid coordinates where:
- X increases to the right
- Y increases downward (like screen coordinates)

And in this system:
- pa = 0: facing right (east) → cos(0) = 1, sin(0) = 0 → dir = (1, 0) ✓
- pa = PI/2: facing down (south) → cos(PI/2) = 0, sin(PI/2) = 1 → dir = (0, 1) ✓
- pa = PI: facing left (west) → cos(PI) = -1, sin(PI) = 0 → dir = (-1, 0) ✓
- pa = -PI/2: facing up (north) → cos(-PI/2) = 0, sin(-PI/2) = -1 → dir = (0, -1) ✓

Camera plane (perpendicular to direction, determines FOV):
- planeX = -sin(pa) * fovScale
- planeY = cos(pa) * fovScale

For pa = 0 (facing east): plane = (0, fovScale) → pointing down ↑ wait, that means the camera "plane" extends in the Y direction. Hmm.

Actually, in the Lode tutorial coordinate system:
- The direction vector and camera plane vector together define the view
- camera plane perpendicular to direction, length determines FOV

For pa = 0: dir = (1, 0), plane = (0, 0.66) 
- This means for cameraX = -1 (left edge), rayDir = (1, 0) + (0, 0.66)*(-1) = (1, -0.66) → upper-left
- For cameraX = 1 (right edge), rayDir = (1, 0) + (0, 0.66)*(1) = (1, 0.66) → lower-right

In the coordinate system where Y increases downward, this gives us a proper view.

OK so:
- dir = (cos(pa), sin(pa))
- When pa = 0: dir = (1, 0) pointing right/east
- plane = (-sin(pa), cos(pa)) * planeLength
  - When pa = 0: plane = (0, 1) * planeLength → (0, 0.66) ✓

Wait, that gives plane = (0, cos(0)) = (0, 1) * scale... but (-sin(0), cos(0)) = (0, 1). Yes, that's correct.

But rotating counterclockwise by 90°: if dir = (cos(pa), sin(pa)), then the perpendicular (rotated 90° CCW) is (-sin(pa), cos(pa)). Since Y increases downward, CCW rotation in screen coords actually looks clockwise. Hmm...

Actually, for the camera plane to work correctly with Y-down coordinates:
- The perpendicular should be "up" when facing right
- "Up" in Y-down coords is negative Y direction

For pa = 0 (facing right):
- dir = (1, 0)
- "up" = (0, -1)
- So plane should be (0, -0.66)?

But in the Lode tutorial example (dir = (-1, 0), plane = (0, -0.66)), it uses Y-up coordinates. When I switch to Y-down, I need to adjust.

Hmm, let me just test empirically and use the formulas that work:
- dirX = cos(pa), dirY = sin(pa) with Y-down
- planeX = cos(pa + PI/2), planeY = sin(pa + PI/2) = ... which is (-sin(pa), cos(pa))

For pa = 0: plane = (0, 1) with Y-down
For pa = PI/2: plane = (-1, 0) with Y-down

Actually, I think the issue is simpler than I'm making it. Let me just use the formulas from the Lode tutorial directly and adjust for my coordinate system.

In the Lode tutorial (using Y-down convention, like screen):
- Player looking right: dirX = 1, dirY = 0, planeX = 0, planeY = 0.66
  - Wait, no. His initial setup is dirX = -1, dirY = 0, planeX = 0, planeY = 0.66
  - This means the player faces LEFT (negative X direction)
  - The plane goes DOWN (positive Y), which in a Y-down screen is "into the screen"

I think I'm overcomplicating this. Let me just use:
- pa = initial angle, start facing one direction
- When turning left, increase pa
- When turning right, decrease pa

For a top-down view with Y-down:
- pa = 0: facing up (north) → dir = (0, -1) 
- pa = PI/2: facing right (east) → dir = (1, 0)
- pa = PI: facing down (south) → dir = (0, 1)

So: dirX = sin(pa), dirY = -cos(pa)? 

Hmm, this is getting confusing. Let me just use simple formulas and verify they work:

I'll use: dirX = cos(pa), dirY = sin(pa), with the convention that:
- pa = 0 → (1, 0) = facing right
- pa = PI/2 → (0, 1) = facing down (in screen coords)
- pa = PI → (-1, 0) = facing left

And the camera plane:
- perpendicular rotated 90° clockwise in screen coords: (sin(pa), -cos(pa)) → wait

The perpendicular to (cos(pa), sin(pa)) rotated 90° counterclockwise is (-sin(pa), cos(pa)).
Rotated 90° clockwise: (sin(pa), -cos(pa)).

I want the camera plane to be perpendicular to the direction. The length of the plane determines FOV.

For pa = 0 (facing right):
- CCW perpendicular: (0, 1) → in Y-down screen, this is "down". For left edge of screen I want "up" and for right edge "down". So cameraX = -1 gives the left edge... 
  - rayDir = dir + plane * cameraX = (1, 0) + (0, 1)*(-1) = (1, -1) → upper right → this is the left edge of the screen? 
  
  Wait, when cameraX = -1, we're casting the leftmost ray. For a player facing right, the leftmost ray should go somewhat upward (negative Y in screen coords).
  
  So (1, -1) normalized points upper-right, which is correct for the left edge when facing right. ✓

- rayDir for cameraX = 1: (1, 0) + (0, 1)*(1) = (1, 1) → lower right → right edge of screen ✓

OK so with plane = (-sin(pa), cos(pa)):
- pa = 0: plane = (0, 1) ✓ (as computed above)
- pa = PI/2 (facing down): plane = (-1, 0)
  - cameraX = -1: rayDir = (0, 1) + (-1, 0)*(-1) = (1, 1) → lower right... 
  - When facing down (south), the left edge of screen should be... to the right side of the player? 
  
  Hmm wait, I think there might be a sign issue. Let me think about which side is "left" when facing a certain direction.

When facing right (pa=0), left is up (negative Y).
When facing down (pa=PI/2), left is right (positive X).

So for pa=PI/2, leftmost ray (cameraX=-1) should point somewhat to the right and down: (1, 1) normalized. That seems right! Because "left" when facing down is "right" in world coords.

And for pa=PI/2, rightmost ray (cameraX=1) should point somewhat to the left and down: (-1, 1) normalized. That also seems right!

OK so the formulas are:
- dirX = cos(pa), dirY = sin(pa)
- planeX = -sin(pa), planeY = cos(pa)  (plane length is 1, will scale by FOV factor)

Actually wait, the plane length determines FOV. For FOV = PI/3:
- half FOV = PI/6
- center of screen is along dir
- edges of screen are at PI/6 from center
- The screen plane at the center should have length = 2 * tan(PI/6) = 2/sqrt(3) ≈ 1.155

But in Lode's tutorial, the plane starts at length 0.66 for the default FOV. Let me just use the normalized plane (length 1) and scale appropriately.

Actually wait. The relationship between plane length and FOV:
- The plane is the "screen" through which we view
- Half the plane spans tan(FOV/2) perpendicular to the direction
- So full plane length = 2 * tan(FOV/2)

For FOV = PI/3 (60°): plane_length = 2 * tan(PI/6) = 2 * 0.577 = 1.155
For FOV = PI/3.5 (about 51°): plane_length ≈ 1.0
For Wolfenstein-style FOV (~66°): plane_length ≈ 2 * tan(33°) ≈ 1.299

In Lode's tutorial with dir=(−1,0) and plane=(0,0.66), the plane length is 0.66. And in that tutorial, each pixel at x=0 corresponds to cameraX=-1, and x=W-1 to cameraX=1. So:
- cameraX = 2*x/W - 1
- rayDirX = dirX + planeX * cameraX
- rayDirY = dirY + planeY * cameraX

The effective FOV depends on the plane length. With plane=(0, 0.66):
half FOV = atan(0.66 * 1.0) / (sqrt(dirX^2 + dirY^2)) 
= atan(0.66) since dir is unit vector
≈ 33.4°
FOV ≈ 66.8°

OK, so for a Wolfenstein-like FOV, I'll use plane length ≈ 0.66.

Let me just code it with a configurable FOV parameter and compute the plane accordingly.

```javascript
const FOV = Math.PI / 3; // 60 degrees
const planeScale = Math.tan(FOV / 2);

// In the raycasting loop:
let planeX = -Math.sin(pa) * planeScale;
let planeY = Math.cos(pa) * planeScale;
let dirX = Math.cos(pa);
let dirY = Math.sin(pa);
```

Wait, but these should be computed once when pa changes, not every frame... or every ray cast. Actually, I can compute them once per frame.

OK, let me stop deliberating and write the code. I'm going to write the full thing now.

Let me also remember to handle the case where rayDirX or rayDirY is 0 (to avoid division by zero in deltaDist calculations). I'll add a small epsilon.

Here's my approach for the complete file:

I'll now write the complete HTML file. It will be a comprehensive, self-contained raycasting maze shooter.

Let me also think about the enemy AI once more:

Each enemy has:
- x, y position (float)
- health
- state: 'idle', 'alert', 'chase', 'attack', 'hurt', 'die', 'dead'
- alert timer
- attack timer
- sprite animation frame

AI Behavior:
- IDLE: Stand in place. If player is within detection range AND has line of sight, switch to ALERT.
- ALERT: Briefly freeze, then switch to CHASING. Play alert sound.
- CHASING: Move toward player. If close enough and has LOS, switch to ATTACK. Otherwise shoot periodically.
- ATTACK: Stand in place, face player, shoot periodically. If player moves out of range, switch to CHASING.
- HURT: Brief stun, then resume previous state.
- DIE: Play death animation, then remove.
- DEAD: Marked for removal.

Movement toward player:
- Calculate direction vector to player
- Normalize
- Move in that direction, checking for collisions

For the hitscan shooting:
- From the source position, cast a ray toward the target
- If it hits a wall before reaching the target, no hit
- Otherwise, the target takes damage

OK, now let me actually write the complete code. I'll be comprehensive and make it polished.

Let me aim for about 700 lines of well-structured JavaScript.

For the maze, I want it to be a good playable maze. 32x32 should be fine. I'll use recursive backtracking, carving 1-wide passages.

For enemy placement, I'll place them in open areas away from the player's starting position.

For the procedural textures, I'll make them look nice:

**Texture 0: Red Brick**
- Dark red/maroon mortar
- Bricks in a running bond pattern
- Each brick slightly different shade

**Texture 1: Grey Stone**
- Light grey base
- Darker grey veins/edges
- Some dark spots

**Texture 2: Wood Panel**
- Medium brown
- Horizontal grain lines
- Slight color variation

**Texture 3: Blue Tech**
- Dark blue-grey base
- Light blue grid lines
- Yellow/green accent dots

I'll generate these using canvas 2D drawing with some random variation.

For enemy sprites, I'll generate 64x64 pixel art:
- Simple humanoid shape
- Holding a gun
- 2-3 animation frames (stand, walk)
- Death frames

Actually, for simplicity and better visual impact, let me draw enemies as simple geometric creatures. They'll look like colored humanoids.

I'll pre-render each enemy type and animation frame as an ImageData.

Let me also add:
- Drops (health packs, ammo) from killed enemies
- Pickup when player walks over them
- These make the game more engaging

OK writing the code now...

I realize I need to also think about what maps look like. Let me create a maze with a clear layout:

```
1111111111111111
1..............1
1.1.1.1.1.1.1.1
1..............1
1.1.1.1.1.1.1.1
1..............1
..............
etc.
```

Hmm, this is getting complex to describe. Let me just implement the recursive backtracking algorithm properly.

For the maze generation using recursive backtracking:
1. Create a grid of cells (initially all walls)
2. Start at a cell, mark it as empty
3. Pick a random unvisited neighbor
4. Remove the wall between current and chosen neighbor
5. Move to the chosen neighbor
6. Repeat until all cells visited
7. Backtrack when no unvisited neighbors

The standard algorithm uses a 2D array where odd rows/columns are walls and even rows/columns are cells. For a 32x32 map, this gives a 16x16 cell maze with 1-wide passages.

But I want wider passages for gameplay. Let me use a different approach:
- Divide the map into 2x2 cell blocks
- Each block is either a wall or a passage
- Use recursive backtracking on the block grid

With a 32x32 map and 2x2 blocks, that's 16x16 blocks. The recursive backtracking on this creates a 16x16 maze with 2-wide passages.

Actually, let me simplify: use a smaller grid (like 16x16) where each cell is 2x2 tiles, and carve passages between cells. This gives wider corridors.

Or even simpler: just use a simple recursive backtracking on a 16x16 grid and scale up the wall array to 32x32 by doubling each cell. So a 16x16 maze becomes 32x32 with walls and corridors properly sized.

Let me code it. Here's the approach:
1. Create a 16x16 maze using recursive backtracking
2. Each cell in the 16x16 maze maps to a 2x2 area in the 32x32 map
3. Walls are 2 tiles wide, passages are 2 tiles wide

Actually, this would make the map 32x32 but with thick walls. Let me use a simpler approach:

1. Create a raw maze on a 16x16 grid (1 = wall, 0 = passage)
2. Expand to 32x32 by:
   - Each maze cell becomes a 2x2 block
   - Walls between adjacent empty cells get a 2-wide gap carved

OR even simpler: just create a maze directly on 32x32. The cells are at even coordinates, and I remove walls between them.

Here's a clean approach:
```javascript
// Initialize all as walls
for (let y = 0; y < MH; y++)
    for (let x = 0; x < MW; x++)
        map[y*MW+x] = 1;

// Recursive backtracking
function carve(x, y) {
    map[y*MW+x] = 0;
    const dirs = [[0,-1],[0,1],[-1,0],[1,0]];
    shuffle(dirs);
    for (const [dx, dy] of dirs) {
        const nx = x + dx*2;
        const ny = y + dy*2;
        if (nx >= 0 && nx < MW && ny >= 0 && ny < MH && map[ny*MW+nx] === 1) {
            // Also carve the wall between
            map[(y+dy)*MW + (x+dx)] = 0;
            carve(nx, ny);
        }
    }
}

// Start carving from (1, 1)
carve(1, 1);
```

This places cells at odd coordinates and passages (between cells) at even positions. The result is a standard maze where all cells are at odd (x, y) positions.

For a 32x32 map, this gives a 15x15 maze of cells (coordinates 1, 3, 5, ..., 29) with 1-wide passages between them. That's good enough.

The player starts at (1.5, 1.5) (center of cell [1,1]).

Enemies are placed at various empty cells in the maze.

This is a good, clean approach. Let me use it.

Now, for the game, I also want some visual variety. I'll make some walls be different texture types. When generating the maze, I can assign random texture types to different wall cells.

OK I'm going to write the code now. Let me structure it clearly:

```
1. Canvas setup & constants
2. Utility functions (shuffle, distance, etc.)
3. Audio system
4. Texture generation
5. Maze generation
6. Enemy generation & management
7. Input handling
8. Player update (movement, collision)
9. Rendering (floor/ceiling, raycast walls, sprites, z-buffer)
10. HUD (health, ammo, minimap, crosshair)
11. Game state management
12. Main loop
```

Let me write the complete file. I'll make it thorough and well-commented.

Here is the plan for the textures. I'll use an approach where I create textures by drawing geometric patterns:

```javascript
function generateTextures() {
    const textures = [];
    
    for (let t = 0; t < 4; t++) {
        const canvas = document.createElement('canvas');
        canvas.width = canvas.height = TEX;
        const c = canvas.getContext('2d');
        
        // Generate based on type
        switch(t) {
            case 0: // Brick
                c.fillStyle = '#663311';
                c.fillRect(0, 0, TEX, TEX);
                // Draw bricks...
                break;
            // etc.
        }
        
        // Extract pixel data for fast sampling
        const data = c.getImageData(0, 0, TEX, TEX);
        textures.push(data);
    }
    
    return textures;
}
```

Then when sampling a texture pixel at (u, v):
```javascript
const idx = ((v & (TEX-1)) * TEX + (u & (TEX-1))) * 4;
const r = data.data[idx];
const g = data.data[idx + 1];
const b = data.data[idx + 2];
```

This is clean and efficient.

For the screen buffer, I'll write directly to the Uint8ClampedArray of the ImageData:
```javascript
const si = (y * W + x) * 4;
pix[si] = r;
pix[si+1] = g;
pix[si+2] = b;
pix[si+3] = 255;
```

OK, let me finalize and write the complete code now. I'm going to write it all at once, making sure everything is correct.

One more design decision: for the floor/ceiling, I'll use a gradient. The ceiling will be a dark blue-gray, and the floor will be a dark brownish color. This gives a nice atmosphere without the complexity of floor raycasting.

For performance, filling the screen buffer is straightforward - just a loop through all pixels with a gradient calculation.

```javascript
// Fill floor and ceiling
for (let y = 0; y < H; y++) {
    const isFloor = y >= H/2;
    const t = Math.abs(y - H/2) / (H/2); // 0 at horizon, 1 at edge
    const shade = 1 - t * 0.7; // darker at edges
    
    if (isFloor) {
        const r = Math.floor(40 * shade);
        const g = Math.floor(35 * shade);
        const b = Math.floor(25 * shade);
        for (let x = 0; x < W; x++) {
            const i = (y * W + x) * 4;
            pix[i] = r; pix[i+1] = g; pix[i+2] = b; pix[i+3] = 255;
        }
    } else {
        // Ceiling
        const r = Math.floor(20 * shade);
        const g = Math.floor(20 * shade);
        const b = Math.floor(35 * shade);
        for (let x = 0; x < W; x++) {
            const i = (y * W + x) * 4;
            pix[i] = r; pix[i+1] = g; pix[i+2] = b; pix[i+3] = 255;
        }
    }
}
```

This is simple and fast.

Now for the weapon rendering, I'll use canvas 2D drawing after the 3D scene:
```javascript
function drawWeapon() {
    ctx.save();
    // Gun body
    ctx.fillStyle = '#444';
    ctx.beginPath();
    ctx.moveTo(W/2 - 15, H - 60 + bobY);
    // ... draw gun shape
    ctx.fill();
    
    // Muzzle flash
    if (flashTimer > 0) {
        ctx.fillStyle = `rgba(255, 200, 50, ${flashTimer})`;
        ctx.beginPath();
        ctx.arc(W/2 + bobX, H - 100 + bobY, 20 * flashTimer, 0, Math.PI*2);
        ctx.fill();
    }
    ctx.restore();
}
```

OK, I'll make the weapon look like a shotgun or rifle, drawn with simple shapes.

For the minimap:
```javascript
function drawMinimap() {
    const mmScale = 5; // pixels per tile
    const mmX = 10, mmY = 10;
    
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(mmX, mmY, MW*mmScale, MH*mmScale);
    
    for (let y = 0; y < MH; y++) {
        for (let x = 0; x < MW; x++) {
            if (map[y*MW+x] > 0) {
                ctx.fillStyle = '#666';
                ctx.fillRect(mmX + x*mmScale, mmY + y*mmScale, mmScale, mmScale);
            }
        }
    }
    
    // Player dot
    ctx.fillStyle = '#0f0';
    ctx.fillRect(mmX + player.x*mmScale - 1, mmY + player.y*mmScale - 1, 3, 3);
    
    // Enemy dots
    ctx.fillStyle = '#f00';
    for (const e of enemies) {
        if (e.health > 0) {
            ctx.fillRect(mmX + e.x*mmScale - 1, mmY + e.y*mmScale - 1, 2, 2);
        }
    }
}
```

Alright, I'm going to write the complete code now. No more planning.

Let me also add a simple particle effect for the muzzle flash and blood splatter when enemies are hit.

Actually, for simplicity, I'll skip particles and just use screen flash and sprite reactions.

One more thing: I need to handle keyboard input for turning. The WASD controls are:
- W: move forward
- S: move backward
- A: strafe left
- D: strafe right
- Mouse: look left/right
- Arrow Left/Right: turn in place
- Arrow Up/Down: move forward/backward

Mouse movement changes the player angle. Arrow keys provide an alternative to mouse.

For mouse look, I'll use pointer lock and track mouse movement:
```javascript
document.addEventListener('mousemove', e => {
    if (locked) {
        pa += e.movementX * 0.002;
    }
});
```

The sense of mouse movement should be configurable, but I'll set a reasonable default.

OK, WRITING THE CODE NOW. For real. Starting now. No more planning.

Let me go ahead and create the file.

Actually, one tricky part: for the enemy line of sight, I need a raycast from the enemy to the player. But I don't want to run a full raycast for every enemy every frame. Let me optimize:

1. Only check LOS every few frames (e.g., every 10 frames)
2. For the LOS check, I can sample the map at regular intervals along the line from enemy to player

A simpler LOS check:
```javascript
function hasLOS(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    const dist = Math.sqrt(dx*dx + dy*dy);
    const steps = Math.floor(dist);
    for (let i = 1; i < steps; i++) {
        const t = i / steps;
        const cx = x1 + dx * t;
        const cy = y1 + dy * t;
        if (map[Math.floor(cy)*MW + Math.floor(cx)] > 0) {
            return false;
        }
    }
    return true;
}
```

This is a simple interpolation check. It's not perfect (it's walking the line between the two points), but it's good enough for game purposes.

For enemy shooting from the player's perspective: when an enemy shoots, check if the player is close enough and has LOS. If so, the player takes damage.

OK, writing the full code now. I'll structure it into clear sections and make it all work together.

For the sprite rendering, I need to think about how to efficiently render enemies as sprites that respect wall occlusion. Let me use the following approach:

1. Pre-render enemy sprites as arrays of RGBA pixels (64x64 or similar)
2. For each frame, for each visible enemy:
   a. Calculate the transform (depth and horizontal offset in camera space)
   b. Calculate screen bounds
   c. For each column of the sprite:
      - Check zBuffer[column] against sprite depth
      - If closer to camera, draw that column of the sprite
   d. For each pixel in the column:
      - Get the sprite pixel
      - If not transparent, write to screen buffer

This is standard for software renderers. It'll work but might be slow with many enemies.

For performance, I can:
- Only render enemies within a certain distance
- Clamp sprite size to a maximum
- Skip sprites entirely behind walls (check center first)

Let me implement this efficiently.

For the enemy sprite pre-rendering, I'll create a simple 3D-ish looking soldier:
- Head: a circle (skin-colored)
- Body: a rectangle (colored clothes)
- Legs: two rectangles
- Arms: two lines
- Gun: a line with a rectangle at the end

I'll render this to an offscreen canvas and store the pixel data.

For animation, I'll have 2-3 frames:
- Stand: neutral position
- Walk: legs in different positions
- Shoot: arm with gun extended
- Death: falling backward

OK, enough thinking. Let me write the code.

I'll write it methodically, section by section.

Let me start with the basic structure and then fill in each section.

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Maze Shooter</title>
<style>
*{margin:0;padding:0}
html,body{width:100%;height:100%;background:#000;overflow:hidden}
canvas{display:block;cursor:crosshair}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
'use strict';

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

// Screen
const W = 960, H = 600;
canvas.width = W;
canvas.height = H;

// Texture
const TEX = 64;

// Map
const MW = 32, MH = 32;
let map = new Uint8Array(MW * MH);
let wallColors = new Uint8Array(MW * MH); // texture type per cell

// Player
let px = 3.5, py = 3.5, pa = 0;
let pdx = 1, pdy = 0; // direction
let pplaneX = 0, pplaneY = 0.66; // camera plane
let phealth = 100, pammo = 50, pscore = 0;
let pweaponAnim = 0;

// Input
let keys = {};
let mouseDX = 0;
let locked = false;
let shooting = false;

// Enemies
let enemies = [];
let numEnemiesKilled = 0;
let numEnemiesTotal = 0;

// Screen buffer
let imgData = ctx.createImageData(W, H);
let pix = imgData.data;

// Z-buffer
let zbuf = new Float32Array(W);

// Textures (stored as ImageData)
let wallTexes = [];
let spriteTexes = [];

// Timing
let lastTime = performance.now();
let damageFlash = 0;
let shootCooldown = 0;
let gameState = 0; // 0=menu, 1=playing, 2=dead, 3=win

// Audio
let audioCtx = null;

// FPS counter
let fps = 0, fpsTime = 0, frameCount = 0;

// ... (rest of the code)
```

Now let me fill in the details for each section. Actually, let me just write the entire file at once. I've been planning long enough.

For the FOV, let me use the angle-based approach:
- FOV = PI/3 (60 degrees)
- Half FOV = PI/6
- planeScale = tan(PI/6) = 0.577

But I need to recalculate plane when pa changes. Actually, I'll recompute dir and plane every frame from pa. This is simple and avoids accumulated floating-point errors.

```javascript
function updatePlayerVectors() {
    pdx = Math.cos(pa);
    pdy = Math.sin(pa);
    pplaneX = -Math.sin(pa) * 0.66;
    pplaneY = Math.cos(pa) * 0.66;
}
```

Wait, the plane length of 0.66 gives a FOV of 2*atan(0.66) ≈ 67°. That's close to the classic Wolfenstein FOV. Let me keep this.

For the movement speed, I'll use:
- Forward/backward: 3.0 units/second
- Strafe: 2.5 units/second
- Turn: 2.0 radians/second (mouse feel)

For collision detection, I'll check a small radius around the player:
```javascript
function isWall(x, y) {
    const ix = Math.floor(x);
    const iy = Math.floor(y);
    return map[iy * MW + ix] > 0;
}

// In update:
const margin = 0.2;
if (!isWall(px + dx * dt * margin, py)) px += dx * dt;
if (!isWall(px, py + dy * dt * margin)) py += dy * dt;
```

Actually, the proper way is to check a bounding box or circle:
```javascript
function movePlayer(dx, dy) {
    const pad = 0.2;
    const newX = px + dx;
    const newY = py + dy;
    if (!isWall(newX + pad * Math.sign(dx), py)) px = newX; // hmm, this isn't quite right
    if (!isWall(px, newY + pad * Math.sign(dy))) py = newY;
}
```

For a proper approach:
```javascript
const pad = 0.2;
const nextX = px + dx;
const nextY = py + dy;

// Check X movement
if (!isWall(nextX + pad * (dx > 0 ? 1 : dx < 0 ? -1 : 0), py) && 
    !isWall(nextX + pad * (dx > 0 ? 1 : dx < 0 ? -1 : 0), py + pad) &&
    !isWall(nextX + pad * (dx > 0 ? 1 : dx < 0 ? -1 : 0), py - pad)) {
    px = nextX;
}

// Similarly for Y
```

This is getting complicated. Let me keep it simple: just check the four corners of the player's bounding box.

```javascript
function canMove(x, y) {
    const r = 0.2;
    return !isWall(x - r, y - r) && !isWall(x + r, y - r) &&
           !isWall(x - r, y + r) && !isWall(x + r, y + r);
}
```

This should work well enough.

OK, I'm going to write the complete file now. Let me do it in one big pass.

For the enemy sprites, I'll create them programmatically:

```javascript
function generateEnemySprite(color1, color2, color3) {
    // Front-facing humanoid
    const size = 64;
    const c = document.createElement('canvas');
    c.width = c.height = size;
    const cx = c.getContext('2d');
    
    // Background transparent
    cx.clearRect(0, 0, size, size);
    
    // Legs
    cx.fillStyle = color1; // pants
    cx.fillRect(size*0.35, size*0.6, size*0.12, size*0.35);
    cx.fillRect(size*0.53, size*0.6, size*0.12, size*0.35);
    
    // Body
    cx.fillStyle = color2; // shirt
    cx.fillRect(size*0.3, size*0.3, size*0.4, size*0.32);
    
    // Head
    cx.fillStyle = color3; // skin
    cx.beginPath();
    cx.arc(size*0.5, size*0.22, size*0.12, 0, Math.PI*2);
    cx.fill();
    
    // Gun
    cx.fillStyle = '#888';
    cx.fillRect(size*0.47, size*0.2, size*0.06, size*0.35);
    
    return c;
}
```

I'll create 2 enemy types with different colors. For animation, I can create a second frame by offsetting the leg positions.

Actually, for a simpler and more Wolfenstein-like feel, let me create sprites that look more like actual characters. I'll make them 3-frame animations (stand, walk1, walk2).

For the shooting pose:
- Frame: normal stand
- Frame: gun raised
- This adds variety to the animation

Let me at least create stand and walk frames for 2 enemy types.

OK here is my absolute final plan summary:

**GAME FEATURES:**
1. First-person raycasting with textured walls
2. 4 procedural wall textures (brick, stone, wood, tech)
3. Random maze generation (32x32)
4. 8 enemies with AI
5. WASD + mouse controls
6. Arrow key alternative
7. Shooting mechanics
8. Health/ammo pickups
9. HUD with health bar, ammo, minimap, crosshair
10. Weapon viewmodel with shoot animation
11. Damage flash effect
12. Score tracking
13. Level progression
14. Procedural audio (Web Audio API)
15. Menu screen, game over screen

**TECHNICAL:**
- Canvas only, no external assets
- ImageData-based rendering for walls
- Canvas 2D for sprites and HUD overlay
- DDA raycasting algorithm
- Z-buffer for sprite occlusion

Now writing the complete code. Here goes.

For the sprite rendering, I need to be very careful about the z-buffer. Let me use this approach:

```javascript
function renderSprites() {
    // Calculate distances and sort by distance (far to near)
    let spriteList = enemies.filter(e => e.health > 0).map(e => ({
        enemy: e,
        dx: e.x - px,
        dy: e.y - py
    }));
    
    spriteList.sort((a, b) => (b.dx*b.dx + b.dy*b.dy) - (a.dx*a.dx + a.dy*a.dy));
    
    for (const s of spriteList) {
        // Transform sprite to camera space
        const invDet = 1.0 / (pplaneX * pdy - pdx * pplaneY);
        const transformX = invDet * (pdx * s.dy - pdy * s.dx);
        const transformY = invDet * (-pplaneX * s.dy + pplaneY * s.dx);
        
        if (transformY <= 0) continue; // Behind camera
        
        const spriteScreenX = Math.floor((W / 2) * (1 + transformX / transformY));
        const spriteHeight = Math.abs(Math.floor(H / transformY));
        
        const spriteWidth = spriteHeight; // Assuming square sprites
        const drawStartX = Math.max(0, Math.floor(spriteScreenX - spriteWidth / 2));
        const drawEndX = Math.min(W - 1, Math.floor(spriteScreenX + spriteWidth / 2));
        
        const drawStartY = Math.max(0, Math.floor(H / 2 - spriteHeight / 2));
        const drawEndY = Math.min(H - 1, Math.floor(H / 2 + spriteHeight / 2));
        
        // Use the enemy's sprite texture
        const sprite = s.enemy.sprite;
        const spriteData = sprite.ctx.getImageData(0, 0, sprite.width, sprite.height);
        const sp = spriteData.data;
        
        for (let x = drawStartX; x <= drawEndX; x++) {
            // Only draw if this column is closer than the wall
            if (transformY < zbuf[x]) {
                const texX = Math.floor((x - drawStartX) * TEX / spriteWidth);
                
                for (let y = drawStartY; y <= drawEndY; y++) {
                    const texY = Math.floor((y - drawStartY) * TEX / spriteHeight);
                    const spi = (texY * TEX + texX) * 4;
                    const pixi = (y * W + x) * 4;
                    
                    const alpha = sp[spi + 3];
                    if (alpha > 128) {
                        // Distance shading
                        const shade = Math.min(1, 3.0 / (transformY + 0.1));
                        pix[pixi] = sp[spi] * shade;
                        pix[pixi + 1] = sp[spi + 1] * shade;
                        pix[pixi + 2] = sp[spi + 2] * shade;
                        pix[pixi + 3] = 255;
                    }
                }
            }
        }
    }
}
```

Wait, but the sprite should use TEX size for the actual sprite image. The sprite canvas should be TEX x TEX (64x64), and I'm mapping it to the screen projection.

Actually, the sprite resolution doesn't have to match TEX. Let me use a reasonable sprite size. For the enemy sprites, I'll use 64x64 pixel images.

When projecting:
- spriteScreenWidth = spriteHeight (since the sprite is approximately square)
- For each screen column within the projected range, sample the corresponding texture column

Wait, the issue is that the sprite has a fixed texture width, but the screen projection might be wider or narrower. I need to map sprite texture columns to screen columns.

Actually, a simpler approach that looks great in raycasters:
- The projected sprite has a screen width proportional to its screen height
- For each screen column within the sprite's horizontal bounds:
  - Calculate which texture column to sample
  - Draw that column clipped to the z-buffer

Let me reconsider the sizes. If the enemy occupies one tile (1x1 units), and TEX=64, then when the enemy is at distance 1, it should fill about 64 pixels on screen. At distance 10, it should be about 6 pixels.

Actually the sprite height = H / transformY (perpendicular distance), and the width should be the same aspect ratio as the sprite texture.

Let me think about this more carefully. The sprite is drawn at the enemy's position. The distance transformY gives us how big the object appears. If a 1-unit wide object at distance 1 fills the screen width W, then a 1-unit wide sprite at distance d fills W/d screen pixels.

But a standard wall tile is 1 unit wide and at distance 1 it fills `H` screen pixels (from the raycasting formula `lineHeight = H / perpDist`). Actually, lineHeight = H / perpDist, which at distance 1 is H pixels. So 1 unit at distance 1 fills H pixels.

So a 1-unit wide sprite at distance d fills H/d screen pixels wide. An enemy is roughly 0.5 units wide, so its sprite is (H/d) * 0.5 pixels wide.

Hmm, let me use a scaled approach:
```
const scale = H / transformY; // pixels per unit
const spriteScreenWidth = scale * 0.8; // enemy is about 0.8 units wide
const spriteScreenHeight = scale * 0.8;
```

This should give reasonable sprite sizes.

Actually, I think the formula in Lode's tutorial is more standard:
```
spriteHeight = abs(int(screenHeight / transformY))
spriteWidth = spriteHeight // for square sprites
```

But this assumes the sprite is 1 unit tall, which corresponds to the wall height. Let me just use this.

Wait, but what does "1 unit tall" mean for a sprite? If walls are 1 unit tall and appear as `H/perpDist` pixels tall on screen, then a sprite that is 1 unit tall should also appear as `H/perpDist` pixels. That makes sense.

So: `spriteScreenHeight = H / transformY` - but this is `H / spritePerpDist`, not `H / trueDist`. However, `transformY` from the matrix multiplication IS the perpendicular distance. So this is correct.

Actually wait, I need to double-check the coordinate transformation. In Lode's tutorial:
- `transformX = invDet * (dirX * spriteX - dirY * spriteY)`  — wait, his signs might differ
- Let me use the formulas from the tutorial exactly.

From Lode's raycasting tutorial lesson 5:
```cpp
double spriteX = spriteX - posX;
double spriteY = spriteY - posY;

//transform sprite with the inverse camera matrix
double invDet = 1.0 / (planeX * dirY - dirX * planeY);

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY);

int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));
int spriteHeight = abs(int(screenHeight / (transformY)));
```

In this tutorial, the initial setup has:
- dirX = -1, dirY = 0 (facing west)
- planeX = 0, planeY = 0.66

Let me verify with my coordinate system:
- pa = 0: player faces east: dirX = cos(0) = 1, dirY = sin(0) = 0
- pa = 0: planeX = -sin(0) = 0, planeY = cos(0) = 1 → but I want length 0.66, so planeX = 0, planeY = 0.66

Hmm, but cos(0) = 1, not 0.66. So I need:
- planeX = -sin(pa) * 0.66
- planeY = cos(pa) * 0.66

For pa = 0: plane = (0, 0.66) ✓
For pa = PI/2: plane = (-0.66, 0) ✓

OK so:
```javascript
pdx = Math.cos(pa);
pdy = Math.sin(pa);
pplaneX = -Math.sin(pa) * 0.66;
pplaneY = Math.cos(pa) * 0.66;
```

Now for the sprite transform:
- spriteX = enemyX - px
- spriteY = enemyY - py
- invDet = 1.0 / (pplaneX * pdy - pdx * pplaneY) = 1.0 / (-sin(pa)*0.66 * sin(pa) - cos(pa) * (-sin(pa)*0.66)) 
  Wait, that doesn't simplify well. Let me just compute it:
  
  invDet = 1.0 / (pplaneX * pdy - pdx * pplaneY)
  = 1.0 / (-sin(pa)*0.66 * sin(pa) - cos(pa) * (-sin(pa)*0.66))
  
  Hmm wait, let me just compute:
  pplaneX * pdy - pdx * pplaneY
  = (-sin(pa) * 0.66) * sin(pa) - cos(pa) * (cos(pa) * 0.66)
  = -0.66 * sin²(pa) - 0.66 * cos²(pa)
  = -0.66 * (sin²(pa) + cos²(pa))
  = -0.66

So invDet = 1.0 / (-0.66) = -1/0.66 ≈ -1.515

Hmm, negative. Let me check if I have the signs right. In Lode's tutorial:
- dirX = -1, dirY = 0, planeX = 0, planeY = 0.66 (facing west, Y-down screen)
- invDet = 1.0 / (planeX * dirY - dirX * planeY) = 1.0 / (0*0 - (-1)*0.66) = 1.0 / 0.66 ≈ 1.515

In my system:
- dirX = cos(0) = 1, dirY = sin(0) = 0 (facing east), planeX = 0, planeY = 0.66
- invDet = 1.0 / (0*0 - 1*0.66) = 1.0 / (-0.66) ≈ -1.515

The difference in sign is because I'm facing the opposite direction. The formula still works because it changes the signs of both transformX and transformY equally, so the ratio transformX/transformY and the screen position calculation work out correctly.

transformY will be negative, which means the sprite is in front of the player (positive Z in camera space). And the sprite height = abs(H / transformY) which uses absolute value, so it's fine.

OK, I think the math works out. Let me just implement it and test.

Actually, I just realized: the sign of transformY depends on the coordinate system. If transformY < 0 means the sprite is in front, then:
- spriteScreenX = (W/2) * (1 + transformX / transformY)
  With transformY < 0, if transformX > 0 (sprite to the right), transformX/transformY is negative, so spriteScreenX < W/2. But the sprite should be to the right of center! 

Hmm, this means my coordinate system might be off. Let me think again...

Actually with invDet = -1.515 for my setup:
- transformX = -1.515 * (pdy * spriteX - pdx * spriteY)
  = -1.515 * (0 * (ex-px) - 1 * (ey-py))
  = -1.515 * (-(ey-py))
  = 1.515 * (ey-py)

For a sprite to the right (ey > py): transformX > 0
- transformY = -1.515 * (-pplaneY * spriteX + pplaneX * spriteY)
  = -1.515 * (-0.66 * (ex-px) + 0 * (ey-py))
  = -1.515 * (-0.66 * (ex-px))
  = 1.515 * 0.66 * (ex-px)

For a sprite in front (ex > px since facing east): transformY > 0

Oh! So with my coordinate system, transformY > 0 means in front. Let me recheck:
- pa = 0, facing east
- Enemy at (5, 3) (in front and to the right)
- spriteX = 5 - px (say px = 3.5, so spriteX = 1.5)
- spriteY = 3 - py (say py = 3.5, so spriteY = -0.5)

transformY = -1.515 * (-0.66 * 1.5 + 0 * (-0.5)) = -1.515 * (-0.99) = 1.4999

Yes! TransformY is positive. Good.

transformX = -1.515 * (0 * 1.5 - 1 * (-0.5)) = -1.515 * 0.5 = -0.757

spriteScreenX = (960/2) * (1 + (-0.757) / 1.4999) = 480 * (1 - 0.505) = 480 * 0.495 = 237.6

The enemy is to the right of the player (ey > py), so it should appear to the right of center... wait, actually in screen coordinates, right is positive X. Let me check:
- Player faces east (positive X)
- Enemy at (5, 3), player at (3.5, 3.5)
- Relative to player, enemy is in front-right

In screen coordinates (where right is positive):
- The enemy should appear on the right side of the screen
- spriteScreenX = 237.6 (left of center at 480)

That's wrong! The enemy is to the right of the player but appears on the left side of the screen.

Hm, I think there might be a sign issue. Let me look at this more carefully.

When pa = 0 (facing east):
- Looking at the screen, "east" is to the right
- The camera plane is (0, 0.66), which means down in screen Y

If I'm at (3.5, 3.5) facing east, looking at an enemy at (5, 3):
- The enemy is to my right-front
- It should appear on the right side of my screen

But my calculation gives spriteScreenX = 237 (left side). Something is wrong.

Let me recalculate. The formula is:
spriteScreenX = (W/2) * (1 + transformX / transformY)

With W = 960:
spriteScreenX = 480 * (1 + (-0.757) / 1.4999)
= 480 * (1 - 0.505)
= 480 * 0.495
= 237.6

So the sprite is at screen X = 237, which is left of center (480). But the enemy is to the player's right. This is backwards!

I think the issue is with my coordinate system. Let me see...

In Lode's tutorial with dirX = -1, dirY = 0, planeX = 0, planeY = 0.66:
- The player faces west (left on screen if X increases to the right)
- Wait, in a screen with Y-down, if dirX = -1, the player faces LEFT
  
Hmm, that seems wrong for Lode's example. Let me check...

In Lode's tutorial, the 2D map is shown with X increasing to the right and Y increasing downward. The initial player position is (22, 12) with dirX = -1, dirY = 0, planeX = 0, planeY = 0.66.

With dirX = -1: the player faces left (west). With planeX = 0, planeY = 0.66: the camera plane extends downward (positive Y, which is downward on screen).

For the leftmost ray (cameraX = -1):
- rayDirX = -1 + 0*(-1) = -1
- rayDirY = 0 + 0.66*(-1) = -0.66
- Direction: upper-left (negative X, negative Y)

For the rightmost ray (cameraX = 1):
- rayDirX = -1 + 0*(1) = -1
- rayDirY = 0 + 0.66*(1) = 0.66
- Direction: lower-left (negative X, positive Y)

So the view spans from upper-left to lower-left in world space, centered on left (west). This makes sense - the player faces west.

OK so in Lode's setup, positive Y is downward on the screen. This means:
- Facing east (positive X): the player looks to the right
- Looking from (3.5, 3.5) toward (5, 3): the enemy is at positive X (right) and negative Y (up on screen)

The enemy should appear on the right side of the screen (positive X is right in both world and screen).

But with my coordinate system (Y-down for screen), looking east:
dirX = 1, dirY = 0, planeX = 0, planeY = 0.66

Leftmost ray (cameraX = -1):
rayDir = (1, 0) + (0, 0.66)*(-1) = (1, -0.66) → upper-right

Rightmost ray (cameraX = 1):
rayDir = (1, 0) + (0, 0.66)*(1) = (1, 0.66) → lower-right

OK, so the camera spans from upper-right to lower-right, centered on right/east. This makes sense.

Now the enemy at (5, 3):
- spriteX = 5 - 3.5 = 1.5
- spriteY = 3 - 3.5 = -0.5

Since Y is positive downward on screen, negative Y means the enemy is above the player (north in map space). Combined with positive X offset (to the right), the enemy is to the upper-right of the player.

Since the player faces east, an upper-right enemy should appear on the right side of the screen (slightly above center).

My calculation gave transformX = -0.757, which means spriteScreenX < W/2 (left side). That's wrong if the enemy is to the player's right.

Let me recalculate more carefully:
```
invDet = 1.0 / (planeX * dirY - dirX * planeY)
       = 1.0 / (0 * 0 - 1 * 0.66)
       = 1.0 / (-0.66)
       = -1.515

transformX = invDet * (dirY * spriteX - dirX * spriteY)
           = -1.515 * (0 * 1.5 - 1 * (-0.5))
           = -1.515 * (0 + 0.5)
           = -0.7575

transformY = invDet * (-planeY * spriteX + planeX * spriteY)
           = -1.515 * (-0.66 * 1.5 + 0 * (-0.5))
           = -1.515 * (-0.99)
           = 1.499
```

So transformX = -0.757, transformY = 1.499

spriteScreenX = 480 * (1 + (-0.757) / 1.499)
              = 480 * (1 - 0.505)
              = 480 * 0.495
              = 237.6

The result is 237.6, which is left of center. But the enemy should be to the right!

Hmm, let me think about what's going wrong. Maybe my plane orientation is wrong?

Let me think about it differently. The camera plane should be such that:
- The leftmost ray points to the left side of the screen
- The rightmost ray points to the right side of the screen

With pa = 0 (facing east):
- Left of the view should be "up" (north, negative Y) 
- Right of the view should be "down" (south, positive Y)

So for cameraX = -1 (left): ray should point somewhat up/north → negative Y component
For cameraX = 1 (right): ray should point somewhat down/south → positive Y component

With planeX = 0, planeY = 0.66:
- cameraX = -1: rayDir = (1, 0) + (0, 0.66)*(-1) = (1, -0.66) → negative Y component ✓ (up/north)
- cameraX = 1: rayDir = (1, 0) + (0, 0.66)*(1) = (1, 0.66) → positive Y component ✓ (down/south)

OK so the raycasting is consistent. Let me think about the sprite...

The enemy is at world (5, 3), player at (3.5, 3.5). The enemy is northeast of the player (positive X, negative Y in the screen-down Y system).

When the player faces east, looking at the center of the screen (rayDir = (1, 0)):
- The enemy at (5, 3) relative to player is at (1.5, -0.5)
- The angle from the forward direction to the enemy: atan2(-0.5, 1.5) ≈ -18.4° (above center, since negative Y is up)
- On the screen, this should be in the upper half (above center) and roughly centered-right

The sprite should appear roughly at screen X = W/2 + something, screen Y < H/2. Let me see:

spriteScreenX should be > W/2 if the enemy is to the right of center...

Wait, I think the issue might be: what does "right" mean on the screen? In my coordinate system:
- Screen X increases to the right
- Enemy at world (5, 3) → east of player, slightly north
- When facing east, the enemy is almost straight ahead, slightly above center

So the sprite should be near the center horizontally, slightly below (or above?) center vertically.

spriteScreenX = 237 ≈ left of center → THIS IS WRONG

Let me double-check with Lode's formula but his coordinate system. In Lode's setup:
- dirX = -1, dirY = 0 (facing west)
- planeX = 0, planeY = 0.66

A sprite at position relative to player of (spriteX, spriteY) = (-1.5, 0.5) (in front-left, below in Y-down):

transformX = 1/0.66 * (-1*0.5 - 0*(-1.5)) = 1.515 * (-0.5) = -0.757
transformY = 1/0.66 * (-0.66*(-1.5) + 0*0.5) = 1.515 * 0.99 = 1.499

spriteScreenX = 480 * (1 + (-0.757)/1.499) = 480 * 0.495 = 237.6

Hmm, same number. But the sprite is at (-1.5, 0.5) relative to player facing west... Let me think about this differently.

If the player faces west (negative X), then:
- "Left" of the view is in the -Y direction (up on screen, since Y-down)
- "Right" of the view is in the +Y direction (down on screen)

The sprite at relative position (-1.5, 0.5):
- X = -1.5 is in the same direction as facing (west) → in front
- Y = 0.5 is downward → to the right of the view (when facing west)

So it should be to the RIGHT of center, but my calculation says 237 (LEFT).

Something is definitely wrong. Let me look at the Lode tutorial formula more carefully.

From the tutorial:
```cpp
double invDet = 1.0 / (planeX * dirY - dirX * planeY);
double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY);
```

With dirX = -1, dirY = 0, planeX = 0, planeY = 0.66:
invDet = 1.0 / (0*0 - (-1)*0.66) = 1/0.66 = 1.515

For sprite at (-1.5, 0.5) relative:
transformX = 1.515 * (0*(-1.5) - (-1)*0.5) = 1.515 * 0.5 = 0.757
transformY = 1.515 * (-0.66*(-1.5) + 0*0.5) = 1.515 * 0.99 = 1.499

spriteScreenX = 480 * (1 + 0.757/1.499) = 480 * 1.505 = 722.4

That's to the RIGHT of center! So I must have had the wrong sign. Let me recheck:

Oh! I see the issue. In my calculation above I had `dirY * spriteX - dirX * spriteY`:
- With Lode's values: 0*(-1.5) - (-1)*(0.5) = 0 + 0.5 = 0.5
- With my values: 0*1.5 - 1*(-0.5) = 0 + 0.5 = 0.5

But then: invDet * 0.5:
- Lode: 1.515 * 0.5 = 0.757 ✓ (positive → right of center)
- Mine: -1.515 * 0.5 = -0.757 ✗ (negative → left of center)

The issue is the invDet! Mine is negative because:
- Lode: planeX * dirY - dirX * planeY = 0*0 - (-1)*0.66 = 0.66 → invDet = 1.515 > 0
- Mine: planeX * dirY - dirX * planeY = 0*0 - 1*0.66 = -0.66 → invDet = -1.515 < 0

The sign of invDet affects everything. With my coordinate system (facing east, positive X), the invDet is negative. This flips the signs of both transformX and transformY.

When transformY is negative: spriteHeight = abs(H / transformY), which is positive. Good.

When transformX / transformY: both are negative, so the ratio is positive. Let me recalculate:

transformX = -1.515 * (0*1.5 - 1*(-0.5)) = -1.515 * 0.5 = -0.757
transformY = -1.515 * (-0.66*1.5 + 0*(-0.5)) = -1.515 * (-0.99) = 1.499

Oh wait, transformY is positive (1.499), not negative! Let me recompute:

transformY = invDet * (-planeY * spriteX + planeX * spriteY)
= -1.515 * (-0.66 * 1.5 + 0 * (-0.5))
= -1.515 * (-0.99)
= 1.499

So transformY = 1.499 (positive). And transformX = -0.757 (negative).

spriteScreenX = 480 * (1 + (-0.757) / 1.499) = 480 * (1 - 0.505) = 480 * 0.495 = 237.6

The enemy is to the right of the player and in front. The screen X is left of center. That's wrong!

Hmm wait. Let me reconsider. Maybe I have my coordinate system inconsistent.

In my setup:
- X increases to the right
- Y increases downward (screen convention)
- pa = 0: facing east (right)
- dir = (cos(0), sin(0)) = (1, 0) facing right/east

Now, the camera plane (0, 0.66): this points downward (positive Y).

Leftmost ray (cameraX = -1):
rayDir = (1, 0) + (0, 0.66)*(-1) = (1, -0.66) → points up-right

Rightmost ray (cameraX = 1):
rayDir = (1, 0) + (0, 0.66)*(1) = (1, 0.66) → points down-right

So the screen spans from "up-right" to "down-right", centered on "right" (east). This makes sense.

The enemy at (5, 3) relative to player at (3.5, 3.5):
- Relative X = 1.5 (in front, positive X)
- Relative Y = -0.5 (above, since Y-down means negative = up)

The enemy is slightly above the view center. On screen, it should be slightly above center (screen Y < H/2) and roughly at center X or slightly...

Actually, "above" in world coordinates means above center in the screen. The horizontal position should be near center since the enemy is almost directly ahead.

But I got screen X = 237, which is LEFT of center. Let me think about why...

Oh wait, I think I see the issue. The camera plane determines what "left" and "right" mean on screen. The camera plane is (0, 0.66). When I decrease cameraX from 0 to -1, I'm subtracting camera plane from the direction, which changes rayDir from (1, 0) to (1, -0.66). This means "left" in screen space corresponds to the direction of negative Y (upward) in the world.

For the enemy at relative position (1.5, -0.5):
- It's in front and slightly above
- Since "left" = up in my coordinate system, an enemy slightly above center should be slightly LEFT of center on screen

spriteScreenX = 237 ≈ left of center at 480 → 256 pixels left of center → This makes sense! The enemy is slightly above the horizontal midline, so it appears slightly left of center (in my coordinate system where "left" = up).

Wait, that means "left" of screen = up, and "right" of screen = down in my coordinate system? That seems odd. Let me reconsider.

When facing east (right):
- Screen left side = what you see to your left when facing east = north = upward = negative Y
- Screen right side = south = downward = positive Y

An enemy to the player's right (positive X direction) and above (negative Y direction):
- Should appear on the RIGHT side of the screen (positive X is to the right)

But wait, we're facing east. The enemy is at (5, 3) from (3.5, 3.5). It's ahead and to the... let me think.

If I'm standing at (3.5, 3.5) facing east (increasing X):
- My left is north (decreasing Y)
- My right is south (increasing Y)

The enemy at (5, 3) is ahead and to the NORTH (3 < 3.5, Y is decreasing = north). Since I'm facing east, north is to my LEFT.

So the enemy should appear on the LEFT side of the screen! But wait, that still seems wrong with common screen conventions.

Ah, I see the confusion. In screen coordinates where Y increases downward:
- "North" on the map = decreasing Y = upward on screen
- When facing east, left is north = up on screen

The enemy at Y=3 (above the player in screen coordinates since Y=3 < 3.5) is indeed to the left when facing east. So it should appear on the LEFT side of the screen.

spriteScreenX = 237 (left of center) ✓ ✓ ✓

It was correct all along! I was confusing myself.

Let me verify with a sprite to the right (south, Y > player Y):
Enemy at (5, 4) from (3.5, 3.5):
- Relative: (1.5, 0.5) (ahead and to the south = to the player's right)

transformX = -1.515 * (0*1.5 - 1*0.5) = -1.515 * (-0.5) = 0.757
transformY = -1.515 * (-0.66*1.5 + 0*0.5) = -1.515 * (-0.99) = 1.499

spriteScreenX = 480 * (1 + 0.757/1.499) = 480 * 1.505 = 722.4

722 is to the RIGHT of center, which is correct because this enemy is to the player's south (right when facing east).

OK! The formulas are correct! My earlier confusion was just about which direction things map on screen.

Now let me also verify what happens with the sprite height. For the at (237, 1.499):
spriteHeight = abs(H / transformY) = abs(600 / 1.499) ≈ 400 pixels

That seems very tall. For an enemy at distance ~1.5 units, the sprite should be quite large. Let me check: at distance 1.5 with FOV 60°, a 1-unit-tall enemy should appear as 600/1.5 = 400 pixels tall. That actually seems right for a 60-degree FOV - the enemy is very close and would dominate the screen.

An enemy 10 units away: spriteHeight = 600/10 = 60 pixels. That's a reasonable size for a distant enemy.

OK the math works out. Let me think about the sprite mapping.

If the sprite is 64x64 texels and the projected height is 400 pixels:
- Each texel maps to 400/64 ≈ 6.25 pixels
- For a 60-pixel high sprite, each texel maps to 60/64 ≈ 0.94 pixels (some texels will be skipped)

To handle this, I should sample the sprite texture at a rate that covers all screen pixels. For each screen column within the sprite's horizontal bounds, I determine the corresponding texture column, and then for each screen row within the sprite's vertical bounds, I determine the texture row and sample the pixel.

For the mapping:
```javascript
const texX = Math.floor((x - drawStartX) * TEX / spriteScreenWidth) & (TEX - 1); // if TEX is power of 2
```

Wait, actually the sprite map coordinates should map the screen projection to the texture space:
```javascript
const u = (x - (spriteScreenX - spriteScreenWidth/2)) / spriteScreenWidth * TEX;
```

And for each row:
```javascript
const v = (y - (spriteDrawStartY)) / spriteHeight * TEX;
```

OK I think I have a solid understanding now. Let me just write the code.

For performance, one important optimization: I can skip sprite columns where zBuffer[x] < transformY (the wall is closer than the sprite). This means I only draw columns where the sprite is visible.

Let me also cap the sprite height to avoid drawing enormously large sprites:
```javascript
const maxHeight = H * 2; // don't draw sprites bigger than 2x screen height
const spriteScreenHeight = Math.min(maxHeight, Math.abs(H / transformY));
```

And skip sprites that are completely off-screen:
```javascript
if (drawStartX > W - 1 || drawEndX < 0) continue;
```

OK I'm going to write the final code now. Let me compose the whole file.

Actually, one more thing: for the weapon sway and bob. I'll add:
- A slight side-to-side sway as the player walks
- The weapon lifts slightly when the player moves

```javascript
const bobSpeed = 0.1;
const bobAmount = 0.005;
const weaponBobX = Math.sin(gameTime * bobSpeed) * bobAmount * moveSpeed;
const weaponBobY = Math.abs(Math.cos(gameTime * bobSpeed)) * bobAmount * moveSpeed;
```

Actually, for the weapon, since we draw it after the 3D scene, we use canvas 2D:
```javascript
function drawWeapon() {
    const bobX = Math.sin(gameTime * 8) * 3;
    const bobY = Math.abs(Math.sin(gameTime * 8)) * 2;
    const gunY = H - 100 + bobY;
    
    // Gun body
    ctx.fillStyle = '#333';
    ctx.fillRect(W/2 - 20 + bobX, gunY, 40, 80);
    
    // Barrel
    ctx.fillStyle = '#555';
    ctx.fillRect(W/2 - 5 + bobX, gunY - 50, 10, 55);
    
    // Muzzle flash
    if (flashTimer > 0) {
        ctx.fillStyle = `rgba(255, ${200*flashTimer}, 50, ${flashTimer})`;
        ctx.beginPath();
        ctx.arc(W/2 + bobX, gunY - 55, 15 + 20 * flashTimer, 0, Math.PI * 2);
        ctx.fill();
    }
    
    // Reload animation
    // ...
}
```

OK NOW I WILL WRITE THE CODE. No more deliberation.

Let me write the complete index.html file.

Alright, here's my final mental model:

```
File: index.html
Structure:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Raycaster Maze Shooter</title>
<style>
/* Full screen dark background, canvas fills viewport */
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
'use strict';

// ============================================================
// CONFIGURATION
// ============================================================
const W = 960, H = 600;
const TEX = 64;
const MW = 32, MH = 32;
const FOV = 0.66; // camera plane length (~66 degree FOV)
const MOVE_SPEED = 3.0;
const MOUSE_SENSE = 0.002;
const SHOOT_COOLDOWN = 0.3; // seconds

// ============================================================
// CANVAS SETUP
// ============================================================
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = W;
canvas.height = H;

// ============================================================
// STATE
// ============================================================
let px, py, pa; // player x, y, angle
let pdx, pdy, pplaneX, pplaneY;
let phealth, pammo, pscore;
let map; // Uint8Array
let wallTexType; // which texture for each wall cell
let enemies;
let gameState; // 'menu', 'playing', 'dead', 'won'
let lastTime, damageFlash, shootCooldown, gameTime;
let weaponAnim; // 0 = idle, 1-10 = shooting animation
let keys = {};
let mouseDX = 0;
let locked = false;

// Rendering
let imgData = ctx.createImageData(W, H);
let pix = imgData.data;
let zbuf = new Float32Array(W);

// Textures
let wallTexes = []; // array of ImageData
let spriteTexes = []; // array of {frames: [ImageData], width, height}

// ============================================================
// AUDIO (Web Audio API - procedural)
// ============================================================
let audioCtx;

function initAudio() {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}

function playSound(type) {
    if (!audioCtx) return;
    const now = audioCtx.currentTime;
    
    if (type === 'shoot') {
        // Short noise burst
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.1, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / data.length);
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.3;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    } else if (type === 'hit') {
        // Thud
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.05, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) data[i] = Math.sin(i * 200 * Math.PI / audioCtx.sampleRate) * (1 - i / data.length) * 0.3;
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.2;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    } else if (type === 'enemyDie') {
        // Falling tone
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.3, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) {
            const freq = 400 - i * 1000 / data.length;
            data[i] = Math.sin(i * freq * Math.PI / audioCtx.sampleRate) * (1 - i / data.length) * 0.2;
        }
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.3;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    } else if (type === 'enemyShoot') {
        // Distant shot
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.15, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * (1 - i / data.length) * 0.15;
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.15;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    } else if (type === 'pickup') {
        // Short sine up
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.15, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) {
            data[i] = Math.sin(i * (200 + i * 800 / data.length) * Math.PI / audioCtx.sampleRate) * (1 - i / data.length) * 0.2;
        }
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.3;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    } else if (type === 'hurt') {
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.15, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) {
            data[i] = (Math.random() * 2 - 1) * (1 - i / data.length) * 0.3;
        }
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.3;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    } else if (type === 'levelComplete') {
        // Victory jingle
        const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.8, audioCtx.sampleRate);
        const data = buf.getChannelData(0);
        const notes = [523, 659, 784, 1047];
        const noteLen = 0.15;
        for (let i = 0; i < data.length; i++) {
            const t = i / audioCtx.sampleRate;
            const noteIdx = Math.floor(t / noteLen);
            if (noteIdx >= notes.length) { data[i] = 0; continue; }
            const freq = notes[noteIdx];
            data[i] = Math.sin(t * freq * 2 * Math.PI) * (1 - (t % noteLen) / noteLen) * 0.2;
        }
        const src = audioCtx.createBufferSource();
        src.buffer = buf;
        const gain = audioCtx.createGain();
        gain.gain.value = 0.3;
        src.connect(gain).connect(audioCtx.destination);
        src.start(now);
    }
}

// ============================================================
// PROCEDURAL TEXTURES
// ============================================================
function generateTextures() {
    const textures = [];
    
    for (let t = 0; t < 4; t++) {
        const c = document.createElement('canvas');
        c.width = TEX; c.height = TEX;
        const cx = c.getContext('2d');
        
        switch(t) {
            case 0: // Red Brick
                cx.fillStyle = '#553322';
                cx.fillRect(0, 0, TEX, TEX);
                // Brick pattern
                const brickH = 16, brickW = 32, mortar = 2;
                for (let row = 0; row < TEX / brickH; row++) {
                    const offset = (row % 2) * (brickW / 2);
                    for (let col = -1; col < TEX / brickW + 1; col++) {
                        const bx = offset + col * brickW + 1;
                        const by = row * brickH + 1;
                        const shade = 0.85 + Math.random() * 0.15;
                        cx.fillStyle = `rgb(${150*shade}, ${70*shade}, ${40*shade})`;
                        cx.fillRect(bx, by, brickW - mortar, brickH - mortar);
                    }
                }
                break;
                
            case 1: // Grey Stone
                cx.fillStyle = '#888';
                cx.fillRect(0, 0, TEX, TEX);
                // Add noise/veining
                const imageData = cx.getImageData(0, 0, TEX, TEX);
                const d = imageData.data;
                for (let i = 0; i < d.length; i += 4) {
                    const noise = (Math.random() - 0.5) * 40;
                    d[i] = Math.min(255, Math.max(0, d[i] + noise));
                    d[i+1] = Math.min(255, Math.max(0, d[i+1] + noise));
                    d[i+2] = Math.min(255, Math.max(0, d[i+2] + noise - 10));
                }
                cx.putImageData(imageData, 0, 0);
                // Add some edge lines
                ctx.strokeStyle = 'rgba(100,100,100,0.3)';
                for (let i = 0; i < 8; i++) {
                    cx.beginPath();
                    cx.moveTo(Math.random()*TEX, Math.random()*TEX);
                    cx.lineTo(cx.lineX || 0, cx.lineY || 0);
                    cx.stroke();
                }
                break;
                
            case 2: // Wood Panel
                cx.fillStyle = '#8B6914';
                cx.fillRect(0, 0, TEX, TEX);
                // Horizontal planks
                for (let row = 0; row < TEX; row += 13) {
                    cx.fillStyle = `rgb(${120 + Math.random()*20}, ${90 + Math.random()*20}, ${25 + Math.random()*15})`;
                    cx.fillRect(0, row, TEX, 12);
                    // Vertical grain
                    for (let g = 0; g < 15; g++) {
                        const gx = Math.random() * TEX;
                        cx.fillStyle = 'rgba(0,0,0,0.1)';
                        cx.fillRect(gx, row, 1, 12);
                    }
                }
                // Darker edges at plank borders
                for (let row = 0; row < TEX; row += 13) {
                    cx.fillStyle = 'rgba(0,0,0,0.3)';
                    cx.fillRect(0, row, TEX, 2);
                }
                break;
                
            case 3: // Blue Tech
                cx.fillStyle = '#334';
                cx.fillRect(0, 0, TEX, TEX);
                // Grid lines
                cx.strokeStyle = 'rgba(80, 120, 200, 0.4)';
                cx.lineWidth = 1;
                for (let x = 0; x < TEX; x += 16) {
                    cx.beginPath();
                    cx.moveTo(x, 0);
                    cx.lineTo(x, TEX);
                    cx.stroke();
                }
                for (let y = 0; y < TEX; y += 16) {
                    cx.beginPath();
                    cx.moveTo(0, y);
                    cx.lineTo(TEX, y);
                    cx.stroke();
                }
                // LED dots
                for (let x = 8; x < TEX; x += 16) {
                    for (let y = 8; y < TEX; y += 16) {
                        cx.fillStyle = Math.random() > 0.3 ? 'rgba(50, 200, 50, 0.6)' : 'rgba(200, 50, 50, 0.4)';
                        cx.beginPath();
                        cx.arc(x + Math.random()*4-2, y + Math.random()*4-2, 2, 0, Math.PI*2);
                        cx.fill();
                    }
                }
                break;
        }
        
        textures.push(cx.getImageData(0, 0, TEX, TEX));
    }
    
    return textures;
}

// ... (more code to follow)
```

OK I think I actually need to just write the whole thing in one go. Let me do that now. I'll write it as a complete, working HTML file.

For the stone texture, I messed up the edge line rendering. Let me fix that - I was using `ctx` instead of `cx`. Let me also not add those random lines; instead use a different approach for the stone texture.

For enemy sprites, I'll pre-render them programmatically:
- Type 1 (Soldier): Tan/beige head, blue body, gray pants
- Type 2 (Fast): Green head, brown body, darker pants
- Each has 2 frames (stand, walk) and 2 death frames

Actually, for simplicity, I'll just make 2-3 frames per enemy:
- Stand frame (frame 0)
- Walk frame 1 (frame 1)  
- Walk frame 2 (frame 2)

The game can pick which frame to show based on animation time.

For death, enemies will have a simple animation where they shrink and fade, or just disappear after a brief delay.

Let me also add some variety: enemies can have different starting poses (standing or patrolling).

For the enemy patrolling behavior, I can have some enemies walk back and forth along a corridor.

Actually, let me keep the behavior simple: enemies stand still until they detect the player, then they chase and shoot.

Here's my refined AI logic:
1. Calculate distance to player
2. If distance < DETECTION_RANGE and has line of sight → ALERT
3. If ALERT, move toward player
4. If distance < ATTACK_RANGE and has line of sight → SHOOT periodically
5. If hit by player → take damage, brief HURT state
6. If health <= 0 → die (brief animation, then remove)

Movement: move toward player. If blocked by wall, try to adjust direction. Simple steering.

OK let me write the complete code now. I'm going to write the whole file from start to finish.

Let me think about one more thing: the map should have some interesting layout. A pure random maze can be confusing. Let me add some open rooms or wider areas to make it more playable.

I'll modify the maze generation: after generating the basic maze, I'll randomly select a few cells and expand them into 2x2 open areas. This creates small rooms for variety.

Actually, to keep things simpler, let me just use the straight maze for now and see how it plays.

For the level, I'll regenerate the maze when the player clears all enemies, creating a new "level".

Let me also add:
- Health pickups (from killed enemies)
- Ammo pickups (from killed enemies)
- These spawn at the enemy's position when they die

Pickup items:
- Health: +25 health, appears as a red cross
- Ammo: +10 ammo, appears as yellow boxes

OK, here's my complete implementation plan. Writing at last:

```
Line count estimate: ~800 lines of JS
Total file: ~900 lines
```

Let me write it all now. I'll be comprehensive and make sure everything compiles correctly.

For texture generation, stone texture:
```javascript
case 1: // Grey Stone
    // Fill with base color
    cx.fillStyle = '#999';
    cx.fillRect(0, 0, TEX, TEX);
    
    // Add large blobs for stone texture
    const imgData = cx.getImageData(0, 0, TEX, TEX);
    const d = imgData.data;
    for (let i = 0; i < d.length; i += 4) {
        const noise = (Math.random() - 0.5) * 50;
        d[i] += noise;     // R
        d[i+1] += noise;   // G  
        d[i+2] += noise - 10; // B (slightly bluer)
    }
    cx.putImageData(imgData, 0, 0);
    break;
```

This creates a noisy stone texture. Good enough.

For wood texture:
```javascript
case 2: // Wood Panel
    cx.fillStyle = '#8B6914';
    cx.fillRect(0, 0, TEX, TEX);
    
    // Horizontal grain lines
    for (let y = 0; y < TEX; y++) {
        const grain = Math.sin(y * 0.5) * 10;
        const brightness = 60 + grain + (Math.random() - 0.5) * 10;
        cx.fillStyle = `rgb(${140 + brightness * 0.3}, ${100 + brightness * 0.2}, ${30 + brightness * 0.15})`;
        // Actually, this would draw a line for each row. Let me do it differently.
        // Use an alternative approach: draw individual pixels for grain
    }
```

Hmm, drawing pixel by pixel for the wood grain would be slow. Let me use a different approach with ImageData.

Actually, for the canvas approach, let me draw the base color and then overlay some detail patterns:

```javascript
case 2: // Wood Panel
    // Base wood color
    cx.fillStyle = '#8B6914';
    cx.fillRect(0, 0, TEX, TEX);
    
    // Darker horizontal lines for wood grain
    cx.strokeStyle = 'rgba(60, 35, 5, 0.15)';
    cx.lineWidth = 1;
    for (let y = 0; y < TEX; y += 3) {
        cx.beginPath();
        cx.moveTo(0, y);
        cx.lineTo(TEX, y);
        cx.stroke();
    }
    
    // Lighter grain lines
    cx.strokeStyle = 'rgba(180, 140, 50, 0.1)';
    for (let y = 1; y < TEX; y += 5) {
        cx.beginPath();
        cx.moveTo(0, y);
        cx.lineTo(TEX, y);
        cx.stroke();
    }
    
    // Plank edges
    for (let y = 0; y < TEX; y += 16) {
        cx.fillStyle = 'rgba(0, 0, 0, 0.15)';
        cx.fillRect(0, y, TEX, 2);
    }
    break;
```

This should produce a reasonable wood-grain effect.

OK, let me just write the whole thing now. I need to be decisive and efficient.

Here's the FINAL file structure:

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Raycaster Maze Shooter</title>
<style>
*{margin:0;padding:0}
html,body{width:100%;height:100%;background:#000;overflow:hidden;cursor:crosshair}
canvas{display:block;position:absolute;top:0;left:0}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
'use strict';
// ... complete game code about 800 lines
</script>
</body>
</html>
```

Let me write it now. I'll be thorough but won't second-guess myself as much.

Actually, let me reconsider my approach for the wood texture. Let me use ImageData directly for all textures - it gives me more control.

```javascript
function generateStoneTexture() {
    const c = document.createElement('canvas');
    c.width = TEX; c.height = TEX;
    const cx = c.getContext('2d');
    const img = cx.createImageData(TEX, TEX);
    for (let i = 0; i < img.data.length; i += 4) {
        const v = 128 + Math.floor((Math.random() - 0.5) * 50);
        img.data[i] = v;
        img.data[i+1] = v;
        img.data[i+2] = v - 10;
        img.data[i+3] = 255;
    }
    cx.putImageData(img, 0, 0);
    return cx.getImageData(0, 0, TEX, TEX);
}
```

Wait, I'm overcomplicating the textures. Let me use a simple approach: draw with canvas 2D for all textures. The patterns will be clean and it's fast to code.

For the stone texture, I'll draw a bunch of random rectangles with slightly different gray shades:

```javascript
case 1: // Grey Stone
    for (let i = 0; i < 20; i++) {
        const shade = 80 + Math.random() * 60;
        cx.fillStyle = `rgb(${shade}, ${shade}, ${shade - 10})`;
        cx.fillRect(Math.random()*TEX|0, Math.random()*TEX|0, 
                    10 + Math.random()*30|0, 10 + Math.random()*30|0);
    }
    // Add fine grain
    const img = cx.getImageData(0, 0, TEX, TEX);
    for (let j = 0; j < img.data.length; j += 4) {
        const n = (Math.random() - 0.5) * 30;
        img.data[j] += n;
        img.data[j+1] += n;
        img.data[j+2] += n;
    }
    cx.putImageData(img, 0, 0);
    break;
```

OK I think I've spent way too long planning. Let me just write the complete file now.

Key decisions:
1. Use ImageData for screen buffer (pixel-level control for walls and sprites)
2. Use canvas 2D for HUD overlay (text, minimap, weapon)
3. Use canvas 2D for texture generation
4. Convert textures to ImageData for pixel sampling during raycasting
5. All in one file, no external resources

Let me compose the final code. I'll write it completely and correctly.

For the minimap, I'll use canvas 2D since it's just simple rectangles.

For the HUD, I'll use canvas 2D for:
- Health bar
- Ammo count
- Score
- Crosshair
- Minimap
- Level info
- Menu/Game over screens

For the weapon, I'll use canvas 2D for the gun model.

The only part using ImageData is the 3D rendering (walls, floor, ceiling, sprites).

After rendering the 3D scene to the ImageData, I'll put it on canvas, then use canvas 2D for everything else on top.

Alright, here's the code. I'll write it now, completely.

For pointer lock, I need:
```javascript
canvas.addEventListener('click', () => {
    canvas.requestPointerLock();
});

document.addEventListener('pointerlockchange', () => {
    locked = document.pointerLockElement === canvas;
});

document.addEventListener('mousemove', e => {
    if (locked) {
        pa += e.movementX * MOUSE_SENSE;
    }
});
```

Let me also handle keyboard input properly:
```javascript
document.addEventListener('keydown', e => {
    keys[e.code] = true;
    e.preventDefault();
});
document.addEventListener('keyup', e => {
    keys[e.code] = false;
});
```

And I'll use both WASD and arrow keys:
- KeyW or ArrowUp: move forward
- KeyS or ArrowDown: move back
- KeyA or ArrowLeft: turn left (or strafe left with some modifier)
- KeyD or ArrowRight: turn right (or strafe right)
- Space: strafe right (or jump in games, but no jumping here)
- Shift: strafe left

Actually, let me keep it simple:
- W: move forward
- S: move backward
- A: strafe left
- D: strafe right
- Mouse: look around
- Arrow Left/Right: turn in place
- Arrow Up/Down: move forward/backward
- Click: shoot
- R: reload

For the enemy shooting: each enemy has a shoot timer. When they can see the player and the timer is up, they shoot. The player takes damage based on distance (closer = more damage).

Let me think about enemy projectile... actually in Wolfenstein 3D, enemy shots are also hitscan. So I'll check if the enemy has a clear line of sight to the player, and if so, the player takes instant damage.

Damage calculation:
- Player shot: 15-25 damage depending on distance
- Enemy shot: 10-20 damage

OK, enough. Let me write the complete code.

One final thought: I want to make sure the game is actually FUN. Key things:
1. Clear feedback when shooting (visual + audio)
2. Enemies provide a good challenge
3. Maze is navigable (not too confusing)
4. Visual clarity (easy to distinguish walls, enemies, HUD)

For the maze, I'll use a decent size with reasonable density. The maze should have enough open space to maneuver.

For visual clarity:
- Different wall textures should be visually distinct
- Enemies should be bright/colorful against the muted walls
- HUD should be readable
- The crosshair should be visible

Let me finalize the color palette:
- Wall textures: brick (red-brown), stone (gray), wood (brown), tech (blue-gray)
- Enemy: bright red/green (depending on type)
- Player weapon: dark gray
- HUD: green text on dark background
- Crosshair: white
- Floor: dark brown
- Ceiling: dark blue-gray

OK, writing the complete code NOW. This is it.

Hmm, actually I realize I should also handle the case where the player tries to move diagonally into a corner (both x and y blocked). With separate x and y collision checks, this should be handled naturally.

```javascript
// Try to move in X
const newX = px + dx;
if (canMove(newX, py)) px = newX;

// Try to move in Y
const newY = py + dy;
if (canMove(px, newY)) py = newY;
```

This handles diagonal movement correctly because each axis is checked independently.

But there's a subtlety: if both X and Y are blocked (corner), the player won't move at all, which is correct. If only one axis is blocked, the player slides along the wall, which is also correct and feels good.

OK time to code. I'm going to write this as a complete, polished, working game.

Here we go for REAL.

I'll organize the code carefully:

```javascript
// Section 1: Setup & Config
// Section 2: Audio
// Section 3: Texture Generation
// Section 4: Maze Generation
// Section 5: Enemy Management
// Section 6: Input
// Section 7: Game Update (player movement, enemy AI, combat)
// Section 8: Rendering (floor/ceiling, walls, sprites)
// Section 9: HUD
// Section 10: Game State (menu, playing, etc.)
// Section 11: Main Loop
```

Let me write the complete implementation. I'll target about 700-900 lines.

Here's the texture generation. I need to be really careful to make procedural textures that look good and are distinguishable from each other.

For the wood texture, I want horizontal grain patterns. The key is making the grain look natural. I'll use sine waves with varying frequencies:

```javascript
// Wood texture
for (let y = 0; y < TEX; y++) {
    for (let x = 0; x < TEX; x++) {
        // Grain follows horizontal lines with some waviness
        const grain = Math.sin(x * 0.3 + y * 0.1) * 0.1;
        const plank = Math.floor(y / 16); // which plank we're on
        const plankOffset = plank * 7.3; // prime number for variation
        const base = 120 + Math.sin(plankOffset) * 20;
        const detail = Math.sin(x * 0.1 + y * 0.5) * 10;
        const val = base + detail + grain * 15;
        // Set pixel
    }
}
```

Actually, this per-pixel approach is slow for texture generation but it only happens once, so it's fine.

But using canvas draw operations for textures is faster and cleaner. Let me use a mix: canvas operations for the broad strokes and ImageData for fine noise.

OK I NEED to just start writing. Let me go. No more consideration.

Let me write the complete file. I'll start typing and keep going until it's complete.

Actually, one more thing. For the sprite rendering to work properly with the z-buffer, I need to make sure I only draw sprite columns where the sprite is actually behind the wall. The condition is `zbuf[col] > spriteDepth` (where transformY is the depth). Actually wait, `zbuf[col]` stores the wall perpendicular distance for column col. The sprite's perpendicular distance is `transformY`. I should draw the sprite column only if `zbuf[col] >= spriteDepth` (sprite is at least as close as the wall).

But actually, since walls are drawn first and then sprites are drawn on top:
- For each sprite column, check if the sprite at that column is closer than the wall (transformY < zbuf[col])
- If so, draw the sprite pixels

Wait, closer means SMALLER distance. So: draw sprite if `transformY < zbuf[col]`.

And we need to make sure we handle the edge case where only part of the sprite is visible:
- Clip the sprite to the screen bounds
- For each column, only draw if transformY < zbuf[col]

OK, let me finally write the code. Here it goes:

For efficiency, I'll process sprites like this:
```javascript
for (let x = startX; x < endX; x++) {
    if (x < 0 || x >= W) continue;
    if (transformY >= zbuf[x]) continue; // wall is closer
    // Draw this column of the sprite
    const texX = ((x - (startX)) / (endX - startX) * TEX) | 0;
    // For each pixel in the column...
}
```

But iterating pixel by pixel inside the column loop is slow. Let me optimize by precomputing column offsets into the sprite data.

Actually, for 8 enemies at reasonable sizes, the pixel iteration is fine. At worst, 8 * 64 * 100 = 51,200 pixel operations per frame. That's nothing for a modern browser.

Alright, WRITING. THE. CODE. NOW.

Let me finalize the enemy rendering. I want enemies to cast shadows on the floor (a simple dark ellipse at their feet) for visual grounding. This is easy: draw a dark ellipse at the enemy's position on the floor, before drawing the enemy sprite itself.

Actually, floor rendering is just a flat color in my approach, so I can't easily add shadows to the floor within the ImageData framework. I'll skip shadows and just focus on getting the sprites right.

Alternative: I could draw the shadow as a small dark ellipse on the screen at the enemy's projected foot position. But this would require screen-space calculations and would need to be done with canvas 2D on top. Let me skip it for simplicity.

OK, the code. Now. For real. Let me make the structure clear:

```javascript
// =========================================
// RAYCASTER MAZE SHOOTER
// =========================================

// --- CONFIGURATION ---
const W = 960, H = 600, TEX = 64;
const MW = 32, MH = 32;
const FOV_SCALE = 0.66;
const MOVE_SPEED = 3.0;
const ROT_SPEED = 2.0;
const MOUSE_SENSE = 0.002;
const SHOOT_CD = 0.3;
const ENEMY_COUNT = 8;

// --- CANVAS SETUP ---
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = W;
canvas.height = H;

// --- RENDERING BUFFERS ---
const imgData = ctx.createImageData(W, H);
const pix = imgData.data;
const zbuf = new Float32Array(W);

// --- GAME STATE ---
let state = 'menu'; // menu, playing, dead, cleared
let px, py, pa; // player position and angle
let pdx, pdy, planeX, planeY; // direction and camera plane
let pHealth, pAmmo, pScore;
let shootTimer, damageFlash, weaponKick;
let gameTime;
let map, texTypes;
let enemies, items;
let keys = {};
let locked = false;

// --- TEXTURES ---
let wallTex = [];
let spriteTex = [];

// --- AUDIO ---
let actx;

// Utility: seeded random for reproducibility per level
let seed;
function rng() {
    seed = (seed * 16807 + 0) % 2147483647;
    return (seed - 1) / 2147483646;
}

// --- INIT AUDIO ---
function initAudio() {
    try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
    catch(e) {}
}

// --- MAKE SOUND ---
function sound(type) {
    if (!actx) return;
    // ... various sound effects using BufferSource
}

// --- TEXTURE GEN ---
function genTextures() {
    wallTex = [];
    // 4 wall textures
    for (let t = 0; t < 4; t++) {
        const c = document.createElement('canvas');
        c.width = TEX; c.height = TEX;
        const x = c.getContext('2d');
        // ... generate texture based on t
        wallTex.push(c.getImageData(0, 0, TEX, TEX));
    }
}

// --- MAZE ---
function genMaze() {
    map = new Uint8Array(MW * MH).fill(1);
    texTypes = new Uint8Array(MW * MH);
    
    // Recursive backtracking
    function carve(cx, cy) {
        map[cy * MW + cx] = 0;
        // Assign random texture type to adjacent walls
        // ...
        const dirs = [[0,-2],[0,2],[-2,0],[2,0]];
        // shuffle
        for (let i = dirs.length - 1; i > 0; i--) {
            const j = Math.floor(rng() * (i + 1));
            [dirs[i], dirs[j]] = [dirs[j], dirs[i]];
        }
        for (const [dx, dy] of dirs) {
            const nx = cx + dx, ny = cy + dy;
            if (nx > 0 && nx < MW-1 && ny > 0 && ny < MH-1 && map[ny*MW+nx] === 1) {
                map[(cy+dy/2)*MW + (cx+dx/2)] = 0;
                carve(nx, ny);
            }
        }
    }
    
    // Start from (1,1)
    carve(1, 1);
    
    // Ensure player area is clear
    map[1*MW+1] = 0;
    map[1*MW+2] = 0;
    map[2*MW+1] = 0;
    map[2*MW+2] = 0;
    
    // Assign wall texture types
    for (let y = 0; y < MH; y++) {
        for (let x = 0; x < MW; x++) {
            if (map[y*MW+x] > 0) {
                texTypes[y*MW+x] = Math.floor(rng() * 4);
            }
        }
    }
    
    // Place enemies and items
    placeEntities();
}

function placeEntities() {
    enemies = [];
    items = [];
    
    for (let i = 0; i < ENEMY_COUNT; i++) {
        let ex, ey;
        let tries = 0;
        do {
            ex = Math.floor(rng() * (MW - 4)) + 2;
            ey = Math.floor(rng() * (MH - 4)) + 2;
            tries++;
        } while ((map[ey*MW+ex] !== 0 || Math.hypot(ex-px, ey-py) < 8) && tries < 100);
        
        if (tries < 100) {
            enemies.push({
                x: ex + 0.5, y: ey + 0.5,
                health: 30 + Math.floor(rng() * 20),
                maxHealth: 50,
                state: 'idle',
                alertRange: 8 + rng() * 4,
                attackRange: 2.5,
                shootTimer: 1 + rng() * 2,
                animTimer: rng() * 100,
                alertTimer: 0,
                hitFlash: 0,
                type: rng() > 0.5 ? 0 : 1
            });
        }
    }
}

// --- INPUT ---
document.addEventListener('keydown', e => { keys[e.code] = true; if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault(); });
document.addEventListener('keyup', e => { keys[e.code] = false; });
canvas.addEventListener('click', () => { canvas.requestPointerLock(); });
document.addEventListener('pointerlockchange', () => { locked = document.pointerLockElement === canvas; });
document.addEventListener('mousemove', e => { if (locked) pa += e.movementX * MOUSE_SENSE; });

// --- UPDATE ---
function update(dt) {
    if (state !== 'playing') return;
    
    gameTime += dt;
    if (shootTimer > 0) shootTimer -= dt;
    if (damageFlash > 0) damageFlash -= dt;
    if (weaponKick > 0) weaponKick -= dt;
    
    // Update player angle
    if (keys['ArrowLeft']) pa -= ROT_SPEED * dt;
    if (keys['ArrowRight']) pa += ROT_SPEED * dt;
    
    // Calculate movement direction
    let mx = 0, my = 0;
    let forwardX = Math.cos(pa) * MOVE_SPEED * dt;
    let forwardY = Math.sin(pa) * MOVE_SPEED * dt;
    let strafeX = Math.cos(pa + Math.PI/2) * MOVE_SPEED * dt;
    let strafeY = Math.sin(pa + Math.PI/2) * MOVE_SPEED * dt;
    
    if (keys['KeyW'] || keys['ArrowUp']) { mx += forwardX; my += forwardY; }
    if (keys['KeyS'] || keys['ArrowDown']) { mx -= forwardX; my -= forwardY; }
    if (keys['KeyA']) { mx -= strafeX; my -= strafeY; }
    if (keys['KeyD']) { mx += strafeX; my += strafeY; }
    
    // Collision detection
    const pad = 0.2;
    if (mx !== 0) {
        const nx = px + mx;
        if (isWalkable(nx + pad * Math.sign(mx), py) && isWalkable(nx + pad * Math.sign(mx), py + pad) && isWalkable(nx + pad * Math.sign(mx), py - pad)) {
            px = nx;
        }
    }
    if (my !== 0) {
        const ny = py + my;
        if (isWalkable(px, ny + pad * Math.sign(my)) && isWalkable(px + pad, ny + pad * Math.sign(my)) && isWalkable(px - pad, ny + pad * Math.sign(my))) {
            py = ny;
        }
    }
    
    // Update direction vectors
    pdx = Math.cos(pa);
    pdy = Math.sin(pa);
    planeX = -Math.sin(pa) * FOV_SCALE;
    planeY = Math.cos(pa) * FOV_SCALE;
    
    // Player shooting
    if ((keys['MouseLeft'] || keys['Space']) && shootTimer <= 0 && pAmmo > 0) {
        playerShoot();
    }
    
    // Update enemies
    for (let e of enemies) {
        if (e.health <= 0) continue;
        
        e.animTimer += dt;
        e.hitFlash = Math.max(0, e.hitFlash - dt);
        
        const dx = px - e.x;
        const dy = py - e.y;
        const dist = Math.sqrt(dx*dx + dy*dy);
        
        // Check LOS
        const los = hasLOS(e.x, e.y, px, py);
        
        switch(e.state) {
            case 'idle':
                if (dist < e.alertRange && los) {
                    e.state = 'alert';
                    e.alertTimer = 0.5;
                    sound('alert');
                }
                break;
            case 'alert':
                e.alertTimer -= dt;
                if (e.alertTimer <= 0) e.state = 'chase';
                break;
            case 'chase':
                if (dist > 1.5) {
                    e.x += (dx/dist) * 1.5 * dt;
                    e.y += (dy/dist) * 1.5 * dt;
                    // Don't walk through walls
                    if (map[Math.floor(e.y)*MW + Math.floor(e.x)] > 0) {
                        e.x -= (dx/dist) * 1.5 * dt;
                        e.y -= (dy/dist) * 1.5 * dt;
                    }
                }
                if (dist < e.attackRange && los) {
                    e.state = 'attack';
                }
                if (dist > e.alertRange * 1.5) {
                    e.state = 'idle';
                }
                break;
            case 'attack':
                e.shootTimer -= dt;
                if (e.shootTimer <= 0) {
                    e.shootTimer = 1.5 + rng();
                    e.canSee = los;
                    if (los) {
                        // Shoot at player
                        const edmg = 8 + Math.floor(rng() * 8);
                        pHealth -= edmg;
                        damageFlash = 0.2;
                        sound('hurt');
                        if (pHealth <= 0) {
                            pHealth = 0;
                            state = 'dead';
                            sound('playerDie'); // need to add this
                        }
                    }
                }
                if (dist > e.attackRange * 1.5 || !los) {
                    e.state = 'chase';
                }
                break;
        }
    }
    
    // Check item pickup
    for (let i = items.length - 1; i >= 0; i--) {
        const it = items[i];
        if (Math.hypot(it.x - px, it.y - py) < 0.7) {
            if (it.type === 'health') {
                pHealth = Math.min(100, pHealth + 25);
                sound('pickup');
            } else if (it.type === 'ammo') {
                pAmmo += 10;
                sound('pickup');
            }
            items.splice(i, 1);
        }
    }
    
    // Check win condition
    if (numEnemiesKilled >= ENEMY_COUNT && enemies.every(e => e.health <= 0)) {
        state = 'cleared';
        sound('levelComplete');
    }
}

function isWalkable(x, y) {
    const ix = Math.floor(x);
    const iy = Math.floor(y);
    if (ix < 0 || ix >= MW || iy < 0 || iy >= MH) return false;
    return map[iy * MW + ix] === 0;
}

function hasLOS(x1, y1, x2, y2) {
    const dx = x2 - x1, dy = y2 - y1;
    const dist = Math.sqrt(dx*dx + dy*dy);
    const steps = Math.ceil(dist);
    for (let i = 1; i < steps; i++) {
        const t = i / steps;
        if (map[Math.floor(y1 + dy*t) * MW + Math.floor(x1 + dx*t)] > 0) return false;
    }
    return true;
}

function playerShoot() {
    pAmmo--;
    weaponKick = 0.15;
    shootTimer = SHOOT_CD;
    sound('shoot');
    
    // Check for hit
    // Cast a ray toward crosshair center and check enemies
    // Actually, simpler: check each enemy for proximity to crosshair
    const hitDist = 1000;
    let hitEnemy = null;
    
    for (let e of enemies) {
        if (e.health <= 0) continue;
        // Check if enemy is in front of player
        const edx = e.x - px, edy = e.y - py;
        const dot = edx * pdx + edy * pdy;
        if (dot <= 0) continue;
        
        // Check if enemy is near crosshair center
        // Transform to camera space
        const invDet = 1.0 / (planeX * pdy - pdx * planeY);
        const tx = invDet * (pdy * edx - pdx * edy);
        const ty = invDet * (-planeY * edx + planeX * edy);
        
        if (ty <= 0) continue;
        
        const sx = (W/2) * (1 + tx / ty);
        const spriteH = Math.abs(H / ty);
        const spriteW = spriteH;
        
        // Check if crosshair (center of screen) is within the sprite
        const left = sx - spriteW/2;
        const right = sx + spriteW/2;
        if (W/2 >= left && W/2 <= right && spriteH > 20) {
            // Enemy is roughly in crosshair
            if (hitEnemy === null || ty < hitDist) {
                hitDist = ty;
                hitEnemy = e;
            }
        }
    }
    
    // Also check wall hits for bullet impact effect
    
    if (hitEnemy) {
        const dmg = 15 + Math.floor(rng() * 15);
        hitEnemy.health -= dmg;
        hitEnemy.hitFlash = 0.1;
        sound('hit');
        
        if (hitEnemy.health <= 0) {
            hitEnemy.state = 'dead';
            pScore += 100;
            sound('enemyDie');
            
            // Drop items
            if (rng() > 0.5) {
                items.push({x: hitEnemy.x, y: hitEnemy.y, type: rng() > 0.5 ? 'health' : 'ammo'});
            }
        }
    }
}

// --- RENDER ---
function render() {
    // Clear with floor/ceiling
    for (let y = 0; y < H; y++) {
        let r, g, b;
        const t = (y < H/2) ? (H/2 - y) / (H/2) : (y - H/2) / (H/2);
        const shade = 1 - t * 0.5;
        if (y < H/2) {
            // Ceiling
            r = Math.floor(15 * shade);
            g = Math.floor(15 * shade);
            b = Math.floor(25 * shade);
        } else {
            // Floor
            r = Math.floor(35 * shade);
            g = Math.floor(30 * shade);
            b = Math.floor(20 * shade);
        }
        const pi = y * W * 4;
        for (let x = 0; x < W; x++) {
            pix[pi + x*4] = r;
            pix[pi + x*4 + 1] = g;
            pix[pi + x*4 + 2] = b;
            pix[pi + x*4 + 3] = 255;
        }
    }
    
    // Raycasting
    for (let x = 0; x < W; x++) {
        const camX = 2 * x / W - 1;
        const rayX = pdx + planeX * camX;
        const rayY = pdy + planeY * camX;
        
        let mapX = Math.floor(px);
        let mapY = Math.floor(py);
        
        const deltaDistX = Math.abs(1 / (rayX || 0.00001));
        const deltaDistY = Math.abs(1 / (rayY || 0.00001));
        
        let stepX, sideDistX;
        let stepY, sideDistY;
        
        if (rayX < 0) { stepX = -1; sideDistX = (px - mapX) * deltaDistX; }
        else { stepX = 1; sideDistX = (mapX + 1 - px) * deltaDistX; }
        if (rayY < 0) { stepY = -1; sideDistY = (py - mapY) * deltaDistY; }
        else { stepY = 1; sideDistY = (mapY + 1 - py) * deltaDistY; }
        
        let side = 0;
        while (true) {
            if (sideDistX < sideDistY) {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            } else {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }
            const cell = map[mapY * MW + mapX];
            if (cell > 0) break;
        }
        
        // Perpendicular distance
        const perpDist = side === 0 
            ? (mapX - px + (1 - stepX) / 2) / (rayX || 0.00001)
            : (mapY - py + (1 - stepY) / 2) / (rayY || 0.00001);
        
        zbuf[x] = perpDist;
        
        const lineH = Math.floor(H / perpDist);
        const drawStart = Math.max(0, Math.floor(-lineH / 2 + H / 2));
        const drawEnd = Math.min(H - 1, Math.floor(lineH / 2 + H / 2));
        
        // Texture coordinate
        let wallX;
        if (side === 0) wallX = py + perpDist * rayY;
        else wallX = px + perpDist * rayX;
        wallX -= Math.floor(wallX);
        let texX = Math.floor(wallX * TEX);
        if ((side === 0 && rayX > 0) || (side === 1 && rayY < 0)) texX = TEX - texX - 1;
        
        // Get texture
        const texIdx = texTypes[mapY * MW + mapX] - 1;
        const tex = wallTex[texIdx >= 0 ? texIdx : 0];
        
        // Brightness
        const brightness = side === 0 ? 1.0 : 0.65;
        const distFade = Math.min(1, 5.0 / (perpDist + 0.5));
        
        // Draw column
        for (let y = drawStart; y <= drawEnd; y++) {
            const texY = Math.floor(((y * 2 - H + lineH) / lineH) * TEX / 2) & (TEX - 1);
            const ti = (texY * TEX + texX) * 4;
            const pi = (y * W + x) * 4;
            pix[pi]     = tex.data[ti] * brightness * distFade;
            pix[pi + 1] = tex.data[ti+1] * brightness * distFade;
            pix[pi + 2] = tex.data[ti+2] * brightness * distFade;
            pix[pi + 3] = 255;
        }
    }
    
    // Put rendered image
    ctx.putImageData(imgData, 0, 0);
    
    // Sprites
    renderSprites();
    
    // HUD
    drawHUD();
}

// ... etc
```

OK OK OK. I'm going to write the entire file in one shot. Let me do it. I'll be careful with all the indices and math.

Let me think about one more thing: I want to make sure the texTypes array is populated correctly. Each cell in the map has a texture type: 0 for empty, 1-4 for the four wall textures.

In the maze generation, after carving, I'll iterate over the map and assign texture types to wall cells.

```javascript
for (let i = 0; i < MW * MH; i++) {
    if (map[i] > 0) {
        map[i] = Math.floor(rng() * 4) + 1; // 1 to 4
    }
}
```

Then when raycasting, I use `map[mapY * MW + mapX] - 1` as the texture index (0-3).

Perfect. Let me write the complete file now.

For enemy sprites, I need to pre-render them. Let me create two enemy types:

Type 0 (Soldier - blue): 
- Head: skin tone circle
- Body: blue rectangle
- Legs: dark blue rectangles
- Gun: gray

Type 1 (Mutant - green):
- Head: greenish
- Body: dark green
- Legs: brown
- Gun: dark gray

Each enemy has 3 animation frames (stand, walk1, walk2), and 4 death frames.

For pre-rendering, I'll create offscreen canvases:

```javascript
function genEnemySprite(type, frame) {
    const c = document.createElement('canvas');
    c.width = TEX; c.height = TEX;
    const cx = c.getContext('2d');
    cx.imageSmoothingEnabled = false;
    
    // Background transparent
    cx.clearRect(0, 0, TEX, TEX);
    
    let bodyColor, headColor, legColor, gunColor;
    if (type === 0) {
        bodyColor = '#3366CC';
        headColor = '#D4A574';
        legColor = '#333';
        gunColor = '#888';
    } else {
        bodyColor = '#228B22';
        headColor = '#A0A050';
        legColor = '#555';
        gunColor = '#666';
    }
    
    if (frame === 'death') {
        // Draw enemy falling over
        // Flattened body
        cx.fillStyle = bodyColor;
        cx.fillRect(10, 40, 44, 12);
        cx.fillStyle = headColor;
        cx.beginPath();
        cx.arc(32, 44, 10, 0, Math.PI * 2);
        cx.fill();
    } else {
        // Legs
        const legOffset = frame === 1 ? 3 : -3; // Walking animation
        cx.fillStyle = legColor;
        cx.fillRect(22, 40 + legOffset, 8, 22 - legOffset);
        cx.fillRect(34, 40 - legOffset, 8, 22 + legOffset);
        
        // Body
        cx.fillStyle = bodyColor;
        cx.fillRect(22, 22, 20, 22);
        
        // Arms
        cx.fillStyle = bodyColor;
        cx.fillRect(14, 24, 10, 16);
        cx.fillRect(38, 24, 10, 16);
        
        // Gun (in right hand)
        cx.fillStyle = gunColor;
        cx.fillRect(38, 28, 12, 4); // Barrel
        cx.fillRect(36, 26, 4, 12); // Body
        
        // Head
        cx.fillStyle = headColor;
        cx.beginPath();
        cx.arc(32, 18, 10, 0, Math.PI * 2);
        cx.fill();
    }
    
    return cx.getImageData(0, 0, TEX, TEX);
}
```

Hmm, for walking animation, frame 1 and 2 should show different leg positions. Frame 0 (stand) should have legs together.

Let me also add a death animation sequence.

For the enemy, I'll track:
- current frame (0=stand, 1=walk1, 2=walk2, death frames)
- animation timer

OK, I think I have enough detail planned. Let me write the complete file. I'll be thorough and make sure everything works.

Let me think about one edge case: when the player dies, I need to show a game over screen. When all enemies are killed, I need to show a victory screen with the option to restart.

For the menu screen: title with instructions to click to start.

Let me finalize the game flow:
1. MENU state: show title, "Click to start" text
2. On click: init game, enter PLAYING state
3. PLAYING state: main gameplay
4. If player dies → DEAD state, "You died" screen
5. If all enemies killed → CLEARED state, "Level cleared!" with score
6. In DEAD/CLEARED: press key/click to restart

For restart, regenerate everything: new maze, new enemies, reset player stats.

OK, WRITING THE CODE NOW. ABSOLUTELY FINAL VERSION. I'm going to type the complete file content.

Actually, wait - for the enemy sprite animation, I should also have the enemy face the player (billboard). Since the sprites are pre-rendered from one direction, they'll look like they're always facing the camera if we render them as sprites. That's exactly what we want.

For the visual quality, I should also add:
- Floor/ceiling gradient instead of flat colors
- Maybe add some darker vignette effect
- Smooth screen rendering

For the minimap:
- Semi-transparent dark background
- White dots for walls
- Green dot for player (rotating arrow showing direction)
- Red dots for enemies
- Yellow dots for items
- Scale: each tile = 4-5 pixels

Let me finalize the minimap size. MW=32, MH=32. With 5px per tile: 160x160 pixels. Place it in the top-right corner or top-left corner. Let me do top-left with a slight offset.

For the HUD:
- Health bar on the left
- Ammo count on the right  
- Score at the top center
- Crosshair in the center
- Minimap in corner
- Level indicator

Let me also add a floor shadow under the weapon for visual grounding.

OK, I truly will write it now. One final check: I should make sure the textures are generated before any rendering happens. I'll put texture generation in the init function along with audio context initialization.

BEGIN WRITING THE FILE:

Building a complete Wolfenstein 3D-style raycaster shooter — this is a substantial project. Let me write the entire thing.

write /tmp/ws/index.html (48314 bytes)
toolResult

Successfully wrote 48314 bytes to /tmp/ws/index.html

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modelring-2.6-1t
vendorinclusionai
harnesspi-agent
generated2026-08-01

EVALUATION

1/5 broken

Completely black screen throughout; JS error 'seed is not defined' likely crashed the renderer entirely.

off-prompt

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.0%
on click0.0%
on drag0.0%
on wheel0.0%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread0.3 / 255
console errors0
js errors
  • seed is not defined

METRICS

runtime600.0s
tokens in1.6k
tokens out63k
cached0
cost / run$0.0397