Wolfenstein raycaster · step-3.7-flash
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.
This one-shot run produced no runnable artifact.
agent produced no index.html
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 2 messages
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.
thinking
The user wants a first-person raycasting maze shooter in the style of Wolfenstein 3D, implemented as a single self-contained HTML file with inline CSS and JavaScript. No external assets, no CDNs, no network requests. Procedural textures only.
Requirements:
- First-person raycasting maze shooter
- WASD movement
- Arrow keys or mouse turning
- Textured grid maze
- Enemies you can shoot
- Procedural textures (generated via canvas)
- Single page, all inline
I need to build a complete game. Let me think about the components:
1. **Map**: A 2D grid array representing walls and empty space.
2. **Player**: Position (x, y), direction angle, plane vector for raycasting.
3. **Raycasting engine**: Cast rays for each column of the screen, calculate wall distances, draw textured walls with proper projection and fisheye correction.
4. **Textures**: Generate procedural textures using canvas (brick wall, etc.)
5. **Sprites/Enemies**: Enemies as billboarded sprites in the map, drawn after walls with depth sorting.
6. **Shooting**: A gun mechanism that fires at enemies in the center or crosshair, reducing their health.
7. **HUD**: Health, ammo, score, crosshair.
8. **Controls**: WASD for movement, arrow keys/mouse for turning.
Implementation details:
**Raycasting:**
- Use DDA algorithm for wall detection.
- For each x-pixel column, calculate ray direction.
- Step through grid until wall hit.
- Calculate distance perpendicular to camera plane to avoid fisheye.
- Calculate wall hit position for texture x-coordinate.
- Draw vertical strip with texture mapping.
**Textures:**
- Create offscreen canvas for brick texture: draw rectangles, mortar lines, some color variation.
- Create a few textures or just one for walls.
**Enemies:**
- Store enemy objects with position (x,y), state (alive/dead/attack), health, type.
- After rendering walls, render sprites: transform sprite position to camera space, check if in front, project to screen coordinates.
- Sort sprites by distance and draw.
**Shooting:**
- On click or space, fire.
- Check if an enemy is within a small angle of the center and within a fixed distance, reduce health.
**Minimap:**
- Optional but helpful. Maybe small corner map.
Given the complexity, the code will be substantial but fit in one file.
I should make the game loop, input handling, rendering, etc.
Let me design the map. Something like a 16x16 or 24x24 grid with corridors and rooms.
Player starts at (1.5, 1.5) or similar, facing east.
Let me write the code structure:
```html
<!DOCTYPE html>
<html>
<head>
<title>Raycaster Maze</title>
<style>
body { margin:0; overflow:hidden; background:#000; }
canvas { display:block; }
#ui { position:absolute; top:10px; left:10px; color:#fff; font-family:monospace; font-size:18px; pointer-events:none; }
#hud { position:absolute; bottom:0; width:100%; height:60px; background:rgba(0,0,0,0.7); display:flex; align-items:center; justify-content:center; color:white; font-family:monospace; font-size:20px; gap:40px; }
#crosshair { position:absolute; top:50%; left:50%; width:20px; height:20px; border:2px solid rgba(255,255,255,0.8); border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui"></div>
<div id="hud">
<span>Health: <span id="hp">100</span></span>
<span>Ammo: <span id="ammo">50</span></span>
<span>Score: <span id="score">0</span></span>
</div>
<div id="crosshair"></div>
<script>
// setup canvas
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W, H;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// Input
const keys = {};
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);
let mouseLook = { x: 0, dx: 0 };
canvas.addEventListener('mousemove', e => {
mouseLook.dx = e.movementX || e.screenX - (canvas.width/2);
// relative not always available? better use pointer lock.
});
// Actually for smoother mouse look, request pointer lock.
canvas.addEventListener('click', () => canvas.requestPointerLock());
document.addEventListener('pointerlockchange', () => { ... });
// But simpler: just use pointer lock and movementX.
```
Actually, implementing full mouse look with pointer lock is the hallmark of these shooters. Let's do that.
But if pointer lock isn't active, maybe fallback to arrow keys turning.
Map: I'll use a simple 2D array, 1=wall, 0=empty, 2=enemy spawn or something. But better to store enemies separately.
Enemies: array of {x,y,hp,alive}
Textures: generate using offscreen canvas. I'll make a few:
- Wall1: brick
- Wall2: stone
- Enemy: a simple procedural sprite like a skull or guard. But let's make a distinctive top-down sprite? Actually billboarded sprite.
But generating complex sprites procedurally is tough. I can make a simple "enemy" sprite: a circle with eyes, maybe pulsing red. Or draw a rudimentary humanoid. Given the constraints, a simple sprite resembling a guard or monster drawn on an offscreen canvas with some colors.
I'll generate a few textures:
1. Wall texture: 64x64 brick pattern
2. Enemy texture: 64x64 character sprite
3. Floor/Ceiling: gradient colors or tiled checkerboard.
Actually for ceiling and floor, I can just draw flat colors without textures for simplicity, or use a simple grid pattern.
Let's plan the raycasting math:
- dirX = cos(angle), dirY = sin(angle)
- planeX = -sin(angle) * fov_factor, planeY = cos(angle) * fov_factor? Actually standard: camera plane is dir rotated 90 degrees scaled by view width. So for a 45° FOV, plane length is tan(fov/2) or use 0.66 for 66°.
- For each x screen column (0 to W):
- cameraX = 2*x/W - 1
- rayDirX = dirX + planeX * cameraX
- rayDirY = dirY + planeY * cameraX
- mapX = floor(player.x), mapY = floor(player.y)
- deltaDistX = abs(1/rayDirX), deltaDistY = abs(1/rayDirY)
- stepX, stepY, sideDistX, sideDistY
- DDA loop until hit
- perpWallDist = (sideDistX - deltaDistX) if north/south else (sideDistY - deltaDistY)
- lineHeight = floor(H / perpWallDist)
- drawStart = max(-lineHeight/2 + H/2, 0)
- drawEnd = min(lineHeight/2 + H/2, H-1)
- texture calculation based on hit position
- draw vertical texture slice
For sprites:
- Transform sprite position relative to player.
- Calculate inverse camera matrix or transform to camera space:
- spriteX = sprite.x - player.x
- spriteY = sprite.y - player.y
- invDet = 1/(planeX*dirY - dirX*planeY)
- transformX = invDet*(dirY*spriteX - dirX*spriteY)
- transformY = invDet*(-planeY*spriteX + planeX*spriteY)
- spriteScreenX = floor((W/2)*(1+transformX/transformY))
- spriteHeight = abs(floor(H/transformY))
- drawStartY = max(-spriteHeight/2 + H/2, 0)
- drawEndY = min(spriteHeight/2 + H/2, H-1)
- similarly for width
- iterate over vertical stripe and draw.
Shooting:
- Find enemy with smallest angle to center and within some FOV.
- If transformY positive (in front) and distance < range, reduce HP.
Now, write the code.
I'll generate textures with canvas:
- `generateBrickTexture()`: 128x128 with brick layout, random shades.
- `generateEnemyTexture()`: 64x64, a simple guard face, maybe green, with red eyes.
For the floor, I can draw a simple grid: two colors alternating squares.
Ceiling: dark gray.
Let's design the map. I'll make a 16x16 grid with some walls, an courtyard, maybe a room with enemies.
Map:
```
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,
1,0,1,0,1,0,1,1,1,1,0,1,1,1,0,1,
1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,
1,0,1,1,1,1,1,1,0,1,1,1,0,1,0,1,
1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,
1,1,1,0,1,1,0,1,1,1,1,1,0,1,0,1,
1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,1,
1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,
1,0,1,1,1,0,1,0,1,1,1,0,1,1,0,1,
1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,
1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,1,
1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,
1,0,1,1,1,1,1,0,1,0,1,1,1,1,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
```
Enemies at several positions:
{ x: 3.5, y: 3.5 }, { x: 7.5, y: 5.5 }, etc.
Let's define walls texture as 1, enemy spawns just added to array.
Now the actual code. I'll keep it reasonably sized.
I should generate textures before the game loop.
Let's code carefully.
One byproduct: need to handle texture x properly when ray hits vertical vs horizontal wall. Standard approach: use hit side to determine texture x coordinate. Actually for a wall with continuous texture, both sides can have x based on hit fraction. But usually if using same texture both sides, it's fine.
I'll use `textureX` derived from hit position:
- If side == 0 (NS wall): hit position is `player.y + perpWallDist * rayDirY`, textureX = hit position - floor(hit position). But need to flip if dirX < 0? Let's use standard logic:
- if side == 0: wallX = playerY + perpDist * rayDirY
- else: wallX = playerX + perpDist * rayDirX
- wallX -= floor(wallX)
- texX = floor(wallX * texWidth)
- if side == 1 and rayDirY > 0 or side == 0 and rayDirX < 0: texX = texWidth - texX - 1;
This handles mirrored textures for shadow effect by darkening side==1.
Then drawing:
- `texture[wall[texX]]` get color.
I need to pre-generate texture data as ImageData.
For performance, since it's JS canvas, maybe use 256x240 or similar. Let's make game resolution 320x200 scaled up? Actually drawing to canvas at full resolution with getImageData/putImageData is slow. Better to just draw to canvas at window size, using `fillRect` or better `drawImage` from offscreen canvas textures.
Simpler approach: create offscreen canvases for textures, then during raycasting `ctx.drawImage(texture, sx, 0, sw, sh, screenX, drawStart, 1, drawEnd-drawStart)`. But drawing 800 drawImage calls per frame might be okay on modern devices with 2D canvas. Alternatively, use `ImageData` for a smaller internal resolution and draw to a small canvas scaled up. That's more authentic and faster.
Let's render at, say, 320x200 internal, then draw scaled to window size with `ctx.drawImage(gameCanvas, 0, 0, gameCanvas.width, gameCanvas.height, 0, 0, W, H)`. This gives that retro look and is fast.
Yes, I'll do that.
Game canvas: `const SCREEN_WIDTH = 320; const SCREEN_HEIGHT = 200;`. Create `const screenCanvas = document.createElement('canvas'); screenCanvas.width=SCREEN_WIDTH; screenCanvas.height=SCREEN_HEIGHT; const sctx = screenCanvas.getContext('2d');`.
Then render to sctx, and finally `ctx.drawImage(screenCanvas, 0, 0, W, H)`.
Textures: 64x64. Walls are textured pixels. Ceiling/floor: just colored.
For sprites, render them to the screen canvas with `drawImage` or manual pixel? Since sprites are small, `drawImage` is fine.
FPS: We'll use `requestAnimationFrame`, delta time for movement.
Movement: forward/backward modifies x/y based on cos/sin. Strafe uses perpendicular.
Collision: Check map grid at new position.
Shooting animation: a gun flash overlay or just a muzzle flash near bottom center.
Let's write the code step by step in my mind.
First, texture generation:
```javascript
function generateBrickTexture() {
const c = document.createElement('canvas');
c.width = 64; c.height = 64;
const g = c.getContext('2d');
const brickW = 32, brickH = 16;
// mortar color
g.fillStyle = '#444';
g.fillRect(0,0,64,64);
for(let row=0; row<64/brickH; row++) {
const offset = (row%2===0) ? 0 : brickW/2;
for(let col=-1; col<64/brickW+1; col++) {
const x = col*brickW + offset + 1;
const y = row*brickH + 1;
const shade = 120 + Math.random()*40;
g.fillStyle = `rgb(${shade}, ${shade*0.7}, ${shade*0.5})`;
g.fillRect(x, y, brickW-2, brickH-2);
}
}
return c;
}
```
Enemy sprite: Make it look like a guard. I'll draw a green uniform, red eyes. Keep it simple.
```javascript
function generateEnemyTexture() {
const c = document.createElement('canvas');
c.width = 64; c.height = 128; // taller
const g = c.getContext('2d');
// head
g.fillStyle = '#3a3'; // green helmet
g.fillRect(10,0,44,30);
// face
g.fillStyle = '#aaa'; // skin
g.fillRect(14,8,36,22);
// eyes
g.fillStyle = '#f00';
g.fillRect(18,12,8,8);
g.fillRect(38,12,8,8);
// body
g.fillStyle = '#262';
g.fillRect(12,30,40,50);
// arms
g.fillRect(0,34,12,24);
g.fillRect(52,34,12,24);
// legs
g.fillStyle = '#1a1';
g.fillRect(14,80,16,48);
g.fillRect(34,80,16,48);
return c;
}
```
Maybe generate multiple enemy sprites with varies? For now one is fine.
Now the game state:
```javascript
const map = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
... // 16x16
];
const world = {
player: { x: 1.5, y: 1.5, angle: Math.PI/4, hp: 100, ammo: 50, score: 0 },
enemies: [
{x:3.5, y:3.5, hp:30, alive:true},
// ...
],
keys: {},
...
};
```
Running the game loop:
```javascript
function update(dt) {
// handle input
const speed = 3.0 * dt; // units per second
const rotSpeed = 2.0 * dt;
// turning
if (keys['ArrowLeft']) player.angle -= rotSpeed;
if (keys['ArrowRight']) player.angle += rotSpeed;
// mouse turn if pointer lock
if (document.pointerLockElement === canvas) {
player.angle += mouseLook.dx * 0.002;
mouseLook.dx = 0; // reset after
}
// movement vectors
const dx = Math.cos(player.angle) * speed;
const dy = Math.sin(player.angle) * speed;
const perpX = Math.cos(angle + Math.PI/2)*speed;
const perpY = Math.sin(angle + Math.PI/2)*speed;
// forward/backward
let moveX=0, moveY=0;
if (keys['KeyW']) { moveX += dx; moveY += dy; }
if (keys['KeyS']) { moveX -= dx; moveY -= dy; }
if (keys['KeyA']) { moveX -= perpX; moveY -= perpY; }
if (keys['KeyD']) { moveX += perpX; moveY += perpY; }
// collision detect with map
const newX = player.x + moveX;
const newY = player.y + moveY;
const margin = 0.2; // player radius
if (map[Math.floor(newY)][Math.floor(newX + (newX>player.x?margin:-margin))] === 0) player.x = newX;
if (map[Math.floor(newY + (newY>player.y?margin:-margin))][Math.floor(newX)] === 0) player.y = newY;
// shooting
if (keys['Space'] || keys['KeyF'] || keys['Mouse0']) {
if (!reloading) fire();
}
}
```
Fire function:
```javascript
function fire() {
if (world.player.ammo <= 0) return;
world.player.ammo--;
muzzleFlashTimer = 0.1; // show flash
// Find closest enemy in center ~1 rad FOV
let target = null, best = 100;
const fov = 0.8, maxDist = 10;
for (const e of world.enemies) {
if (!e.alive) continue;
const sx = e.x - player.x, sy = e.y - player.y;
const dist = Math.sqrt(sx*sx+sy*sy);
if (dist > maxDist) continue;
// angle difference
const a = Math.atan2(sy, sx);
let diff = a - player.angle;
while (diff > Math.PI) diff -= Math.PI*2;
while (diff < -Math.PI) diff += Math.PI*2;
if (Math.abs(diff) < fov/dist) { // actually just angular width? simpler: check if enemy within center half
// simple: use sprite transform? just check angle
const angle = Math.acos(sx/dist), // no, use atan2
if (Math.abs(diff) < best) { best = Math.abs(diff); target = e; }
}
}
if (target) {
target.hp -= 15;
if (target.hp <= 0) { target.alive = false; world.player.score += 100; }
// damage flash
}
}
```
Rendering:
First ceiling and floor. Using the small screen canvas.
Floor casting is more complex but we can just draw a gradient. Simple: top half ceiling gradient, bottom half floor gradient. No texture for floor/cool. Or I can do a simple checkered floor cast using vertical line scan. Let's do a simple vertical scan floor: for each y > H/2, compute ray direction for floor, convert to world coords, and draw pixel.
Simpler: just fill ceiling and floor solid colors or gradient. To make it look better without much code: a simple gradient from top to middle for ceiling, and middle to bottom for floor.
After walls, draw sprites.
Then draw HUD text directly on canvas: health, ammo, score.
Also draw gun: a simple pixelated gun at bottom.
Since rendering at 320x200, gun is just pixels or a small image.
Let's write the rendering pseudocode:
```javascript
function render() {
// clear
sctx.fillStyle = '#555'; sctx.fillRect(0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
// floor/ceiling: just horizontal lines
for(let y=0; y<SCREEN_HEIGHT/2; y++) {
const c = Math.floor(40 - y*(40/100));
sctx.fillStyle = `rgb(${c},${c},${c+10})`;
sctx.fillRect(0,y,SCREEN_WIDTH,Y);
}
for(let y=SCREEN_HEIGHT/2; y<SCREEN_HEIGHT; y++) {
const c = Math.floor(30 + (y-SCREEN_HEIGHT/2)*(50/100));
sctx.fillStyle = `rgb(${c},${c*0.7},${c*0.5})`;
sctx.fillRect(0,y,SCREEN_WIDTH,1);
}
// raycasting walls
for(let x=0; x<SCREEN_WIDTH; x++) {
// ... compute perpDist, side, etc.
// lineHeight = ceilingY - floorY
// draw textured column
}
// sprites
// sort by distance
// for each sprite, draw with clipping
// gun
if (muzzleFlashTimer > 0) {
sctx.fillStyle = '#ff0';
sctx.fillRect(140, 140+Math.random()*10, 40, 20);
}
// then draw gun sprite: simple rectangles.
// finally draw to main canvas with nearest neighbor scaling
ctx.drawImage(screenCanvas,0,0,SCREEN_WIDTH,SCREEN_HEIGHT,0,0,W,H);
}
```
For drawing textured walls, since there's no ImageData, we'll use `ctx.drawImage` or `putImageData`. Using `putImageData` for each column is also too slow. Better: use an offscreen canvas for the wall and draw a 1-pixel wide slice from it. But typical approach for low res is:
For each x:
create a 1x64 ImageData? No.
Use `textureImageData` array and put into a temporary ImageData, then `putImageData` with proper offset? That also scans per pixel.
Simpler: pre-create the texture as a canvas, then `sctx.drawImage(textureCanvas, texX, 0, 1, 64, x, drawStart, 1, drawEnd-drawStart)`. This is straightforward and should work fine in 2D canvas.
Yes!
```javascript
const texX = ...; // 0-63
const destX = x;
const destY = drawStart;
const destW = 1;
const destH = drawEnd - drawStart;
const srcX = texX;
const srcY = 0; // if using only top half? no, use 0 to textureHeight.
// But if the texture is 64 high, we need to scale to destH. drawImage handles that.
sctx.drawImage(wallTextureCanvas, srcX, 0, 1, 64, destX, destY, 1, destH);
```
If side == 1, darken: `sctx.globalAlpha = 0.6` or multiply with drawImage? Actually `drawImage` doesn't support tinting. Instead, I can overlay a black rectangle or set globalAlpha. So after drawing, `if (side) { sctx.globalAlpha = 0.4; sctx.fillStyle='#000'; sctx.fillRect(x, drawStart, 1, destH); sctx.globalAlpha=1; }`. But that's slow per column. Better: pre-render two versions of the texture or just always draw slightly dimmed? For performance, we can skip the darkening or apply a simple multiply using composition `sctx.globalCompositeOperation = 'multiply'`. That might be slow.
Simpler: use a darker fillRect over the column when side==1. Since there are only 320 columns, it's fine.
Actually a classic trick: draw `sctx.fillStyle = 'rgba(0,0,0,0.3)'; sctx.fillRect(x, drawStart, 1, destH);` if side==1.
I'll include that.
For sprites:
Pre-render enemy as a canvas image, then for each visible sprite:
- `drawStartX` and `drawEndX` based on width.
- Iterate over screen columns (sprite vertical stripes)
- For each stripe, fetch the corresponding texture column from enemy image and draw to screen.
The enemy image might have transparency. Use `sctx.globalCompositeOperation = 'source-over'`. Need to handle alpha if possible: using a separate offscreen canvas with transparent background.
Enemy sprite: `generateEnemyTexture()` returns canvas with transparent area around figure.
Let's make the sprite canvas 64x128 and draw figure leaving cleared background.
Now, for sprite rendering:
- `invDet`, `transformX`, `transformY`.
- `spriteScreenX = (SCREEN_WIDTH/2)*(1+transformX/transformY)`
- `spriteHeight = abs(floor(SCREEN_HEIGHT/transformY))` ; limit to SCN.
- `spriteWidth = same` (we can use 64px wide enemy, width/height proportionally scaled)
- `spriteWidth = abs(floor(SCREEN_HEIGHT/transformY))` ; actually if texture is 64x128, ratio 1:2, use `spriteWidth = spriteHeight/2`.
- `drawStartX = max(-spriteWidth/2 + spriteScreenX, 0)`
- `drawEndX = min(spriteWidth/2 + spriteScreenX, SCREEN_WIDTH-1)`
- `stripeStart` from drawStartX to drawEndX.
- For each stripe: `texX = floor(256*(stripe - spriteScreenX + spriteWidth/2)/spriteWidth) / 256 * enemyCanvas.width`
- if `texX < 0 || >= width` skip.
- If transformY>0 (in front), draw with `sctx.drawImage(enemyCanvas, Math.floor(texX),0,1,height, stripe, drawStartY, 1, spriteHeight)`
Yes.
Now HUD: Since scaling to fullscreen, we can also use DOM overlay for health/ammo or draw on canvas for retro. Overlay is fine.
But we should ensure no external assets, all procedural.
Now putting it all together.
One more thing: enemy AI? They could stay still and just be shooting targets. Maybe simple: if player is close and line of sight, enemy shoots? But user only said "enemies you can shoot". So static is okay. But maybe make them turn to look at player? Not necessary.
To make it game-like, if player shoots enemy and kills it, score increases. Enemies can also damage player when close? Maybe.
I'll add: If enemy alive and within 1 unit of player when player isn't looking? Actually, maybe just stand there. If player walks into them, damage. Or no. Let's simplify: Enemies are just static; you shoot them to score.
Actually for "shooter" feel, maybe enemies have simple attack: if alive and in front, every so often reduce player HP. But I'll keep it simple: static enemies.
Let's write the final code.
I'll carefully write the map from the above plan, ensure it's enclosed by walls on all sides (16x16).
I need to make sure textures and sprite are generated before starting the loop.
The code should be cleaned for syntax. Use strict mode.
Potential performance: 320x200 canvas with drawImage per column per frame is 320 draw calls for walls alone, plus sprites. Maybe okay but let's optimize slightly. I could pre-render the wall into an offscreen buffer, but it's fine.
Actually 320 * 2 (around) draw calls is fine.
Better: To reduce alpha fillRect overhead, I can use a global `shadowBlur`? Not good. Skip darkening side.
Alternatively, use CSS filter on canvas? No.
Let's just skip side darkening or use `sctx.globalAlpha = 0.5; sctx.fillRect(...)` only for half. It might be okay.
I will write the code in a single write operation.
Let's prepare the exact final content.
Wait, the requirement: "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."
So yes.
Potential pitfalls:
- Movement speed scaling: need to use fixed time step or delta time. I'll use `performance.now()` and compute dt.
- Mouse pointer lock must be requested and `movementX` used.
- For browsers without pointer lock, arrow keys is fallback.
Let's make the code.
I'll write it all in one go. Ensure grammar and syntax correct.
Also, I need to handle window onresize to update W and H.
Finally, at the end of code, append `resize(); window.addEventListener('resize', resize);`.
Now, I'll write the full file.
Plan for the texture:
- `const brickTex = generateBrickTexture();`
- `const enemyTex = generateEnemyTexture();`
Map size: 16x16. I'll define `const map = []` with rows as strings for brevity: `"1111111111111111"` etc.
Enemies:
```
const enemies = [
{x:3.5,y:3.5,hp:25},
{x:6.5,y:7.5,hp:25},
{x:8.5,y:2.5,hp:25},
{x:12.5,y:12.5,hp:25},
{x:1.5,y:10.5,hp:25},
];
```
Now gun visuals: draw a pixelated gun at bottom center. Maybe a simple rectangle with a barrel.
Gun drawing:
```javascript
sctx.fillStyle = '#333';
sctx.fillRect(145, 130, 30, 50); // body
sctx.fillStyle = '#555';
sctx.fillRect(149, 100, 22, 30); // barrel
```
Flash:
`if (flash > 0) { sctx.fillStyle=`rgba(255,255,0,${flash})`; sctx.fillRect(130,80,60,60); flash -= dt; }`
For shooting, just set flash = 0.15 and decrement.
Crosshair: we have a DOM element but for retro feel, we can also draw a crosshair on canvas. Let's draw a small cross in center of screen canvas and scale.
I'll implement all of this.
Now the code will be quite long, maybe ~500 lines.
I'll need to ensure pointer lock events:
```javascript
document.addEventListener('pointerlockchange', () => {
locked = document.pointerLockElement === canvas;
});
document.addEventListener('mousemove', (e) => {
if (locked) { player.angle += e.movementX * 0.002; }
});
```
But for mobile? No mouse, but okay.
Now implement fire:
```javascript
function tryFire() {
if (!world.player.ammo || world.fireCooldown > 0) return;
world.fireCooldown = 0.3;
world.player.ammo--;
world.muzzleFlash = 0.15;
// find target enemy in center crosshair
let target = null, min = Infinity;
for (const e of world.enemies) {
if (!e.alive) continue;
const dx = e.x - player.x, dy = e.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > 8) continue;
let a = Math.atan2(dy, dx);
let diff = a - player.angle;
while (diff > Math.PI) diff -= Math.PI*2;
while (diff < -Math.PI) diff += Math.PI*2;
if (Math.abs(diff) < 0.4 && dist < min) {
min = dist; // Actually need to check closest
// but we want the one closest to crosshair center? Let's minimize abs(diff)
// if dist not considered, use min abs diff.
if (Math.abs(diff) < 0.15) target = e; // just pick any if diff small
}
}
if (target) { target.hp -= 20; if (target.hp <= 0) { target.alive = false; world.player.score += 100; } }
}
```
Maybe better: just compute distance of sprite in screen space and check if stripe hits screen center.
Alternatively, after rendering sprites sorted, we have screenX. If an enemy is at center (x within a few pixels), we can hit it.
Simpler: For all enemies, transform to screen space (as in sprite render), if `spriteScreenX` within `[-2,2)` of `SCREEN_WIDTH/2` and `transformY>0` and close enough, hit.
I'll implement raycast-style weapon: if transformY positive and |spriteScreenX - SCREEN_WIDTH/2| < 2, hit. This is accurate.
So during render, we compute transform values for each enemy anyway. Could just store and use later. Or compute twice.
That's fine.
Now, let's produce the file.
I'll produce a well-structured code with comments.
Important: ensure all characters are ASCII.
Let's write.```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Raycasting Maze Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; user-select: none; } #game { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; } #hud { position: absolute; bottom: 20px; width: 100%; text-align: center; color: #0f0; font-size: 22px; text-shadow: 2px 2px #000; pointer-events: none; display: flex; justify-content: center; gap: 40px; } #hud span { background: rgba(0,0,0,0.4); padding: 6px 14px; border-radius: 6px; } #crosshair { position: absolute; top: 50%; left: 50%; width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.85); border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; mix-blend-mode: difference; } #start { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 24px; text-align: center; pointer-events: none; } </style> </head> <body> <canvas id="game"></canvas> <div id="hud"> <span>Health: <b id="hp">100</b></span> <span>Ammo: <b id="ammo">50</b></span> <span>Score: <b id="score">0</b></span> </div> <div id="crosshair"></div> <div id="start">CLICK TO PLAY<br><span style="font-size:14px; opacity:0.7">WASD move<br>Arrows/Mouse aim<br>Click/Space/ F to fire</span></div> <script> // ===== SETUP ===== const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); let W = 0, H = 0; function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } resize(); window.addEventListener('resize', resize); // low-res rendering target const SCREEN_WIDTH = 320; const SCREEN_HEIGHT = 200; const screenCanvas = document.createElement('canvas'); screenCanvas.width = SCREEN_WIDTH; screenCanvas.height = SCREEN_HEIGHT; const sctx = screenCanvas.getContext('2d'); // ===== MAP & STATE ===== const map = []; const mapDefs = [ "1111111111111111", "1000000000000001", "1011110111111101", "1010000000000101", "1011101110111101", "1000000000100001", "1011010000101101", "1000010000100001", "1011101110111101", "1000000000000101", "1011110111111101", "1010000100000101", "1001101010100101", "1010001011101101", "1000000000100001", "1111111111111111" ]; for (const row of mapDefs) { map.push(row.split('').map(Number)); } const world = { player: { x: 1.5, y: 1.5, angle: Math.PI/4, hp: 100, ammo: 50, score: 0 }, enemies: [ {x:3.5, y:3.5, hp:25, alive:true}, {x:7.5, y:2.5, hp:25, alive:true}, {x:6.5, y:6.5, hp:25, alive:true}, {x:12.5, y:3.5, hp:25, alive:true}, {x:13.5, y:13.5, hp:25, alive:true}, {x:3.5, y:12.5, hp:25, alive:true}, {x:14.5, y:8.5, hp:25, alive:true}, {x:8.5, y:9.5, hp:25, alive:true} ], fireCooldown: 0, muzzleFlash: 0 }; // ===== TEXTURES (procedural) ===== function generateBrickTexture() { const c = document.createElement('canvas'); c.width = 64; c.height = 64; const g = c.getContext('2d'); g.fillStyle = '#3a2a1a'; g.fillRect(0,0,64,64); const bw = 32, bh = 16; for (let r=0; r<64/bh; r++) { const off = (r%2===0) ? 0 : bw/2; for (let col=-1; col<64/bw+1; col++) { const x = Math.floor(col*bw + off + 1); const y = Math.floor(r*bh + 1); const shade = 110 + Math.random()*50; g.fillStyle = `rgb(${shade+20},${shade*0.7},${shade*0.5})`; g.fillRect(x, y, bw-2, bh-2); // cracks g.fillStyle = `rgba(0,0,0,${Math.random()*0.3})`; g.fillRect(x+Math.random()*10, y+Math.random()*5, 4, 1); } } return c; } function generateEnemyTexture() { const c = document.createElement('canvas'); c.width = 64; c.height = 128; const g = c.getContext('2d'); // helmet g.fillStyle = '#3a5'; g.fillRect(8,0,48,28); g.fillStyle = '#262'; g.fillRect(10,4,44,20); // face g.fillStyle = '#bba'; g.fillRect(14,10,36,20); // eyes g.fillStyle = '#f00'; g.fillRect(18,14,8,8); g.fillRect(38,14,8,8); // mouth g.fillStyle = '#522'; g.fillRect(26,24,12,4); // body uniform g.fillStyle = '#363'; g.fillRect(12,30,40,56); // belt g.fillStyle = '#221'; g.fillRect(12,60,40,6); // belt buckle g.fillStyle = '#aa0'; g.fillRect(28,60,8,6); // arms g.fillStyle = '#363'; g.fillRect(0,34,12,28); g.fillRect(52,34,12,28); // gloves g.fillStyle = '#422'; g.fillRect(0,58,12,8); g.fillRect(52,58,12,8); // legs g.fillStyle = '#252'; g.fillRect(14,86,18,42); g.fillRect(32,86,18,42); // boots g.fillStyle = '#311'; g.fillRect(12,116,20,12); g.fillRect(32,116,20,12); // shadow outline g.strokeStyle = '#000'; g.lineWidth = 2; g.strokeRect(14,10,36,20); return c; } const brickTex = generateBrickTexture(); const enemyTex = generateEnemyTexture(); // ===== INPUT ===== const keys = {}; window.addEventListener('keydown', e => { keys[e.code] = true; if (['Space','KeyF','ArrowLeft','ArrowRight','KeyW','KeyA','KeyS','KeyD'].includes(e.code)) e.preventDefault(); if (e.code === 'Space' || e.code === 'KeyF') tryFire(); }); window.addEventListener('keyup', e => keys[e.code] = false); let locked = false; document.addEventListener('pointerlockchange', () => { locked = document.pointerLockElement === canvas; }); document.addEventListener('mousemove', e => { if (locked) world.player.angle += e.movementX * 0.002; }); canvas.addEventListener('mousedown', () => { if (!locked) { canvas.requestPointerLock().catch(()=>{}); } else { tryFire(); } }); document.getElementById('start').style.display = 'block'; // ===== HELPERS ===== function tryFire() { if (world.fireCooldown > 0 || world.player.ammo <= 0) return; world.fireCooldown = 0.25; world.player.ammo--; world.muzzleFlash = 0.12; document.getElementById('start').style.display = 'none'; // check crosshair via projected sprite positions let hit = null; const player = world.player; const dirX = Math.cos(player.angle); const dirY = Math.sin(player.angle); const planeX = -dirY * 0.66; const planeY = dirX * 0.66; const invDet = 1 / (planeX * dirY - dirX * planeY); for (const e of world.enemies) { if (!e.alive) continue; const spriteX = e.x - player.x; const spriteY = e.y - player.y; const transX = invDet * (dirY * spriteX - dirX * spriteY); const transY = invDet * (-planeY * spriteX + planeX * spriteY); if (transY <= 0.1) continue; // behind or clipped const spriteScreenX = Math.floor((SCREEN_WIDTH/2) * (1 + transX / transY)); if (Math.abs(spriteScreenX - SCREEN_WIDTH/2) < 6) { hit = e; // if multiple, closest if (hit && transY < invDet * (dirY * (hit.x - player.x) - dirX * (hit.y - player.y))) { // actually keep smallest transY (closest) if (transY < transformYs?.[hit] ?? 9999) hit = e; } } } if (hit) { hit.hp -= 15; if (hit.hp <= 0) { hit.alive = false; player.score += 100; } } } function sign(x) { return x < 0 ? -1 : 1; } // ===== UPDATE ===== let lastTime = performance.now(); function update() { const now = performance.now(); const dt = Math.min((now - lastTime) / 1000, 0.05); lastTime = now; const p = world.player; const rotSpeed = 2.5 * dt; if (keys['ArrowLeft']) p.angle -= rotSpeed; if (keys['ArrowRight']) p.angle += rotSpeed; const moveSpeed = 2.8 * dt; const dx = Math.cos(p.angle) * moveSpeed; const dy = Math.sin(p.angle) * moveSpeed; const px = Math.cos(p.angle + Math.PI/2) * moveSpeed; const py = Math.sin(p.angle + Math.PI/2) * moveSpeed; let mx = 0, my = 0; if (keys['KeyW']) { mx += dx; my += dy; } if (keys['KeyS']) { mx -= dx; my -= dy; } if (keys['KeyA']) { mx -= px; my -= py; } if (keys['KeyD']) { mx += px; my += py; } const margin = 0.2; const nx = p.x + mx; const ny = p.y + my; const mxCell = Math.floor(nx + (mx>0?margin:-margin)); const myCell = Math.floor(ny + (my>0?margin:-margin)); if (map[Math.floor(ny)][Math.floor(nx + (mx>0?margin:-margin))] === 0) p.x = nx; if (map[Math.floor(ny + (my>0?margin:-margin))][Math.floor(nx)] === 0) p.y = ny; // keep within bounds p.x = Math.max(1, Math.min(map[0].length-2, p.x)); p.y = Math.max(1, Math.min(map.length-2, p.y)); if (world.fireCooldown > 0) world.fireCooldown -= dt; if (world.muzzleFlash > 0) world.muzzleFlash -= dt; } // ===== RENDER ===== function render() { const p = world.player; // clear sctx.fillStyle = '#000'; sctx.fillRect(0,0,SCREEN_WIDTH,SCREEN_HEIGHT); // ceiling/floor gradient const mid = SCREEN_HEIGHT/2; for (let y=0; y<mid; y++) { const v = Math.floor(40 - y * (40/100)); sctx.fillStyle = `rgb(${v},${v},${v+15})`; sctx.fillRect(0,y,SCREEN_WIDTH,1); } for (let y=mid; y<SCREEN_HEIGHT; y++) { const v = Math.floor(30 + (y-mid) * (50/100)); sctx.fillStyle = `rgb(${v},${Math.floor(v*0.7)},${Math.floor(v*0.5)})`; sctx.fillRect(0,y,SCREEN_WIDTH,1); } // rays dir const dirX = Math.cos(p.angle); const dirY = Math.sin(p.angle); const planeX = -dirY * 0.66; const planeY = dirX * 0.66; // cast rays for (let x=0; x<SCREEN_WIDTH; x++) { const cameraX = 2 * x / SCREEN_WIDTH - 1; const rayDirX = dirX + planeX * cameraX; const rayDirY = dirY + planeY * cameraX; let mapX = Math.floor(p.x); let mapY = Math.floor(p.y); let sideDistX, sideDistY; const deltaDistX = Math.abs(1 / (rayDirX || 1e-10)); const deltaDistY = Math.abs(1 / (rayDirY || 1e-10)); let stepX, stepY; let hit = 0, side = 0; if (rayDirX < 0) { stepX = -1; sideDistX = (p.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - p.x) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (p.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - p.y) * deltaDistY; } for (let i=0; i<40; i++) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } if (map[mapY] && map[mapY][mapX] > 0) { hit = 1; break; } } if (!hit) continue; let perpWallDist; if (side === 0) perpWallDist = (sideDistX - deltaDistX); else perpWallDist = (sideDistY - deltaDistY); const lineHeight = Math.floor(SCREEN_HEIGHT / (perpWallDist + 0.0001)); let drawStart = -lineHeight/2 + SCREEN_HEIGHT/2; if (drawStart < 0) drawStart = 0; let drawEnd = lineHeight/2 + SCREEN_HEIGHT/2; if (drawEnd >= SCREEN_HEIGHT) drawEnd = SCREEN_HEIGHT-1; const wallHeight = drawEnd - drawStart; // texture x let wallX; if (side === 0) wallX = p.y + perpWallDist * rayDirY; else wallX = p.x + perpWallDist * rayDirX; wallX -= Math.floor(wallX); let texX = Math.floor(wallX * 64); if (side === 0 && rayDirX > 0) texX = 64 - texX - 1; if (side === 1 && rayDirY < 0) texX = 64 - texX - 1; texX = Math.max(0, Math.min(63, texX)); // draw wall slice sctx.drawImage(brickTex, texX, 0, 1, 64, x, drawStart, 1, wallHeight); if (side === 1) { sctx.globalAlpha = 0.35; sctx.fillStyle = '#000'; sctx.fillRect(x, drawStart, 1, wallHeight); sctx.globalAlpha = 1.0; } } // sprites (enemies) const spriteOrder = []; for (const e of world.enemies) { if (!e.alive) continue; const spriteX = e.x - p.x; const spriteY = e.y - p.y; const dist = Math.hypot(spriteX, spriteY); spriteOrder.push({ e, dist, sx: spriteX, sy: spriteY }); } spriteOrder.sort((a,b) => b.dist - a.dist); const invDet = 1 / (planeX * dirY - dirX * planeY); for (const obj of spriteOrder) { const {e, sx, sy} = obj; const transX = invDet * (dirY * sx - dirX * sy); const transY = invDet * (-planeY * sx + planeX * sy); if (transY <= 0.5) continue; const spriteScreenX = Math.floor((SCREEN_WIDTH/2) * (1 + transX / transY)); const spriteHeight = Math.abs(Math.floor(SCREEN_HEIGHT / transY)); const drawStartY = Math.floor(-spriteHeight/2 + SCREEN_HEIGHT/2); const spriteWidth = Math.floor(spriteHeight / 2); const drawStartX = Math.floor(-spriteWidth/2 + spriteScreenX); const texWidth = 64, texHeight = 128; for (let stripe = Math.max(0, drawStartX); stripe < Math.min(SCREEN_WIDTH, drawStartX + spriteWidth); stripe++) { const relX = stripe - spriteScreenX + spriteWidth/2; const texX = Math.floor((relX / spriteWidth) * texWidth); if (texX < 0 || texX >= texWidth) continue; const stripeH = spriteHeight; const y0 = drawStartY; sctx.drawImage(enemyTex, texX, 0, 1, texHeight, stripe, y0, 1, stripeH); } } // floor grid overlay hint? just skip. // weapon & muzzle flash const p90 = p.angle; // direction for slight bob etc omitted // flash on enemy deaths? skip // muzzle flash if (world.muzzleFlash > 0) { sctx.globalAlpha = 0.8; const flicker = Math.random(); sctx.fillStyle = `rgb(255,240,${Math.random()>0.5?200:50})`; sctx.beginPath(); sctx.arc(SCREEN_WIDTH/2 + (Math.random()-0.5)*10, SCREEN_HEIGHT - 50, 22 + flicker*15, 0, Math.PI*2); sctx.fill(); sctx.globalAlpha = 1.0; } // simple gun graphic (pixelated) sctx.fillStyle = '#222'; sctx.fillRect(SCREEN_WIDTH/2-12, SCREEN_HEIGHT-60, 24, 60); sctx.fillStyle = '#444'; sctx.fillRect(SCREEN_WIDTH/2-10, SCREEN_HEIGHT-62, 20, 6); sctx.fillStyle = '#151'; sctx.fillRect(SCREEN_WIDTH/2-2, SCREEN_HEIGHT-60, 4, 30); // barrel sctx.fillStyle = '#151'; sctx.fillRect(SCREEN_WIDTH/2-2, SCREEN_HEIGHT-90, 4, 34); sctx.fillStyle = '#111'; sctx.fillRect(SCREEN_WIDTH/2-4, SCREEN_HEIGHT-90, 10, 6); // hud overlay onto low-res sctx.fillStyle = '#0f0'; sctx.font = '10px monospace'; sctx.textAlign = 'left'; sctx.fillText(`HP:${p.hp}`, 4, 12); if (p.ammo < 10) sctx.fillStyle = '#fa0'; sctx.fillText(`AMMO:${p.ammo}`, 4, 24); sctx.fillStyle = '#fc0'; sctx.fillText(`SCORE:${p.score}`, 4, 36); if (p.hp <= 20) { sctx.fillStyle = '#f00'; sctx.fillText('LOW HEALTH', 4, 48); } // scale to full screen ctx.imageSmoothingEnabled = false; ctx.drawImage(screenCanvas, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, W, H); } // ===== GAME LOOP ===== function loop() { update(); render(); requestAnimationFrame(loop); } loop(); </script> </body> </html> ```
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.