Wolfenstein raycaster · nex-n2-pro
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.
transcript · 8 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
We need respond by building single-page web app in index.html current dir. Need inspect maybe empty. Need create self-contained HTML with CSS/JS inline no external. Need implement first-person raycasting maze shooter Wolfenstein 3D style: WASD movement, arrow or mouse turning, textured grid maze, enemies shoot. Procedural textures only. Need likely write index.html. Need plan. Since final concise. Need implement robust game. We need create index.html in /tmp/ws. Need maybe inspect cwd. Use bash ls?
{"command":"ls -la /tmp/ws"}total 0 drwxr-sr-x. 2 node node 6 Aug 1 20:59 . drwxrwsrwx. 3 root node 16 Aug 1 20:59 ..
thinking
Need write full index. Need implement raycasting engine. Need include canvas, UI overlay, instructions. Need procedural textures. Need no external.
Let's design game.
HTML:
- full viewport dark.
- canvas id game width maybe 960x540 CSS fill.
- overlay crosshair, HUD health ammo enemies minimap maybe.
- script.
Engine:
- constants width height maybe canvas internal 960x540. DPR? Could set canvas.width = 960; height=540 and CSS responsive. But can adapt to viewport. We'll use logical resolution 960x540 for performance. CSS 100vw/100vh.
- world grid map 16x16 maybe 0 empty, 1 wall, 2 metal, 3 stone, 4 red wall. Maze procedural? User asks textured grid maze, not necessarily procedural maze. But "Procedural textures only, no external assets." We can generate maze algorithmically? Nice. Use grid maze generation with random seed? Deterministic.
Raycasting:
- player x,y,dir angle, plane x,y. FOV maybe 66 deg. Cast for each x pixel: cameraX = 2*x/w -1, rayDir = dir + plane*cameraX. DDA find hit. wallType. perpWallDist. draw vertical strip floor/ceiling sky. wall texture. Need texture coordinates and x offset. Procedural textures: generate 64x64 offscreen canvases for wall types, floor/ceiling? We can draw vertical stripes by sampling generated textures.
- Textures:
- generateTexture(type): 64x64 canvas. Use deterministic noise? Need procedural. Maybe create base, draw bricks/blocks with random variations. Use seeded random.
- wall types: brick, metal, stone, red.
- floor/ceiling maybe draw gradient/noise on offscreen? Raycasting floor casting maybe more complex. Wolfenstein style has flat floor/ceiling. Could draw gradient/noise background. User specifically textured grid maze, "textured grid maze" maybe walls textured. But "through a textured grid maze" maybe enough. Could implement floor/ceiling with procedural gradient and noise.
- Enemies:
- sprites billboards in world. Enemies have x,y, alive, hp, state, cooldown, speed, hurt timer, attack.
- Raycast sprites: For each enemy, transform to camera: spriteX = enemy.x - player.x; spriteY = enemy.y - player.y; invDet = 1/(planeX*dirY - dirX*planeY) (using rayDir/plane). transformX = invDet * (dirY*spriteX - dirX*spriteY); transformY = invDet * (-planeY*spriteX + planeX*spriteY). depth = transformY. screenX = (w/2)*(1 + transformX/transformY); spriteHeight = abs(h/transformY)*scale maybe 1.1. spriteWidth similarly. Draw if in front and visible, not blocked by wall: dist to enemy < wallDist maybe. Draw procedural enemy sprite texture: 64x64 alien/soldier with body, eyes, gun; use damage overlay. Use zBuffer per strip for occlusion. Draw sprite vertical strip only where texture alpha? We can draw whole rectangle with black transparent? Since canvas offscreen texture transparent? Need create sprite canvas with transparent background. We can sample alpha and draw.
- Shooting:
- mouse click or space shoots. Crosshair. Need hit enemy under crosshair. We can raycast from player along dir to find nearest enemy within FOV maybe if enemy screen x near center and visible. Simpler: compute each enemy direction difference angle and distance, require abs(normalized angle diff)<0.05 and no wall closer than enemy. Or use sprite transform: enemy screen x near center, transformY>0, abs(transformX)/transformY < 0.035, and wall hit distance > enemy distance. Shoot with recoil, muzzle flash. Damage 34? Enemies hp 100. Ammo maybe 30, pickups? Need manage.
- Could include enemy shooting at player when visible and within range. Use setInterval per enemy every 1 sec if line-of-sight. Need line-of-sight DDA maybe same ray from enemy to player? Simpler if distance < 8 and no wall in between via raycast from enemy to player.
- Movement:
- WASD. Arrow keys or A/D? User says WASD movement and arrow or mouse turning. We can also allow A/D strafe. Mouse turning via pointer lock? Browser restrictions. Implement mousemove when pointer locked or when mouse held? We can request pointer lock on canvas click. Also if no pointer lock, use mousemove relative if pointer inside? Need no network. Canvas click to capture. Controls:
- keys W/S forward/back, A/D strafe.
- ArrowLeft/Right turn; mouse movement if pointer locked or mouse down? Maybe implement pointer lock: click game canvas, requestPointerLock; mousemove dx adjusts angle. Also if not locked and mouse is over canvas with button down, adjust.
- Space shoot, R reload, M toggle minimap maybe.
- Collision: move player by x,y; if not wall at new position, allow. Use radius.
- Minimap optional draw at bottom right.
- Game loop:
- requestAnimationFrame, delta clamp.
- update movement, enemies, shooting cooldown, reload.
- render.
- Maze:
- Generate grid. Need ensure playable. Could use fixed map with outer walls and corridors. But "maze" maybe generate. Need robust. We can generate with recursive backtracker on odd cells, then add loops. Map dimensions 21x21? Need raycasting grid with odd cells. Use 21x21. Outer walls. Use seed. Then place enemies in empty cells, player at 1,1 maybe. But if map generated, need ensure reachable. Could place enemies in random empty cells not near player. Need map maybe 21x21 (441 cells) with lots of walls. Raycasting fine.
- But Wolfenstein 3D style often grid map with blocks. Need maybe 21x21 with cells 1 or 0. Generate maze with DFS:
```
map = Array(H).fill(1)
carve(x=1,y=1): mark 0; shuffle dirs 2; while in bounds and map[nx][ny]==1: map[y+dy]=0; map[ny][nx]=0; carve(nx,ny)
```
Use odd dims.
Add random openings: choose wall cell with empty neighbors opposite, open.
Place wall types: for empty? only walls. Type based on hash/noise, maybe 1-4.
- Map dimensions odd maybe 21x21. Player at (1.5,1.5). Enemies at random empty cells not too close (distance > 8) and not all. Place 8 enemies.
- Need maybe spawn pickups? Not required. But enemies shoot. Could have health packs/ammo? Could include procedural pickups. But user didn't ask. We can add ammo/health pickups. However complexity. Maybe no.
- Rendering:
- Clear canvas. Draw sky gradient and floor gradient with procedural noise? Could draw top half gradient, bottom floor with perspective maybe. Since wall textures vertical. We can draw background:
- sky: linear gradient #162033 to #070b12; noise stars? Use offscreen 960x270 with random dots.
- floor: gradient #14110e to #050404; draw noise overlay.
- Draw walls:
- For each x from 0 to w-1, compute ray. DDA. Draw vertical strip from ceil(start) to ceil(end). Need texture column.
- DDA:
```
mapX=floor(posX); mapY=floor(posY);
deltaDistX = abs(1/rayDirX) etc; if rayDirX<0 stepX=-1, sideDistX=(posX-mapX)*deltaDistX else (mapX+1-posX)*deltaDistX.
while not hit: if sideDistX<sideDistY...
perp = side==0 ? (mapX - posX + (1-stepX)/2)/rayDirX : (mapY - posY + (1-stepY)/2)/rayDirY.
```
Need guard rayDir zero.
- wallLineStart = -h*perp/2 + h/2; wallLineEnd = h*perp/2 + h/2; clamp maybe.
- texX = wallY if side==0 else wallX; wallX = posX + perp*rayDirX; wallY = posY + perp*rayDirY; texX -= floor(texX); texX *= TEX_SIZE; if side==1 invert maybe darken.
- texXInt = floor(texX). Draw column from texture: drawImage(tex, texXInt,0,1,TEX_SIZE, x, start, 1, stripHeight). But if start/end fractional, drawImage handles. Need avoid start<0/end>h.
- zBuffer[x] = perp.
- Add shading based on distance and side. Could draw semi-transparent overlay dark. Instead drawImage then fillRect with rgba black.
- Draw sprites after walls. Need sort by distance desc. For each sprite:
- transform, if transformY <=0 skip.
- spriteScreenX = 0.5*(w)*(transformX/transformY +1)
- spriteH = Math.abs(h / transformY) * 0.95? In Wolf3D spriteHeight = h/transformY. For enemies maybe 0.85.
- spriteW = spriteH * 0.75? Texture aspect 64x64, so same.
- Draw vertical strips from startX to endX. Need only if in front and not behind wall:
```
for stripe = start; stripe < end; stripe++ {
texX = Math.floor((stripe - (spriteScreenX - spriteW/2)) * TEX / spriteW);
if texX in [0,TEX) && transformY > 0 && stripe>0 && stripe<w && transformY < zBuffer[stripe] {
drawImage(spriteTex, texX,0,1,TEX, stripe, startY, 1, spriteH)
}
}
```
Need startY = -spriteH/2 + h/2. Use enemy sprite texture with alpha. DrawImage preserves alpha. But if draw only strips, no issue.
- Need zBuffer for sprite occlusion. Since walls zBuffer per pixel. Good.
- Draw weapon:
- Use canvas shapes at bottom center. Procedural gun: draw rectangles, muzzle flash if shooting. Could be stylized.
- Draw damage vignette red if hurt.
- Draw HUD:
- Health bar, ammo, enemies, level, FPS maybe.
- Crosshair center.
- overlay messages.
- Minimap:
- draw small square on canvas at top right. Show walls, player, enemies. Need toggle? Could always show small.
- Game states:
- Win/lose. If health <=0, show game over. If enemies alive 0, win. Need restart button maybe.
- Enemies shoot:
- Enemy update: if alive and distance < 9 and lineOfSight:
- face player.
- cooldown -= dt; if <=0 shoot: damage 8-15, player health -= damage, hurtTimer=0.25, message.
- cooldown = 0.8 + random.
- Maybe chase if close? Wolfenstein enemies can move. Need "enemies you can shoot"; maybe they can move toward player but not necessary. Could make enemies patrol/chase:
- if distance > 1.2 and < 8 and LOS, move toward player slowly, collision.
- else random wander maybe.
- Need avoid enemies in walls. Movement collision.
- Enemy AI:
```
if dist < 10 && hasLOS(enemy, player):
if dist > 1.4: enemy.x += dirX*speed*dt
if cooldown<=0 shoot
else cooldown +=?
```
lineOfSight:
- from enemy to player DDA. Could use raycastTo(x,y,dir, maxDist) returns hit type/distance. If hit wall before dist < 0.5 false. But if ray starts inside enemy? Enemy at center of empty cell. Fine.
- Shooting:
- When click/space:
- if ammo <=0, empty sound? no audio maybe.
- ammo--, shotTimer=0.08, recoil=1.
- Find target:
```
best = null; bestScore = Infinity;
for enemy alive:
dx,dy; dist = hypot; angleDiff = abs(normalizeAngle(atan2(dy,dx)-player.dir));
if angleDiff < 0.045 && dist < rayHit.distance - 0.1:
score = angleDiff*10 + dist; choose smallest.
// also screen center method
```
Need line-of-sight not blocked. Use raycastTo with player dir. If hitWall && hitDist < dist-0.1 skip. If no wall, hit if angleDiff small.
If hit: enemy.hp -= 35; enemy.hurt=0.2; if hp<=0 kill.
- Reload: if ammo <=0 and total? No total. We can have ammo capacity and infinite? Better have ammo pack? If infinite, HUD ammo capacity. But shooter should have ammo. Could use infinite magazines? "Ammo" maybe 30 with auto reload infinite reserve? Hmm.
- Simpler: ammo is current magazine with 24, reserve 96. R reload. HUD "Ammo 24/96". If magazine empty, reload after 1s. If reserve 0, no.
- On kill, maybe drop ammo/health? Could add pickups? Not required. But reserve finite. Add procedural pickups:
- pickups array with type ammo/health, x,y. Draw as sprites, pickup on proximity. This adds complexity but nice.
- Maybe infinite ammo with "Ammo ∞" is okay. But shooting mechanic. Could include 100 bullets reserve? Need no external.
- Procedural textures:
- Need generate using deterministic PRNG. Could create function mulberry32(seed). For each texture, draw base fill, random noise pixels, patterns. Use canvas.
- Wall textures:
- brick: fill #73513f, draw mortar lines every 16, bricks with offsets, random color variation.
- metal: fill #4d5866, draw grid lines, rivets circles, panels.
- stone: fill #59606a, draw polygonal cracks.
- red: fill #6b2020, draw darker stripes and skull-like? Maybe.
- Enemy sprite:
- transparent. Draw shadow? Draw head/body. Use random variation? Since multiple enemies can use same texture with hue? Could generate two types.
- Need alpha in offscreen. Use ctx.clearRect. Draw legs, body, head, eyes. Maybe enemy has red glowing eyes, gun.
- Pickup sprites:
- ammo: draw box with bullets.
- health: draw cross.
- Maybe door? Not needed.
- Audio? No external. Could use Web Audio generated beeps. Not required but nice. Need maybe no external. Could implement simple sound effects:
- `audioCtx` on first user interaction. Functions beep/noise. Shooting, hit, hurt. But browser requires user gesture. We can initialize on pointerdown/keydown. Could be okay. But not necessary.
- Need ensure all code in one file. Need final mention index.html present.
Let's craft HTML carefully.
Potential issues:
- Canvas pointer lock: Need click to lock. But if user doesn't want, arrow keys turn. Mouse turning if locked. Also if pointer is over canvas and mouseDown, turn. Need prevent context menu.
- If canvas is full viewport, CSS. Use `document.documentElement.requestPointerLock`? Need `canvas.requestPointerLock()`. On canvas click if gameRunning and not locked request pointer lock. But click also shoots? Could be annoying. We can click anywhere to lock and maybe shoot. Maybe implement:
- `canvas.addEventListener('click', () => { if(document.pointerLockElement !== canvas) canvas.requestPointerLock(); else shoot(); });`
- But if click for pointer lock triggers shoot? We can separate:
- On mousedown left: if not locked requestPointerLock(); else shoot();
- Need prevent default.
- Mousemove: if pointerLockElement===canvas or mouseDown, player.dir += e.movementX*0.0025.
- Keyboard:
- W/S, A/D, ArrowLeft/Right. Need prevent default for arrows/space.
- M toggle minimap.
- P pause maybe.
- Movement:
- `dirX = cos(angle); dirY=sin(angle); planeX=-dirY*planeDist; planeY=dirX*planeDist` where planeDist = tan(fov/2). FOV = 66 deg. In raycasting, dir vector and plane vector.
- Movement:
```
moveX=0, moveY=0;
if W { moveX += dirX; moveY += dirY}
if S { ...}
if A { moveX += dirY; moveY -= dirX } // left strafe
if D { moveX -= dirY; moveY += dirX}
normalize if length>1
speed = shift ? 3.2 : 2.4
tryMove(moveX*speed*dt, 0)
tryMove(0, moveY*speed*dt)
```
Collision with radius:
```
function isWallAt(x,y) { mx=Math.floor(x), my=Math.floor(y); if outside or map[my][mx] !== 0 return true; }
function canMove(x,y) { check four corners x±radius y±radius; }
```
- Enemies collision with player/walls maybe.
- Raycast function:
- Need returns distance, mapX,mapY,side,type. For shooting and LOS.
- Use `MAX_RENDER_DIST=24`.
- If rayDirX/Y near 0, deltaDist = 1e30.
- `while !hit && steps<64`: DDA.
- `perpWallDist`.
- `wallX = side==0 ? player.y + perp*rayDir.y : player.x + perp*rayDir.x;`
- type = map[mapY][mapX] (1-4).
- For LOS from enemy, need pass start x,y and dir:
```
function castRay(startX,startY,angle,maxDist) { ... }
```
Could use same with `origin`.
- For shooting, use `castRay(player.x, player.y, player.dir, 20)` returns hit. Then compare target dist.
- For enemy LOS, use `castRay(enemy.x, enemy.y, atan2(dy,dx), dist+0.5)` and if hit.wall && hit.dist < dist -0.2 false. But if enemy is standing near wall, ray starts at center empty. Fine.
- Render:
- Use `ctx.imageSmoothingEnabled = false;`
- `ctx.clearRect`.
- `drawBackground`: Could use pre-generated sky/floor canvases:
- `skyCanvas` 960x270 with gradient and stars/noise.
- `floorCanvas` 960x270 with gradient and noise, maybe darker bottom.
- draw sky at 0,0,w,h/2; floor at 0,h/2.
- Walls:
- Need texture columns. Since drawImage one pixel wide from source. If start/end maybe integer. Use `Math.ceil`. But if start < 0 and end > h, drawImage with negative height? We'll clamp.
- `let drawStart = Math.max(0, Math.floor(-h*perp/2+h/2)); let drawEnd = Math.min(h-1, Math.ceil(h*perp/2+h/2));`
- `let lineHeight = drawEnd - drawStart;`
- `ctx.globalAlpha = shade; ctx.drawImage(texture, texX,0,1,TEX_SIZE,x,drawStart,1,lineHeight); ctx.globalAlpha=1;`
- But if drawEnd exclusive? canvas drawImage dest height can be drawEnd - drawStart. If drawEnd is h-1, height maybe h-1-start. Fine.
- `ctx.fillStyle = side? 'rgba(0,0,0,.20)'...; fillRect(x,drawStart,1,lineHeight)`.
- zBuffer array length w, fill with MAX.
- Draw sprites:
- Need sort by transformY descending.
- For each enemy alive:
- transform. If transformY <= 0.01 skip.
- spriteScreenX = (w/2)*(1 + transformX/transformY)
- spriteH = Math.min(h*1.8, Math.abs(h / transformY) * 0.9)
- spriteW = spriteH * 0.75? Since texture 64x64, maybe 0.85. Use 0.85.
- startX = Math.floor(spriteScreenX - spriteW/2), endX = Math.ceil(...).
- For stripe:
- if stripe <0 or >=w skip
- texX = floor((stripe - (spriteScreenX - spriteW/2))*TEX_SIZE/spriteW)
- if transformY > zBuffer[stripe] continue.
- drawImage(enemyTex, texX,0,1,TEX,stripe,startY,1,spriteH)
- Need draw only if stripe within.
- Enemy hurt flash: Could use texture variants? Easier after drawing sprite, if enemy.hurt>0, overlay red rectangle strips? But sprite texture has alpha. Could draw with globalComposite? Use `ctx.globalAlpha = 0.45*enemy.hurt/0.2; fillRect(stripe,startY,1,spriteH)` with red. But fillRect not masked to sprite. Better draw sprite texture red? Could generate hurt texture? Or draw the same sprite with `ctx.globalCompositeOperation='source-atop'`? But after drawing strips, we could redraw strips with `ctx.fillStyle='rgba(255,60,60, alpha)'` but not masked. Use offscreen enemyHurtTexture generated by applying red overlay. Then draw from hurt texture. Simpler: if hurt>0 draw from hurt texture. Need generate hurt textures for each enemy type.
- Maybe draw health bar above enemy: if close. Not necessary but nice.
- Pickups:
- Similar sprites. Use pickup texture. Maybe draw only if in front and not blocked.
- Draw weapon:
- `weaponBob = Math.sin(time*speed*10)*... if moving`.
- `gunX = w/2 + recoil*20 + bobX`, `gunY = h + bobY`.
- Draw shadow, gun body. Use shapes:
- barrel, handle, side panels.
- Muzzle flash if shotTimer>0: radial gradient or polygon at top.
- Need after sprites? Weapon in foreground should overlay. Draw after sprites.
- Draw HUD:
- Crosshair: lines center.
- bottom panel: semi-transparent black rectangle, text.
- Health bar.
- message.
- minimap:
- if showMini:
- `size=150`, `cell=size/MAP_H`, draw map walls maybe only nearby? Could draw all. Use wall colors by type. Draw enemies, pickups, player direction.
- Need not too intrusive.
- Damage overlay:
- if hurtTimer>0, fill red radial gradient or alpha.
- Game over/win overlay:
- if state !== 'playing', draw translucent overlay and text.
- Press R restart? But R also reload. Use Enter restart. Or button? Since self-contained, maybe clickable restart. But can include text.
- Need maybe support touch? Not required.
- Need ensure no external resources. Use canvas.
- Need maybe use localStorage? Not needed.
- Need write file. We can compose. Need be careful with quotes, JS syntax.
Let's craft index.html.
Potential issue: Offscreen textures with canvas. Need create before game? We can create after DOM loaded. Since script at end, okay.
Need maybe include `const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d', { alpha: false });` We set canvas.width/height to 960/540. CSS. On resize maybe no need.
Need maybe high-DPI? If canvas internal 960x540 and CSS full, okay. Could adjust canvas.width based on viewport? But raycasting resolution fixed. CSS scales. Could set `canvas.width = Math.min(window.innerWidth, 1280)`? But textures and zBuffer use canvas.width. Simpler fixed 960x540. Good.
Need maybe `ctx.imageSmoothingEnabled = false` each frame.
Need deterministic random:
```
function mulberry32(a){ return function(){ let t=a+=0x6D2B79F5; t=Math.imul(t^t>>>15,t|1); t^=t+Math.imul(t^t>>>7,t|61); return ((t^t>>>14)>>>0)/4294967296;}}
```
Need maybe seeded map. Use `let rand = mulberry32(1337);`
Maze generation:
```
const MAP_W=21, MAP_H=21;
let map = [];
function generateMaze() {
map = Array.from({length:MAP_H},()=>Array(MAP_W).fill(1));
const stack = [{x:1,y:1}];
map[1][1]=0;
const dirs = [[2,0],[-2,0],[0,2],[0,-2]];
while(stack.length) {
const cur=stack[stack.length-1];
const options = shuffle(dirs).filter(([dx,dy]) => {
const nx=cur.x+dx, ny=cur.y+dy;
return nx>0 && nx<MAP_W-1 && ny>0 && ny<MAP_H-1 && map[ny][nx]===1;
});
if(options.length) {
const [dx,dy]=options[0];
map[cur.y+dy/2][cur.x+dx/2]=0;
map[cur.y+dy][cur.x+dx]=0;
stack.push({x:cur.x+dx,y:cur.y+dy});
} else stack.pop();
}
// add loops
for i 0..160: choose x/y odd? wall with empty opposite
if map[y][x]===1 and (map[y-1][x]===0 && map[y+1][x]===0 || map[y][x-1]===0 && map[y][x+1]===0) then map[y][x]=0
// maybe carve rooms
for room 0..3: rx=3+rand*(MAP_W-6), ry=...; if odd? ensure odd; carve 3x3
// type walls
for y/x if map[y][x]===1:
const n = Math.floor(rand()*4)+1;
map[y][x] = n;
}
```
Need if player at 1.5,1.5, map[1][1]=0. Good.
Potential issue: Generated maze may have long corridors, enemies line of sight. Good.
Place enemies:
```
const empty=[];
for y=1..MAP_H-2 for x=1..MAP_W-2 if map[y][x]===0 && dist from player > 6 empty.push({x:x+0.5,y:y+0.5});
shuffle(empty)
enemies = [];
for i=0; i<Math.min(10, empty.length); i++ enemies.push({x:empty[i].x+0.5? wait if x is cell index, center x+0.5, y+0.5, hp:100, ...});
```
Need avoid too close maybe >7.
Pickups:
- After enemies, choose some empty not near player/enemies. Types health/ammo. Place 8.
- Pickups array {x,y,type,taken}.
- Draw pickups as sprites.
- Update pickup if dist < 0.55:
- health if health<100: health=min(100,health+25); message.
- ammo if reserve<120: reserve+=24; message.
Need maybe enemies can shoot and move. If they move, pickups maybe. Good.
Need maybe collision with enemy? If player near enemy, damage? Could push. Not necessary.
Need map wall type:
- Use `map[my][mx]` type 1..4.
Need background canvases:
```
const skyCanvas = document.createElement('canvas'); skyCanvas.width=960; skyCanvas.height=270; const sctx = ...
const grad = sctx.createLinearGradient(0,0,0,270); ...
for stars: fillRect random;
floorCanvas: gradient, noise pixels maybe.
```
Need maybe draw floor with noise. Fine.
Need generate wall textures:
- Use `makeWallTexture(type)`.
- Need `noise` maybe draw many small rects.
- For brick:
```
ctx.fillStyle='#6b4a3a'; fillRect
for y=-8; y<TEX; y+=16 { draw mortar line; for x= -?; x<TEX; x+=32 { fillRect x, y, 32, 16; random dark/light } }
```
- Metal:
```
fill #4a5563; draw panels; draw diagonal highlights; rivets.
```
- Stone:
```
fill #555b64; draw irregular blocks: grid with jitter.
```
- Red:
```
fill #6b1f25; draw darker vertical streaks, small rivets.
```
Need maybe `ctx.imageSmoothingEnabled=false`.
Enemy texture:
```
function makeEnemyTexture(hurt=false) {
ctx.clearRect
// shadow maybe transparent? draw ellipse with rgba black at bottom.
const bodyColor = hurt ? '#ff4a4a' : '#6fbf72';
draw legs, body, head, helmet, eyes red, gun.
}
```
Need alpha. Use `ctx.globalAlpha`? For shadow.
Pickup texture:
- Ammo box: fill brown, label "AMMO"? Canvas text maybe okay. Use procedural.
- Health: white box with red cross.
Could use text "MED" but procedural? Text is generated, okay.
Need maybe generate `wallTextures = [null, makeWallTexture(1), ...]`.
Need maybe `ctx.drawImage(texture, texX, 0, 1, TEX_SIZE, x, drawStart, 1, drawEnd - drawStart)`; if drawEnd - drawStart <=0 skip. `drawImage` source y=0, h=TEX.
Need maybe `drawEnd = Math.min(H, Math.ceil(...))`; start = max(0, floor). If drawEnd > H. Good.
Need maybe zBuffer fill:
```
for (let x=0; x<W; x++) zBuffer[x]=MAX_RENDER_DIST;
```
Need performance okay: 960 rays, 10 enemies, 21 map.
Need maybe `raycast` uses `originX` optional. Since player object. We'll implement:
```
function castRay(originX, originY, angle, maxDist = MAX_RENDER_DIST) {
const rayDirX = Math.cos(angle), rayDirY = Math.sin(angle);
...
}
```
Need if rayDirX=0: deltaDistX=1e30. Good.
```
let hit = false, side=0;
while(!hit && steps<MAX_STEPS) {
if(sideDistX < sideDistY){ sideDistX += deltaDistX; mapX += stepX; side=0;} else...
if(mapX<0||...){ hit=true; perp=maxDist; break;}
if(map[mapY][mapX] > 0) hit=true;
}
if(!hit) {perp=maxDist;}
else {
if(side===0) perp=(mapX-originX+(1-stepX)/2)/rayDirX;
else perp=(mapY-originY+(1-stepY)/2)/rayDirY;
}
```
If rayDir negative, formula positive. Good.
Need `perp = Math.max(0.0001, Math.abs(perp));`
Return `type: hit? map[mapY][mapX] || 1 : 0`.
Need maybe enemy LOS from enemy to player:
```
function hasLineOfSight(ax,ay,bx,by) {
const dx=bx-ax,dy=by-ay,dist=Math.hypot(dx,dy);
if(dist<0.5) return true;
const hit=castRay(ax,ay,Math.atan2(dy,dx),dist);
return !hit.wall || hit.dist >= dist - 0.35;
}
```
But castRay starts at enemy x,y, if ray hits wall at very near due to starting close to wall? Fine.
Need maybe enemy shooting at player from behind wall? line-of-sight prevents.
Need enemy update:
```
function updateEnemies(dt) {
for e of enemies:
if(!e.alive) continue;
e.hurt=Math.max(0,e.hurt-dt);
const dx=player.x-e.x,dy=player.y-e.y,dist=Math.hypot(dx,dy);
if(dist<12 && hasLineOfSight(e.x,e.y,player.x,player.y)) {
e.awake=1;
e.angle=Math.atan2(dy,dx);
if(dist>1.6) {
const nx=dx/dist, ny=dy/dist;
const speed = e.speed*(dist>7?0.45:1);
moveEntity(e,nx*speed*dt,ny*speed*dt,0.28);
}
e.cooldown -= dt;
if(e.cooldown<=0 && dist<9) {
e.cooldown = 0.8 + rand()*0.7;
player.health -= e.damage + rand()*5;
player.hurt=0.35;
messages...
flash...
playSound('hurt')
}
} else {
e.cooldown = Math.min(e.cooldown, 1.2);
e.awake = Math.max(0, e.awake-dt);
// wander
if(e.wanderT<=0){ e.wanderAngle=rand()*TAU; e.wanderT=1.5+rand()*2; }
e.wanderT -= dt;
moveEntity(e, Math.cos(e.wanderAngle)*0.35*dt, Math.sin(e.wanderAngle)*0.35*dt,0.28);
}
}
}
```
But if enemy moves into player? MoveEntity can check collision with player? Need avoid overlap:
```
function entityBlocksPlayer(ex,ey,r) { return Math.hypot(ex-player.x,ey-player.y)<r+player.radius; }
function moveEntity(entity, dx,dy,radius) { try x then y; after move, if overlap push away? }
```
Could ignore. But if enemy moves into player, damage maybe. Maybe in update after movement if dist<0.65 damage.
Need `moveEntity` use `canOccupy(x,y,radius, ignorePlayer=false)`. For player, check walls; for enemies, check walls and other enemies maybe.
```
function canOccupy(x,y,r, includePlayer=true) {
if includePlayer && Math.hypot(x-player.x,y-player.y)<r+player.radius return false;
return canMove(x,y,r)
}
```
But for player, includePlayer false? Need not.
- `canMove` checks walls at four corners. For enemy, if inside map and empty.
Need pickups:
```
function updatePickups() {
for p of pickups if !taken && dist<0.55:
if p.type health and health<100: health...
if ammo and reserve<120: reserve...
p.taken=true; spawn floating message.
}
```
Need draw pickups as sprites. They are not enemies but can use sprite list.
Need shooting:
```
function shoot() {
if(gameState !== 'playing') return;
if(reloadTimer>0) return;
if(magazine<=0) { message='Click R to reload'; playSound('empty'); return; }
magazine--;
shotTimer=0.08; recoil=1;
const hit=castRay(player.x,player.y,player.dir,MAX_RENDER_DIST);
let target=null, best=Infinity;
for e of enemies:
if(!e.alive) continue;
const dx=e.x-player.x,dy=e.y-player.y; const dist=Math.hypot(dx,dy);
if(dist<0.5) continue;
const angle = Math.atan2(dy,dx);
const diff = Math.abs(angleDiff(angle, player.dir));
const crossTolerance = 0.045 + 0.006/(dist+1)? maybe.
if(diff < 0.045 && dist < hit.dist - 0.2) {
const score = diff*1000 + dist;
if(score<best) { target=e; best=score;}
}
if(target) {
target.hp -= 34 + Math.random()*8;
target.hurt = 0.16;
target.vx? no.
message...
playSound('hit')
if(target.hp<=0) { target.alive=false; message='Enemy down'; playSound('kill');}
} else {
playSound('shoot')
}
if(magazine<=0 && reserve>0) { message='Reload (R)'; }
}
```
Need angleDiff:
```
function angleDiff(a,b){ let d=(a-b+Math.PI)%(2*Math.PI)-Math.PI; return Math.abs(d);}
```
In JS `%` can negative; implement.
```
function normalizeAngle(a){ while(a<=-Math.PI)a+=TAU; while(a>Math.PI)a-=TAU; return a;}
```
Need if `hit.dist` maybe MAX_RENDER_DIST. Good.
- Reload:
```
function reload() {
if(gameState !== 'playing') return;
if(reloadTimer>0 || magazine===clipSize) return;
if(reserve<=0){message='No reserve ammo'; return;}
reloadTimer=1.1; message='Reloading...';
}
function updateReload(dt) {
if(reloadTimer>0) {
reloadTimer -= dt;
if(reloadTimer<=0) {
const needed=clipSize-magazine;
const take=Math.min(needed,reserve);
magazine+=take; reserve-=take;
message='Reloaded';
}
}
}
```
Need if reserve finite. Spawn ammo pickups. Maybe reserve starts 72, clip 12. Good.
- If magazine empty after shoot, message. If reserve 0, "No ammo".
- Maybe `shoot` should not reload automatically. Good.
Need game state:
```
let gameState='playing';
function restart() { generateMaze(); setupEntities(); player...; gameState='playing'; message='...'; }
```
Need `restart()` called after initial. But generateMaze uses rand; if reset rand seed.
- On win/lose:
```
if(player.health<=0) { gameState='dead'; message='You died. Press Enter...'; }
if(enemies.every(!alive)) { gameState='won'; message='Maze clear...'; }
```
- Need "R reload" but if game over, R restart? Could handle:
```
if(key==='r' && gameState==='playing') reload(); else if(key==='r' && gameState!=='playing') restart();
if(key==='Enter' && gameState!=='playing') restart();
```
But "R" reload conflicts. Fine.
- Need maybe not use `document.addEventListener('keydown', e => { keys[e.code]=true; if([...]) e.preventDefault(); if(e.code==='KeyR'&&!e.repeat) ...})`.
- Need `keyup` set false.
Need maybe UI overlay outside canvas? User asks single-page web app. We can include CSS/JS inline. Could include DOM HUD? But if using canvas all. No external. Fine.
Maybe include a start overlay with instructions in HTML? Could be helpful. But game starts immediately. We can draw instructions. Need pointer lock instruction. Use canvas overlay. Could also have `<div id="notice">Click to capture mouse</div>` but canvas enough. Since no external.
Need maybe "first-person raycasting maze shooter on a canvas" - all rendering in canvas. Good.
Need maybe no network. Fine.
Need maybe ensure `index.html` present. We'll write.
Let's compose code. Need be cautious with syntax. Use write.
/tmp/ws/index.html (41732 bytes)Successfully wrote 41732 bytes to /tmp/ws/index.html
thinking
Need verify file present maybe maybe run simple grep? Could maybe use `ls -l index.html` and maybe check no external resources. Also maybe open maybe syntax? We can run `node --check`? It's HTML with script; can't directly. Could maybe use `python` to parse? No need. But maybe we should verify file exists.
{"command":"ls -l /tmp/ws/index.html && grep -n \"http\\|cdn\\|<link\\|<script src\" /tmp/ws/index.html || true"}-rw-r--r--. 1 node node 41742 Aug 1 21:02 /tmp/ws/index.html
thinking
Done — `index.html` is present in `/tmp/ws` and is fully self-contained with inline CSS/JS, procedural textures, raycasting, WASD movement, mouse/arrow turning, enemies, shooting, pickups, and no external assets.
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
EVALUATION
Raycasting maze shooter with minimap, textures, HP/ammo HUD, and a death screen, but died quickly with overlapping UI text.
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.