WASD to move | Arrows to turn
FPS: 0
JS details: Constants: const W = 320; const H = 200; const T = 64; const MAP_W = 16; const MAP_H = 16; Map: const map = [...]; // start pos inside an empty cell. Player: let posX = 2.5, posY = 2.5; let dirX = 1, dirY = 0; let planeX = 0, planeY = 0.66; // FOV ~66 deg. Input: const keys = {}; window.addEventListener('keydown', e => keys[e.code] = true); window.addEventListener('keyup', e => keys[e.code] = false); Texture generation: function makeTexture(fn) { // fn(x,y) => [r,g,b] const c = document.createElement('canvas'); c.width = T; c.height = T; const x = c.getContext('2d'); const img = x.createImageData(T,T); const d = img.data; for(let y=0; y>5 ^ y>>5) & 1) return [100,70,50]; else return [80,55,40]; ceiling: dark tiles: if (x%32<2 || y%32<2) return [30,30,35]; else return [50,50,60]; Add a second wall texture maybe for variety? The map can use different values. But map is all 1. I can change some walls to 2 for a different texture. Let's design the map with values 1 or 2. For example, outer walls = 2, inner = 1. Map values: 1 = brick 2 = stone During raycasting, wallTex = map[mapY][mapX] === 2 ? stoneTex : brickTex. Also, maybe apply side shading. Now, the rendering loop. I'll define: const FOG_MAX = 16; function setPixel(idx, r,g,b) { buf[idx] = r; buf[idx+1] = g; buf[idx+2] = b; buf[idx+3] = 255; } Actually, inline. Render function: let lastTime = 0; let bobTime = 0; function frame(now) { const dt = Math.min((now - lastTime)/1000, 0.1); lastTime = now; // movement const moveSpeed = dt * 3.0; const rotSpeed = dt * 2.0; let moved = false; if (keys['KeyW']) { ... moved=true; } if (keys['KeyS']) { ... moved=true; } if (keys['KeyA']) { ... moved=true; } if (keys['KeyD']) { ... moved=true; } if (keys['ArrowLeft']) { rotate -; } if (keys['ArrowRight']) { rotate +; } // bob if (moved) bobTime += dt * 12; else bobTime += dt * 2; // render floor/ceiling // ... // render walls // ... ctx.putImageData(screenData, 0, 0); // weapon drawWeapon(ctx); // fps fpsCounter(now); requestAnimationFrame(frame); } Collision check function: function isWall(x,y) { const mx = Math.floor(x); const my = Math.floor(y); if (mx < 0 || mx >= MAP_W || my < 0 || my >= MAP_H) return true; return map[my][mx] > 0; } When moving, check new position plus a radius. Use radius 0.2: if (!isWall(newX + 0.2, posY) && !isWall(newX - 0.2, posY)) posX = newX; Similarly for y. Rotation: const oldDirX = dirX; dirX = dirX * Math.cos(rotSpeed) - dirY * Math.sin(rotSpeed); dirY = oldDirX * Math.sin(rotSpeed) + dirY * Math.cos(rotSpeed); const oldPlaneX = planeX; planeX = planeX * Math.cos(rotSpeed) - planeY * Math.sin(rotSpeed); planeY = oldPlaneX * Math.sin(rotSpeed) + planeY * Math.cos(rotSpeed); For strafing, use plane vector. Now, floor/ceiling code: for (let y = 0; y < H/2; y++) { const rowDist = (H/2) / (H/2 - y); const leftX = dirX - planeX; const leftY = dirY - planeY; const rightX = dirX + planeX; const rightY = dirY + planeY; const stepX = rowDist * (rightX - leftX) / W; const stepY = rowDist * (rightY - leftY) / W; let fx = posX + rowDist * leftX; let fy = posY + rowDist * leftY; const fog = Math.min(rowDist / FOG_MAX, 1); for (let x = 0; x < W; x++) { const tx = (Math.floor((fx - Math.floor(fx)) * T) & (T-1)); const ty = (Math.floor((fy - Math.floor(fy)) * T) & (T-1)); const ti = (ty * T + tx) * 4; const idx = (y * W + x) * 4; const invFog = 1 - fog; buf[idx] = ceilTex[ti] * invFog; buf[idx+1] = ceilTex[ti+1] * invFog; buf[idx+2] = ceilTex[ti+2] * invFog; buf[idx+3] = 255; fx += stepX; fy += stepY; } } for (let y = H/2+1; y < H; y++) { const rowDist = (H/2) / (y - H/2); // same as above but sample floorTex } Note: H/2 is 100; y loop uses integer. Since H=200, H/2=100. Loop ceiling: y=0..99. Floor: y=101..199. The horizon line y=100 is not drawn by floor/ceiling; walls will cover it, but if no wall, it remains black/clear. We can fill it black or let it be. putImageData initially has transparent; but canvas background is black, so it will show black. Good. Wall code: for (let x=0; x= MAP_W || mapY < 0 || mapY >= MAP_H) { mapX = Math.max(0, Math.min(MAP_W-1, mapX)); mapY = Math.max(0, Math.min(MAP_H-1, mapY)); break; } if (map[mapY][mapX] > 0) break; } let perpWallDist; if (side === 0) perpWallDist = (mapX - posX + (1 - stepX)/2) / rayDirX; else perpWallDist = (mapY - posY + (1 - stepY)/2) / rayDirY; const lineHeight = Math.floor(H / perpWallDist); let drawStart = Math.floor(-lineHeight/2 + H/2); let drawEnd = Math.floor