oneshotlm
Compare all models on this prompt →

Wolfenstein raycaster · deepseek-v3.2

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.

⚠ no runnable output

This one-shot run produced no runnable artifact.

timed out after 600000ms

The transcript below shows what the model did — read it to see why the one shot fell short.

transcript · 40 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
I'll build a single-page web app with a Wolfenstein 3D style raycasting maze shooter. It will include: canvas rendering, grid-based maze, raycasting with textures, enemies, shooting mechanics, WASD movement, mouse/arrow turning, and procedural textures. All in one HTML file.

First, let's check the current directory. Then I'll write the index.html file.
bash {"command":"pwd && ls -la"}
toolResult

toolResult

/workspace total 4 drwxrwxrwx 2 node node 64 Jul 22 18:18 . drwxr-xr-x 1 root root 4096 Jul 22 18:18 ..

assistant
thinking
Good. Now I'll create the index.html file. I'll structure it with canvas, styling, and JavaScript. I'll implement raycasting with texture mapping, grid-based maze generation, enemy AI, shooting, collision detection, mouse movement, and keyboard controls.

Let me write the entire file:
write index.html (40054 bytes)
toolResult

toolResult

Successfully wrote 40054 bytes to index.html

assistant
thinking
Now let's verify the file exists and test it by opening in a browser? We can't open a browser directly, but we can check the file size and maybe serve it with a simple HTTP server for testing? Actually, we just need to ensure the file is correctly written. Let's read a small part to confirm
read {"path":"index.html"}
toolResult

toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wolfenstein 3D Style Raycasting Maze Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', monospace; } body { background: #000; color: #fff; overflow: hidden; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } #gameContainer { position: relative; width: 100%; max-width: 1024px; height: 80vh; margin: 0 auto; } canvas { display: block; background: black; width: 100%; height: 100%; image-rendering: pixelated; image-rendering: crisp-edges; } #ui { position: absolute; top: 10px; left: 10px; color: #ff0; font-size: 16px; text-shadow: 2px 2px 0 #000; z-index: 10; pointer-events: none; } #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #f00; font-size: 24px; z-index: 10; pointer-events: none; } #hud { position: absolute; bottom: 10px; left: 10px; right: 10px; display: flex; justify-content: space-between; align-items: flex-end; color: #fff; font-size: 18px; text-shadow: 2px 2px 0 #000; z-index: 10; pointer-events: none; } .health, .ammo, .score { background: rgba(0, 0, 0, 0.6); padding: 5px 10px; border-radius: 5px; border: 2px solid #444; } #instructions { margin-top: 10px; text-align: center; color: #aaa; font-size: 14px; max-width: 800px; line-height: 1.4; } #fullscreenBtn { margin-top: 10px; padding: 8px 16px; background: #333; color: #fff; border: 2px solid #666; border-radius: 5px; cursor: pointer; font-size: 14px; transition: all 0.2s; } #fullscreenBtn:hover { background: #555; } #gameOver { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.85); color: #f00; padding: 40px; border-radius: 10px; border: 4px solid #f00; text-align: center; z-index: 100; display: none; } #gameOver h2 { font-size: 48px; margin-bottom: 20px; } #gameOver button { padding: 15px 30px; background: #333; color: #fff; border: 2px solid #f00; border-radius: 5px; cursor: pointer; font-size: 20px; transition: all 0.2s; } #gameOver button:hover { background: #555; } </style> </head> <body> <h1>Wolfenstein 3D Style Raycasting Shooter</h1> <div id="gameContainer"> <canvas id="gameCanvas"></canvas> <div id="ui"> <div>FPS: <span id="fps">0</span></div> <div>Pos: <span id="pos">0,0</span></div> <div>Angle: <span id="angle">0</span></div> </div> <div id="crosshair">+</div> <div id="hud"> <div class="health">Health: <span id="healthValue">100</span></div> <div class="ammo">Ammo: <span id="ammoValue">30</span></div> <div class="score">Score: <span id="scoreValue">0</span></div> </div> <div id="gameOver"> <h2>GAME OVER</h2> <p>You have been defeated!</p> <p>Final Score: <span id="finalScore">0</span></p> <button onclick="resetGame()">PLAY AGAIN</button> </div> </div> <div id="instructions"> <p>WASD: Move | Mouse/Arrows: Turn | Left Click: Shoot | R: Reload | F: Fullscreen</p> <p>Navigate the maze, shoot enemies, avoid getting hit!</p> </div> <button id="fullscreenBtn" onclick="toggleFullscreen()">Toggle Fullscreen</button> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); let canvasWidth = canvas.width = canvas.parentElement.clientWidth; let canvasHeight = canvas.height = canvas.parentElement.clientHeight; const FOV = Math.PI / 3; // 60 degrees const MAX_DEPTH = 20; const WALL_HEIGHT = 1.0; const CELL_SIZE = 64; const PLAYER_HEIGHT = 0.5; const PLAYER_RADIUS = 0.2; const PLAYER_SPEED = 0.05; const PLAYER_TURN_SPEED = 0.04; const MOUSE_SENSITIVITY = 0.002; // Game state let player = { x: 1.5 * CELL_SIZE, y: 1.5 * CELL_SIZE, angle: 0, health: 100, ammo: 30, score: 0, movingForward: false, movingBackward: false, strafeLeft: false, strafeRight: false, turningLeft: false, turningRight: false, shooting: false, shootCooldown: 0, reloading: false, reloadTime: 0 }; // Maze grid (1 = wall, 0 = empty, 2 = enemy spawn) const MAP_SIZE = 16; let map = []; // Procedural generation function generateMaze() { map = []; // Fill with walls initially for (let y = 0; y < MAP_SIZE; y++) { map[y] = []; for (let x = 0; x < MAP_SIZE; x++) { // Border walls if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) { map[y][x] = 1; } else { // Random walls with some passages map[y][x] = Math.random() > 0.7 ? 1 : 0; } } } // Ensure player start area is clear const startX = Math.floor(player.x / CELL_SIZE); const startY = Math.floor(player.y / CELL_SIZE); for (let y = startY - 2; y <= startY + 2; y++) { for (let x = startX - 2; x <= startX + 2; x++) { if (x >= 0 && x < MAP_SIZE && y >= 0 && y < MAP_SIZE) { map[y][x] = 0; } } } // Add some enemy spawn points for (let i = 0; i < 8; i++) { let x, y; do { x = Math.floor(Math.random() * (MAP_SIZE - 4)) + 2; y = Math.floor(Math.random() * (MAP_SIZE - 4)) + 2; } while (map[y][x] !== 0 || (Math.abs(x - startX) < 4 && Math.abs(y - startY) < 4)); map[y][x] = 2; } } // Enemy class class Enemy { constructor(x, y) { this.x = x * CELL_SIZE + CELL_SIZE / 2; this.y = y * CELL_SIZE + CELL_SIZE / 2; this.health = 100; this.speed = 0.02; this.attackCooldown = 0; this.state = 'idle'; // idle, chasing, attacking this.angle = Math.random() * Math.PI * 2; this.textureId = Math.floor(Math.random() * 4); this.walkCycle = 0; } update(playerX, playerY) { const dx = playerX - this.x; const dy = playerY - this.y; const dist = Math.sqrt(dx * dx + dy * dy); const angleToPlayer = Math.atan2(dy, dx); if (dist < 300) { this.state = 'chasing'; // Move towards player this.angle = angleToPlayer; this.x += Math.cos(this.angle) * this.speed * CELL_SIZE; this.y += Math.sin(this.angle) * this.speed * CELL_SIZE; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (map[cellY][cellX] === 1) { // Bounce back this.x -= Math.cos(this.angle) * this.speed * CELL_SIZE * 2; this.y -= Math.sin(this.angle) * this.speed * CELL_SIZE * 2; this.angle += Math.PI / 2; } if (dist < 50) { this.state = 'attacking'; if (this.attackCooldown <= 0) { player.health -= 10; this.attackCooldown = 60; // ~1 second at 60fps } } } else { this.state = 'idle'; // Random wandering if (Math.random() < 0.02) { this.angle += (Math.random() - 0.5) * Math.PI; } this.x += Math.cos(this.angle) * this.speed * CELL_SIZE * 0.3; this.y += Math.sin(this.angle) * this.speed * CELL_SIZE * 0.3; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (map[cellY][cellX] === 1) { this.angle += Math.PI / 2; } } if (this.attackCooldown > 0) this.attackCooldown--; this.walkCycle = (this.walkCycle + 0.1) % (Math.PI * 2); } draw(ctx, x, y, width, height) { // Draw enemy sprite in 2D minimap ctx.fillStyle = '#f00'; ctx.fillRect(x - 3, y - 3, 6, 6); } } let enemies = []; // Projectile class class Projectile { constructor(x, y, angle) { this.x = x; this.y = y; this.angle = angle; this.speed = 10; this.distance = 0; this.maxDistance = 500; this.active = true; } update() { this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; this.distance += this.speed; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX] === 1) { this.active = false; return; } } // Check collision with enemies for (let i = enemies.length - 1; i >= 0; i--) { const enemy = enemies[i]; const dx = enemy.x - this.x; const dy = enemy.y - this.y; if (Math.sqrt(dx * dx + dy * dy) < 20) { enemy.health -= 35; this.active = false; if (enemy.health <= 0) { player.score += 100; enemies.splice(i, 1); } break; } } if (this.distance > this.maxDistance) this.active = false; } } let projectiles = []; // Texture generation const textures = []; function generateTextures() { const textureSize = 64; for (let t = 0; t < 4; t++) { const texture = document.createElement('canvas'); texture.width = textureSize; texture.height = textureSize; const tctx = texture.getContext('2d'); // Generate procedural wall texture const hue = Math.floor(Math.random() * 360); const saturation = 30 + Math.random() * 40; const lightness = 20 + Math.random() * 30; // Base color tctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`; tctx.fillRect(0, 0, textureSize, textureSize); // Add some noise/pattern tctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness + 10}%)`; for (let i = 0; i < 200; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); tctx.fillRect(x, y, 2, 2); } // Add brick-like pattern for some textures if (t < 2) { tctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness - 10}%)`; tctx.lineWidth = 2; // Horizontal lines for (let y = 0; y < textureSize; y += 16) { tctx.beginPath(); tctx.moveTo(0, y); tctx.lineTo(textureSize, y); tctx.stroke(); } // Vertical lines with offset every other row for (let y = 0; y < textureSize; y += 16) { for (let x = (y / 16) % 2 === 0 ? 0 : 16; x < textureSize; x += 32) { tctx.beginPath(); tctx.moveTo(x, y); tctx.lineTo(x, y + 16); tctx.stroke(); } } } textures.push(texture); } // Generate floor and ceiling textures const floorTexture = document.createElement('canvas'); floorTexture.width = textureSize; floorTexture.height = textureSize; const fctx = floorTexture.getContext('2d'); fctx.fillStyle = '#333'; fctx.fillRect(0, 0, textureSize, textureSize); fctx.fillStyle = '#444'; for (let i = 0; i < 50; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); fctx.fillRect(x, y, 4, 4); } textures.push(floorTexture); const ceilingTexture = document.createElement('canvas'); ceilingTexture.width = textureSize; ceilingTexture.height = textureSize; const cctx = ceilingTexture.getContext('2d'); cctx.fillStyle = '#111'; cctx.fillRect(0, 0, textureSize, textureSize); cctx.fillStyle = '#222'; for (let i = 0; i < 30; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); cctx.fillRect(x, y, 6, 2); } textures.push(ceilingTexture); } // Raycasting rendering function castRay(angle) { const rayCos = Math.cos(angle); const raySin = Math.sin(angle); // Player position in map coordinates let mapX = Math.floor(player.x / CELL_SIZE); let mapY = Math.floor(player.y / CELL_SIZE); // Ray direction const rayDirX = rayCos; const rayDirY = raySin; // Length of ray from current position to next x or y-side let deltaDistX = Math.abs(1 / rayDirX); let deltaDistY = Math.abs(1 / rayDirY); let perpWallDist; let stepX, stepY; let side; // 0 = x-side, 1 = y-side // Calculate step and initial sideDist if (rayDirX < 0) { stepX = -1; let playerCellX = player.x / CELL_SIZE; let sideDistX = (playerCellX - mapX) * deltaDistX; } else { stepX = 1; let playerCellX = player.x / CELL_SIZE; let sideDistX = (mapX + 1.0 - playerCellX) * deltaDistX; } if (rayDirY < 0) { stepY = -1; let playerCellY = player.y / CELL_SIZE; let sideDistY = (playerCellY - mapY) * deltaDistY; } else { stepY = 1; let playerCellY = player.y / CELL_SIZE; let sideDistY = (mapY + 1.0 - playerCellY) * deltaDistY; } // DDA algorithm let hit = 0; let sideDistX, sideDistY; // Recalculate sideDist properly if (rayDirX < 0) { sideDistX = (player.x / CELL_SIZE - mapX) * deltaDistX; } else { sideDistX = (mapX + 1.0 - player.x / CELL_SIZE) * deltaDistX; } if (rayDirY < 0) { sideDistY = (player.y / CELL_SIZE - mapY) * deltaDistY; } else { sideDistY = (mapY + 1.0 - player.y / CELL_SIZE) * deltaDistY; } while (hit === 0) { // Jump to next map square, either in x-direction, or in y-direction if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check if ray has hit a wall if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) { break; } if (map[mapY][mapX] === 1) hit = 1; } // Calculate distance projected on camera direction if (side === 0) { perpWallDist = (mapX - player.x / CELL_SIZE + (1 - stepX) / 2) / rayDirX; } else { perpWallDist = (mapY - player.y / CELL_SIZE + (1 - stepY) / 2) / rayDirY; } return { distance: perpWallDist, side: side, mapX: mapX, mapY: mapY, hit: hit }; } function draw3DView() { const wallWidth = canvasWidth; const wallHeight = canvasHeight; // Draw ceiling ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvasWidth, canvasHeight / 2); // Draw floor ctx.fillStyle = '#333'; ctx.fillRect(0, canvasHeight / 2, canvasWidth, canvasHeight / 2); // Raycasting for each column for (let x = 0; x < canvasWidth; x++) { // Calculate ray angle const cameraX = 2 * x / canvasWidth - 1; const rayAngle = player.angle + FOV / 2 * cameraX; const ray = castRay(rayAngle); if (ray.hit) { // Calculate line height const lineHeight = canvasHeight / ray.distance; // Calculate texture coordinates let wallX; if (ray.side === 0) { wallX = player.y / CELL_SIZE + ray.distance * Math.sin(rayAngle); } else { wallX = player.x / CELL_SIZE + ray.distance * Math.cos(rayAngle); } wallX -= Math.floor(wallX); // Select texture (based on map value and side) const texIndex = map[ray.mapY][ray.mapX] - 1; const texture = textures[texIndex >= 0 ? texIndex % textures.length : 0]; // Texture X coordinate const texX = Math.floor(wallX * texture.width); if (ray.side === 1) texX = texture.width - texX - 1; // Draw vertical stripe const drawStart = Math.max(0, -lineHeight / 2 + canvasHeight / 2); const drawEnd = Math.min(canvasHeight, lineHeight / 2 + canvasHeight / 2); // Sample texture and draw const texHeight = texture.height; const step = texHeight / lineHeight; let texPos = (drawStart - canvasHeight / 2 + lineHeight / 2) * step; for (let y = drawStart; y < drawEnd; y++) { const texY = Math.floor(texPos) % texHeight; const pixelData = texture.getContext('2d').getImageData(texX, texY, 1, 1).data; // Darken y-side walls for depth effect let r = pixelData[0]; let g = pixelData[1]; let b = pixelData[2]; if (ray.side === 1) { r = r * 0.7; g = g * 0.7; b = b * 0.7; } ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.fillRect(x, y, 1, 1); texPos += step; } } } // Draw enemies as sprites (billboarding) for (const enemy of enemies) { // Calculate angle between player and enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToEnemy = Math.atan2(dy, dx); const angleDiff = angleToEnemy - player.angle; // Normalize angle difference let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; // Check if enemy is within FOV if (Math.abs(normalizedAngleDiff) < FOV / 2) { // Calculate screen position const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const spriteHeight = canvasHeight / dist * CELL_SIZE * 0.8; const spriteWidth = spriteHeight; // Draw enemy sprite (simplified rectangle for now) const spriteY = canvasHeight / 2 - spriteHeight / 2; // Create enemy sprite with color ctx.fillStyle = `hsl(0, 80%, ${50 + Math.sin(enemy.walkCycle) * 10}%)`; ctx.fillRect(screenX - spriteWidth / 2, spriteY, spriteWidth, spriteHeight); // Draw health bar const healthWidth = spriteWidth * (enemy.health / 100); ctx.fillStyle = '#f00'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, spriteWidth, 4); ctx.fillStyle = '#0f0'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, healthWidth, 4); } } } // Draw projectiles ctx.fillStyle = '#ff0'; for (const proj of projectiles) { const dx = proj.x - player.x; const dy = proj.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToProj = Math.atan2(dy, dx); const angleDiff = angleToProj - player.angle; let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; if (Math.abs(normalizedAngleDiff) < FOV / 2) { const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const projSize = Math.max(2, 10 / dist); ctx.fillRect(screenX - projSize / 2, canvasHeight / 2 - projSize / 2, projSize, projSize); } } } // Draw weapon drawWeapon(); } function drawWeapon() { // Simple weapon rendering at bottom center const weaponWidth = canvasWidth * 0.3; const weaponHeight = canvasHeight * 0.4; const weaponX = canvasWidth / 2 - weaponWidth / 2; const weaponY = canvasHeight - weaponHeight + 20; // Weapon base ctx.fillStyle = '#444'; ctx.fillRect(weaponX, weaponY, weaponWidth, weaponHeight); // Barrel ctx.fillStyle = '#222'; ctx.fillRect(canvasWidth / 2 - 5, weaponY - 30, 10, 40); // Trigger ctx.fillStyle = '#666'; ctx.fillRect(canvasWidth / 2 + 20, weaponY + weaponHeight - 20, 10, 15); // Muzzle flash when shooting if (player.shooting && player.shootCooldown > 0) { const flashSize = 30 + Math.random() * 20; ctx.fillStyle = '#ff0'; ctx.fillRect(canvasWidth / 2 - flashSize / 2, weaponY - 60, flashSize, flashSize); } } function drawMiniMap() { const miniMapSize = 150; const cellSize = miniMapSize / MAP_SIZE; const offsetX = 10; const offsetY = canvasHeight - miniMapSize - 10; // Background ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(offsetX, offsetY, miniMapSize, miniMapSize); // Draw walls for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 1) { ctx.fillStyle = '#888'; ctx.fillRect(offsetX + x * cellSize, offsetY + y * cellSize, cellSize, cellSize); } } } // Draw enemies for (const enemy of enemies) { const mapX = enemy.x / CELL_SIZE; const mapY = enemy.y / CELL_SIZE; ctx.fillStyle = '#f00'; ctx.fillRect(offsetX + mapX * cellSize - 2, offsetY + mapY * cellSize - 2, 4, 4); } // Draw player ctx.fillStyle = '#0f0'; const playerMapX = player.x / CELL_SIZE; const playerMapY = player.y / CELL_SIZE; ctx.fillRect(offsetX + playerMapX * cellSize - 3, offsetY + playerMapY * cellSize - 3, 6, 6); // Draw player direction ctx.strokeStyle = '#0f0'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(offsetX + playerMapX * cellSize, offsetY + playerMapY * cellSize); ctx.lineTo( offsetX + playerMapX * cellSize + Math.cos(player.angle) * 15, offsetY + playerMapY * cellSize + Math.sin(player.angle) * 15 ); ctx.stroke(); } // Input handling const keys = {}; window.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; // Shooting with spacebar if (e.key === ' ' && player.ammo > 0 && player.shootCooldown <= 0 && !player.reloading) { player.shooting = true; player.shootCooldown = 10; player.ammo--; projectiles.push(new Projectile( player.x + Math.cos(player.angle) * 20, player.y + Math.sin(player.angle) * 20, player.angle )); } // Reload with R if (e.key === 'r' && player.ammo < 30 && !player.reloading) { player.reloading = true; player.reloadTime = 60; // 1 second } // Prevent arrow keys from scrolling if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(e.key.toLowerCase())) { e.preventDefault(); } }); window.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; if (e.key === ' ') { player.shooting = false; } }); // Mouse movement for looking let mouseLocked = false; let lastMouseX = 0; canvas.addEventListener('click', () => { canvas.requestPointerLock(); }); document.addEventListener('pointerlockchange', () => { mouseLocked = document.pointerLockElement === canvas; }); document.addEventListener('mousemove', (e) => { if (mouseLocked) { player.angle += e.movementX * MOUSE_SENSITIVITY; // Keep angle normalized player.angle %= Math.PI * 2; } lastMouseX = e.clientX; }); // Mouse click for shooting canvas.addEventListener('mousedown', (e) => { if (e.button === 0 && player.ammo > 0 && player.shootCooldown <= 0 && !player.reloading) { player.shooting = true; player.shootCooldown = 10; player.ammo--; projectiles.push(new Projectile( player.x + Math.cos(player.angle) * 20, player.y + Math.sin(player.angle) * 20, player.angle )); } }); canvas.addEventListener('mouseup', (e) => { if (e.button === 0) { player.shooting = false; } }); // Update game state function update() { // Player movement const moveSpeed = PLAYER_SPEED * CELL_SIZE; const turnSpeed = PLAYER_TURN_SPEED; if (keys['w'] || keys['arrowup']) { player.x += Math.cos(player.angle) * moveSpeed; player.y += Math.sin(player.angle) * moveSpeed; checkCollision(); } if (keys['s'] || keys['arrowdown']) { player.x -= Math.cos(player.angle) * moveSpeed; player.y -= Math.sin(player.angle) * moveSpeed; checkCollision(); } if (keys['a']) { // Strafe left player.x += Math.cos(player.angle - Math.PI / 2) * moveSpeed * 0.7; player.y += Math.sin(player.angle - Math.PI / 2) * moveSpeed * 0.7; checkCollision(); } if (keys['d']) { // Strafe right player.x += Math.cos(player.angle + Math.PI / 2) * moveSpeed * 0.7; player.y += Math.sin(player.angle + Math.PI / 2) * moveSpeed * 0.7; checkCollision(); } if (keys['arrowleft']) { player.angle -= turnSpeed; } if (keys['arrowright']) { player.angle += turnSpeed; } // Keep angle normalized player.angle %= Math.PI * 2; // Update cooldowns if (player.shootCooldown > 0) player.shootCooldown--; if (player.reloading) { player.reloadTime--; if (player.reloadTime <= 0) { player.ammo = 30; player.reloading = false; } } // Update projectiles for (let i = projectiles.length - 1; i >= 0; i--) { projectiles[i].update(); if (!projectiles[i].active) { projectiles.splice(i, 1); } } // Update enemies for (const enemy of enemies) { enemy.update(player.x, player.y); } // Spawn new enemies if needed if (enemies.length < 5 && Math.random() < 0.005) { // Find a spawn point for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 2) { // Check distance from player const dist = Math.sqrt( Math.pow(player.x - (x * CELL_SIZE + CELL_SIZE / 2), 2) + Math.pow(player.y - (y * CELL_SIZE + CELL_SIZE / 2), 2) ); if (dist > 200) { enemies.push(new Enemy(x, y)); break; } } } } } // Update UI document.getElementById('pos').textContent = `${Math.floor(player.x)}, ${Math.floor(player.y)}`; document.getElementById('angle').textContent = `${(player.angle * 180 / Math.PI).toFixed(1)}°`; document.getElementById('healthValue').textContent = player.health; document.getElementById('ammoValue').textContent = player.ammo; document.getElementById('scoreValue').textContent = player.score; // Check game over if (player.health <= 0) { gameOver(); } } function checkCollision() { const cellX = Math.floor(player.x / CELL_SIZE); const cellY = Math.floor(player.y / CELL_SIZE); // Check collision with walls if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX] === 1) { // Push player back player.x -= Math.cos(player.angle) * PLAYER_SPEED * CELL_SIZE * 2; player.y -= Math.sin(player.angle) * PLAYER_SPEED * CELL_SIZE * 2; } } // Keep player within bounds player.x = Math.max(PLAYER_RADIUS * CELL_SIZE, Math.min(player.x, MAP_SIZE * CELL_SIZE - PLAYER_RADIUS * CELL_SIZE)); player.y = Math.max(PLAYER_RADIUS * CELL_SIZE, Math.min(player.y, MAP_SIZE * CELL_SIZE - PLAYER_RADIUS * CELL_SIZE)); } function gameOver() { document.getElementById('gameOver').style.display = 'block'; document.getElementById('finalScore').textContent = player.score; } function resetGame() { player = { x: 1.5 * CELL_SIZE, y: 1.5 * CELL_SIZE, angle: 0, health: 100, ammo: 30, score: 0, movingForward: false, movingBackward: false, strafeLeft: false, strafeRight: false, turningLeft: false, turningRight: false, shooting: false, shootCooldown: 0, reloading: false, reloadTime: 0 }; enemies = []; projectiles = []; generateMaze(); spawnEnemies(); document.getElementById('gameOver').style.display = 'none'; } function spawnEnemies() { for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 2) { // Only spawn if far from player const dist = Math.sqrt( Math.pow(player.x - (x * CELL_SIZE + CELL_SIZE / 2), 2) + Math.pow(player.y - (y * CELL_SIZE + CELL_SIZE / 2), 2) ); if (dist > 200) { enemies.push(new Enemy(x, y)); } } } } } // Fullscreen toggle function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(console.log); } else { document.exitFullscreen(); } } // Resize handling window.addEventListener('resize', () => { canvasWidth = canvas.width = canvas.parentElement.clientWidth; canvasHeight = canvas.height = canvas.parentElement.clientHeight; }); // Game loop let lastTime = 0; let fps = 0; let frameCount = 0; let lastFpsUpdate = 0; function gameLoop(timestamp) { const deltaTime = timestamp - lastTime; lastTime = timestamp; // Update FPS counter frameCount++; if (timestamp - lastFpsUpdate >= 1000) { fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate)); document.getElementById('fps').textContent = fps; frameCount = 0; lastFpsUpdate = timestamp; } update(); // Clear canvas ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Draw 3D view draw3DView(); // Draw minimap drawMiniMap(); requestAnimationFrame(gameLoop); } // Initialize function init() { generateTextures(); generateMaze(); spawnEnemies(); requestAnimationFrame(gameLoop); } window.addEventListener('load', init); </script> </body> </html>

assistant
thinking
Looks good. However, there are a few issues: the raycasting function has some variable scoping issues (sideDistX and sideDistY declared twice) and the DDA algorithm may not be perfect. Also, the texture mapping might be off. But overall it's a complete self-contained game.

Let's fix the raycasting function to be more correct. We'll rewrite the castRay function to be simpler and more accurate. Also we need to ensure the texture sampling works.

I'll edit the index.html file to fix the raycasting function and a few other bugs. Let's do a targeted edit. First, find the castRay function. Let's read the relevant section:
read {"path":"index.html"}
toolResult

toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wolfenstein 3D Style Raycasting Maze Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', monospace; } body { background: #000; color: #fff; overflow: hidden; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } #gameContainer { position: relative; width: 100%; max-width: 1024px; height: 80vh; margin: 0 auto; } canvas { display: block; background: black; width: 100%; height: 100%; image-rendering: pixelated; image-rendering: crisp-edges; } #ui { position: absolute; top: 10px; left: 10px; color: #ff0; font-size: 16px; text-shadow: 2px 2px 0 #000; z-index: 10; pointer-events: none; } #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #f00; font-size: 24px; z-index: 10; pointer-events: none; } #hud { position: absolute; bottom: 10px; left: 10px; right: 10px; display: flex; justify-content: space-between; align-items: flex-end; color: #fff; font-size: 18px; text-shadow: 2px 2px 0 #000; z-index: 10; pointer-events: none; } .health, .ammo, .score { background: rgba(0, 0, 0, 0.6); padding: 5px 10px; border-radius: 5px; border: 2px solid #444; } #instructions { margin-top: 10px; text-align: center; color: #aaa; font-size: 14px; max-width: 800px; line-height: 1.4; } #fullscreenBtn { margin-top: 10px; padding: 8px 16px; background: #333; color: #fff; border: 2px solid #666; border-radius: 5px; cursor: pointer; font-size: 14px; transition: all 0.2s; } #fullscreenBtn:hover { background: #555; } #gameOver { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.85); color: #f00; padding: 40px; border-radius: 10px; border: 4px solid #f00; text-align: center; z-index: 100; display: none; } #gameOver h2 { font-size: 48px; margin-bottom: 20px; } #gameOver button { padding: 15px 30px; background: #333; color: #fff; border: 2px solid #f00; border-radius: 5px; cursor: pointer; font-size: 20px; transition: all 0.2s; } #gameOver button:hover { background: #555; } </style> </head> <body> <h1>Wolfenstein 3D Style Raycasting Shooter</h1> <div id="gameContainer"> <canvas id="gameCanvas"></canvas> <div id="ui"> <div>FPS: <span id="fps">0</span></div> <div>Pos: <span id="pos">0,0</span></div> <div>Angle: <span id="angle">0</span></div> </div> <div id="crosshair">+</div> <div id="hud"> <div class="health">Health: <span id="healthValue">100</span></div> <div class="ammo">Ammo: <span id="ammoValue">30</span></div> <div class="score">Score: <span id="scoreValue">0</span></div> </div> <div id="gameOver"> <h2>GAME OVER</h2> <p>You have been defeated!</p> <p>Final Score: <span id="finalScore">0</span></p> <button onclick="resetGame()">PLAY AGAIN</button> </div> </div> <div id="instructions"> <p>WASD: Move | Mouse/Arrows: Turn | Left Click: Shoot | R: Reload | F: Fullscreen</p> <p>Navigate the maze, shoot enemies, avoid getting hit!</p> </div> <button id="fullscreenBtn" onclick="toggleFullscreen()">Toggle Fullscreen</button> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); let canvasWidth = canvas.width = canvas.parentElement.clientWidth; let canvasHeight = canvas.height = canvas.parentElement.clientHeight; const FOV = Math.PI / 3; // 60 degrees const MAX_DEPTH = 20; const WALL_HEIGHT = 1.0; const CELL_SIZE = 64; const PLAYER_HEIGHT = 0.5; const PLAYER_RADIUS = 0.2; const PLAYER_SPEED = 0.05; const PLAYER_TURN_SPEED = 0.04; const MOUSE_SENSITIVITY = 0.002; // Game state let player = { x: 1.5 * CELL_SIZE, y: 1.5 * CELL_SIZE, angle: 0, health: 100, ammo: 30, score: 0, movingForward: false, movingBackward: false, strafeLeft: false, strafeRight: false, turningLeft: false, turningRight: false, shooting: false, shootCooldown: 0, reloading: false, reloadTime: 0 }; // Maze grid (1 = wall, 0 = empty, 2 = enemy spawn) const MAP_SIZE = 16; let map = []; // Procedural generation function generateMaze() { map = []; // Fill with walls initially for (let y = 0; y < MAP_SIZE; y++) { map[y] = []; for (let x = 0; x < MAP_SIZE; x++) { // Border walls if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) { map[y][x] = 1; } else { // Random walls with some passages map[y][x] = Math.random() > 0.7 ? 1 : 0; } } } // Ensure player start area is clear const startX = Math.floor(player.x / CELL_SIZE); const startY = Math.floor(player.y / CELL_SIZE); for (let y = startY - 2; y <= startY + 2; y++) { for (let x = startX - 2; x <= startX + 2; x++) { if (x >= 0 && x < MAP_SIZE && y >= 0 && y < MAP_SIZE) { map[y][x] = 0; } } } // Add some enemy spawn points for (let i = 0; i < 8; i++) { let x, y; do { x = Math.floor(Math.random() * (MAP_SIZE - 4)) + 2; y = Math.floor(Math.random() * (MAP_SIZE - 4)) + 2; } while (map[y][x] !== 0 || (Math.abs(x - startX) < 4 && Math.abs(y - startY) < 4)); map[y][x] = 2; } } // Enemy class class Enemy { constructor(x, y) { this.x = x * CELL_SIZE + CELL_SIZE / 2; this.y = y * CELL_SIZE + CELL_SIZE / 2; this.health = 100; this.speed = 0.02; this.attackCooldown = 0; this.state = 'idle'; // idle, chasing, attacking this.angle = Math.random() * Math.PI * 2; this.textureId = Math.floor(Math.random() * 4); this.walkCycle = 0; } update(playerX, playerY) { const dx = playerX - this.x; const dy = playerY - this.y; const dist = Math.sqrt(dx * dx + dy * dy); const angleToPlayer = Math.atan2(dy, dx); if (dist < 300) { this.state = 'chasing'; // Move towards player this.angle = angleToPlayer; this.x += Math.cos(this.angle) * this.speed * CELL_SIZE; this.y += Math.sin(this.angle) * this.speed * CELL_SIZE; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (map[cellY][cellX] === 1) { // Bounce back this.x -= Math.cos(this.angle) * this.speed * CELL_SIZE * 2; this.y -= Math.sin(this.angle) * this.speed * CELL_SIZE * 2; this.angle += Math.PI / 2; } if (dist < 50) { this.state = 'attacking'; if (this.attackCooldown <= 0) { player.health -= 10; this.attackCooldown = 60; // ~1 second at 60fps } } } else { this.state = 'idle'; // Random wandering if (Math.random() < 0.02) { this.angle += (Math.random() - 0.5) * Math.PI; } this.x += Math.cos(this.angle) * this.speed * CELL_SIZE * 0.3; this.y += Math.sin(this.angle) * this.speed * CELL_SIZE * 0.3; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (map[cellY][cellX] === 1) { this.angle += Math.PI / 2; } } if (this.attackCooldown > 0) this.attackCooldown--; this.walkCycle = (this.walkCycle + 0.1) % (Math.PI * 2); } draw(ctx, x, y, width, height) { // Draw enemy sprite in 2D minimap ctx.fillStyle = '#f00'; ctx.fillRect(x - 3, y - 3, 6, 6); } } let enemies = []; // Projectile class class Projectile { constructor(x, y, angle) { this.x = x; this.y = y; this.angle = angle; this.speed = 10; this.distance = 0; this.maxDistance = 500; this.active = true; } update() { this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; this.distance += this.speed; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX] === 1) { this.active = false; return; } } // Check collision with enemies for (let i = enemies.length - 1; i >= 0; i--) { const enemy = enemies[i]; const dx = enemy.x - this.x; const dy = enemy.y - this.y; if (Math.sqrt(dx * dx + dy * dy) < 20) { enemy.health -= 35; this.active = false; if (enemy.health <= 0) { player.score += 100; enemies.splice(i, 1); } break; } } if (this.distance > this.maxDistance) this.active = false; } } let projectiles = []; // Texture generation const textures = []; function generateTextures() { const textureSize = 64; for (let t = 0; t < 4; t++) { const texture = document.createElement('canvas'); texture.width = textureSize; texture.height = textureSize; const tctx = texture.getContext('2d'); // Generate procedural wall texture const hue = Math.floor(Math.random() * 360); const saturation = 30 + Math.random() * 40; const lightness = 20 + Math.random() * 30; // Base color tctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`; tctx.fillRect(0, 0, textureSize, textureSize); // Add some noise/pattern tctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness + 10}%)`; for (let i = 0; i < 200; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); tctx.fillRect(x, y, 2, 2); } // Add brick-like pattern for some textures if (t < 2) { tctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness - 10}%)`; tctx.lineWidth = 2; // Horizontal lines for (let y = 0; y < textureSize; y += 16) { tctx.beginPath(); tctx.moveTo(0, y); tctx.lineTo(textureSize, y); tctx.stroke(); } // Vertical lines with offset every other row for (let y = 0; y < textureSize; y += 16) { for (let x = (y / 16) % 2 === 0 ? 0 : 16; x < textureSize; x += 32) { tctx.beginPath(); tctx.moveTo(x, y); tctx.lineTo(x, y + 16); tctx.stroke(); } } } textures.push(texture); } // Generate floor and ceiling textures const floorTexture = document.createElement('canvas'); floorTexture.width = textureSize; floorTexture.height = textureSize; const fctx = floorTexture.getContext('2d'); fctx.fillStyle = '#333'; fctx.fillRect(0, 0, textureSize, textureSize); fctx.fillStyle = '#444'; for (let i = 0; i < 50; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); fctx.fillRect(x, y, 4, 4); } textures.push(floorTexture); const ceilingTexture = document.createElement('canvas'); ceilingTexture.width = textureSize; ceilingTexture.height = textureSize; const cctx = ceilingTexture.getContext('2d'); cctx.fillStyle = '#111'; cctx.fillRect(0, 0, textureSize, textureSize); cctx.fillStyle = '#222'; for (let i = 0; i < 30; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); cctx.fillRect(x, y, 6, 2); } textures.push(ceilingTexture); } // Raycasting rendering function castRay(angle) { const rayCos = Math.cos(angle); const raySin = Math.sin(angle); // Player position in map coordinates let mapX = Math.floor(player.x / CELL_SIZE); let mapY = Math.floor(player.y / CELL_SIZE); // Ray direction const rayDirX = rayCos; const rayDirY = raySin; // Length of ray from current position to next x or y-side let deltaDistX = Math.abs(1 / rayDirX); let deltaDistY = Math.abs(1 / rayDirY); let perpWallDist; let stepX, stepY; let side; // 0 = x-side, 1 = y-side // Calculate step and initial sideDist if (rayDirX < 0) { stepX = -1; let playerCellX = player.x / CELL_SIZE; let sideDistX = (playerCellX - mapX) * deltaDistX; } else { stepX = 1; let playerCellX = player.x / CELL_SIZE; let sideDistX = (mapX + 1.0 - playerCellX) * deltaDistX; } if (rayDirY < 0) { stepY = -1; let playerCellY = player.y / CELL_SIZE; let sideDistY = (playerCellY - mapY) * deltaDistY; } else { stepY = 1; let playerCellY = player.y / CELL_SIZE; let sideDistY = (mapY + 1.0 - playerCellY) * deltaDistY; } // DDA algorithm let hit = 0; let sideDistX, sideDistY; // Recalculate sideDist properly if (rayDirX < 0) { sideDistX = (player.x / CELL_SIZE - mapX) * deltaDistX; } else { sideDistX = (mapX + 1.0 - player.x / CELL_SIZE) * deltaDistX; } if (rayDirY < 0) { sideDistY = (player.y / CELL_SIZE - mapY) * deltaDistY; } else { sideDistY = (mapY + 1.0 - player.y / CELL_SIZE) * deltaDistY; } while (hit === 0) { // Jump to next map square, either in x-direction, or in y-direction if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check if ray has hit a wall if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) { break; } if (map[mapY][mapX] === 1) hit = 1; } // Calculate distance projected on camera direction if (side === 0) { perpWallDist = (mapX - player.x / CELL_SIZE + (1 - stepX) / 2) / rayDirX; } else { perpWallDist = (mapY - player.y / CELL_SIZE + (1 - stepY) / 2) / rayDirY; } return { distance: perpWallDist, side: side, mapX: mapX, mapY: mapY, hit: hit }; } function draw3DView() { const wallWidth = canvasWidth; const wallHeight = canvasHeight; // Draw ceiling ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvasWidth, canvasHeight / 2); // Draw floor ctx.fillStyle = '#333'; ctx.fillRect(0, canvasHeight / 2, canvasWidth, canvasHeight / 2); // Raycasting for each column for (let x = 0; x < canvasWidth; x++) { // Calculate ray angle const cameraX = 2 * x / canvasWidth - 1; const rayAngle = player.angle + FOV / 2 * cameraX; const ray = castRay(rayAngle); if (ray.hit) { // Calculate line height const lineHeight = canvasHeight / ray.distance; // Calculate texture coordinates let wallX; if (ray.side === 0) { wallX = player.y / CELL_SIZE + ray.distance * Math.sin(rayAngle); } else { wallX = player.x / CELL_SIZE + ray.distance * Math.cos(rayAngle); } wallX -= Math.floor(wallX); // Select texture (based on map value and side) const texIndex = map[ray.mapY][ray.mapX] - 1; const texture = textures[texIndex >= 0 ? texIndex % textures.length : 0]; // Texture X coordinate const texX = Math.floor(wallX * texture.width); if (ray.side === 1) texX = texture.width - texX - 1; // Draw vertical stripe const drawStart = Math.max(0, -lineHeight / 2 + canvasHeight / 2); const drawEnd = Math.min(canvasHeight, lineHeight / 2 + canvasHeight / 2); // Sample texture and draw const texHeight = texture.height; const step = texHeight / lineHeight; let texPos = (drawStart - canvasHeight / 2 + lineHeight / 2) * step; for (let y = drawStart; y < drawEnd; y++) { const texY = Math.floor(texPos) % texHeight; const pixelData = texture.getContext('2d').getImageData(texX, texY, 1, 1).data; // Darken y-side walls for depth effect let r = pixelData[0]; let g = pixelData[1]; let b = pixelData[2]; if (ray.side === 1) { r = r * 0.7; g = g * 0.7; b = b * 0.7; } ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.fillRect(x, y, 1, 1); texPos += step; } } } // Draw enemies as sprites (billboarding) for (const enemy of enemies) { // Calculate angle between player and enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToEnemy = Math.atan2(dy, dx); const angleDiff = angleToEnemy - player.angle; // Normalize angle difference let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; // Check if enemy is within FOV if (Math.abs(normalizedAngleDiff) < FOV / 2) { // Calculate screen position const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const spriteHeight = canvasHeight / dist * CELL_SIZE * 0.8; const spriteWidth = spriteHeight; // Draw enemy sprite (simplified rectangle for now) const spriteY = canvasHeight / 2 - spriteHeight / 2; // Create enemy sprite with color ctx.fillStyle = `hsl(0, 80%, ${50 + Math.sin(enemy.walkCycle) * 10}%)`; ctx.fillRect(screenX - spriteWidth / 2, spriteY, spriteWidth, spriteHeight); // Draw health bar const healthWidth = spriteWidth * (enemy.health / 100); ctx.fillStyle = '#f00'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, spriteWidth, 4); ctx.fillStyle = '#0f0'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, healthWidth, 4); } } } // Draw projectiles ctx.fillStyle = '#ff0'; for (const proj of projectiles) { const dx = proj.x - player.x; const dy = proj.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToProj = Math.atan2(dy, dx); const angleDiff = angleToProj - player.angle; let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; if (Math.abs(normalizedAngleDiff) < FOV / 2) { const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const projSize = Math.max(2, 10 / dist); ctx.fillRect(screenX - projSize / 2, canvasHeight / 2 - projSize / 2, projSize, projSize); } } } // Draw weapon drawWeapon(); } function drawWeapon() { // Simple weapon rendering at bottom center const weaponWidth = canvasWidth * 0.3; const weaponHeight = canvasHeight * 0.4; const weaponX = canvasWidth / 2 - weaponWidth / 2; const weaponY = canvasHeight - weaponHeight + 20; // Weapon base ctx.fillStyle = '#444'; ctx.fillRect(weaponX, weaponY, weaponWidth, weaponHeight); // Barrel ctx.fillStyle = '#222'; ctx.fillRect(canvasWidth / 2 - 5, weaponY - 30, 10, 40); // Trigger ctx.fillStyle = '#666'; ctx.fillRect(canvasWidth / 2 + 20, weaponY + weaponHeight - 20, 10, 15); // Muzzle flash when shooting if (player.shooting && player.shootCooldown > 0) { const flashSize = 30 + Math.random() * 20; ctx.fillStyle = '#ff0'; ctx.fillRect(canvasWidth / 2 - flashSize / 2, weaponY - 60, flashSize, flashSize); } } function drawMiniMap() { const miniMapSize = 150; const cellSize = miniMapSize / MAP_SIZE; const offsetX = 10; const offsetY = canvasHeight - miniMapSize - 10; // Background ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(offsetX, offsetY, miniMapSize, miniMapSize); // Draw walls for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 1) { ctx.fillStyle = '#888'; ctx.fillRect(offsetX + x * cellSize, offsetY + y * cellSize, cellSize, cellSize); } } } // Draw enemies for (const enemy of enemies) { const mapX = enemy.x / CELL_SIZE; const mapY = enemy.y / CELL_SIZE; ctx.fillStyle = '#f00'; ctx.fillRect(offsetX + mapX * cellSize - 2, offsetY + mapY * cellSize - 2, 4, 4); } // Draw player ctx.fillStyle = '#0f0'; const playerMapX = player.x / CELL_SIZE; const playerMapY = player.y / CELL_SIZE; ctx.fillRect(offsetX + playerMapX * cellSize - 3, offsetY + playerMapY * cellSize - 3, 6, 6); // Draw player direction ctx.strokeStyle = '#0f0'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(offsetX + playerMapX * cellSize, offsetY + playerMapY * cellSize); ctx.lineTo( offsetX + playerMapX * cellSize + Math.cos(player.angle) * 15, offsetY + playerMapY * cellSize + Math.sin(player.angle) * 15 ); ctx.stroke(); } // Input handling const keys = {}; window.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; // Shooting with spacebar if (e.key === ' ' && player.ammo > 0 && player.shootCooldown <= 0 && !player.reloading) { player.shooting = true; player.shootCooldown = 10; player.ammo--; projectiles.push(new Projectile( player.x + Math.cos(player.angle) * 20, player.y + Math.sin(player.angle) * 20, player.angle )); } // Reload with R if (e.key === 'r' && player.ammo < 30 && !player.reloading) { player.reloading = true; player.reloadTime = 60; // 1 second } // Prevent arrow keys from scrolling if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(e.key.toLowerCase())) { e.preventDefault(); } }); window.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; if (e.key === ' ') { player.shooting = false; } }); // Mouse movement for looking let mouseLocked = false; let lastMouseX = 0; canvas.addEventListener('click', () => { canvas.requestPointerLock(); }); document.addEventListener('pointerlockchange', () => { mouseLocked = document.pointerLockElement === canvas; }); document.addEventListener('mousemove', (e) => { if (mouseLocked) { player.angle += e.movementX * MOUSE_SENSITIVITY; // Keep angle normalized player.angle %= Math.PI * 2; } lastMouseX = e.clientX; }); // Mouse click for shooting canvas.addEventListener('mousedown', (e) => { if (e.button === 0 && player.ammo > 0 && player.shootCooldown <= 0 && !player.reloading) { player.shooting = true; player.shootCooldown = 10; player.ammo--; projectiles.push(new Projectile( player.x + Math.cos(player.angle) * 20, player.y + Math.sin(player.angle) * 20, player.angle )); } }); canvas.addEventListener('mouseup', (e) => { if (e.button === 0) { player.shooting = false; } }); // Update game state function update() { // Player movement const moveSpeed = PLAYER_SPEED * CELL_SIZE; const turnSpeed = PLAYER_TURN_SPEED; if (keys['w'] || keys['arrowup']) { player.x += Math.cos(player.angle) * moveSpeed; player.y += Math.sin(player.angle) * moveSpeed; checkCollision(); } if (keys['s'] || keys['arrowdown']) { player.x -= Math.cos(player.angle) * moveSpeed; player.y -= Math.sin(player.angle) * moveSpeed; checkCollision(); } if (keys['a']) { // Strafe left player.x += Math.cos(player.angle - Math.PI / 2) * moveSpeed * 0.7; player.y += Math.sin(player.angle - Math.PI / 2) * moveSpeed * 0.7; checkCollision(); } if (keys['d']) { // Strafe right player.x += Math.cos(player.angle + Math.PI / 2) * moveSpeed * 0.7; player.y += Math.sin(player.angle + Math.PI / 2) * moveSpeed * 0.7; checkCollision(); } if (keys['arrowleft']) { player.angle -= turnSpeed; } if (keys['arrowright']) { player.angle += turnSpeed; } // Keep angle normalized player.angle %= Math.PI * 2; // Update cooldowns if (player.shootCooldown > 0) player.shootCooldown--; if (player.reloading) { player.reloadTime--; if (player.reloadTime <= 0) { player.ammo = 30; player.reloading = false; } } // Update projectiles for (let i = projectiles.length - 1; i >= 0; i--) { projectiles[i].update(); if (!projectiles[i].active) { projectiles.splice(i, 1); } } // Update enemies for (const enemy of enemies) { enemy.update(player.x, player.y); } // Spawn new enemies if needed if (enemies.length < 5 && Math.random() < 0.005) { // Find a spawn point for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 2) { // Check distance from player const dist = Math.sqrt( Math.pow(player.x - (x * CELL_SIZE + CELL_SIZE / 2), 2) + Math.pow(player.y - (y * CELL_SIZE + CELL_SIZE / 2), 2) ); if (dist > 200) { enemies.push(new Enemy(x, y)); break; } } } } } // Update UI document.getElementById('pos').textContent = `${Math.floor(player.x)}, ${Math.floor(player.y)}`; document.getElementById('angle').textContent = `${(player.angle * 180 / Math.PI).toFixed(1)}°`; document.getElementById('healthValue').textContent = player.health; document.getElementById('ammoValue').textContent = player.ammo; document.getElementById('scoreValue').textContent = player.score; // Check game over if (player.health <= 0) { gameOver(); } } function checkCollision() { const cellX = Math.floor(player.x / CELL_SIZE); const cellY = Math.floor(player.y / CELL_SIZE); // Check collision with walls if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX] === 1) { // Push player back player.x -= Math.cos(player.angle) * PLAYER_SPEED * CELL_SIZE * 2; player.y -= Math.sin(player.angle) * PLAYER_SPEED * CELL_SIZE * 2; } } // Keep player within bounds player.x = Math.max(PLAYER_RADIUS * CELL_SIZE, Math.min(player.x, MAP_SIZE * CELL_SIZE - PLAYER_RADIUS * CELL_SIZE)); player.y = Math.max(PLAYER_RADIUS * CELL_SIZE, Math.min(player.y, MAP_SIZE * CELL_SIZE - PLAYER_RADIUS * CELL_SIZE)); } function gameOver() { document.getElementById('gameOver').style.display = 'block'; document.getElementById('finalScore').textContent = player.score; } function resetGame() { player = { x: 1.5 * CELL_SIZE, y: 1.5 * CELL_SIZE, angle: 0, health: 100, ammo: 30, score: 0, movingForward: false, movingBackward: false, strafeLeft: false, strafeRight: false, turningLeft: false, turningRight: false, shooting: false, shootCooldown: 0, reloading: false, reloadTime: 0 }; enemies = []; projectiles = []; generateMaze(); spawnEnemies(); document.getElementById('gameOver').style.display = 'none'; } function spawnEnemies() { for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 2) { // Only spawn if far from player const dist = Math.sqrt( Math.pow(player.x - (x * CELL_SIZE + CELL_SIZE / 2), 2) + Math.pow(player.y - (y * CELL_SIZE + CELL_SIZE / 2), 2) ); if (dist > 200) { enemies.push(new Enemy(x, y)); } } } } } // Fullscreen toggle function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(console.log); } else { document.exitFullscreen(); } } // Resize handling window.addEventListener('resize', () => { canvasWidth = canvas.width = canvas.parentElement.clientWidth; canvasHeight = canvas.height = canvas.parentElement.clientHeight; }); // Game loop let lastTime = 0; let fps = 0; let frameCount = 0; let lastFpsUpdate = 0; function gameLoop(timestamp) { const deltaTime = timestamp - lastTime; lastTime = timestamp; // Update FPS counter frameCount++; if (timestamp - lastFpsUpdate >= 1000) { fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate)); document.getElementById('fps').textContent = fps; frameCount = 0; lastFpsUpdate = timestamp; } update(); // Clear canvas ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Draw 3D view draw3DView(); // Draw minimap drawMiniMap(); requestAnimationFrame(gameLoop); } // Initialize function init() { generateTextures(); generateMaze(); spawnEnemies(); requestAnimationFrame(gameLoop); } window.addEventListener('load', init); </script> </body> </html>

assistant
thinking
Now I need to replace the castRay function with a corrected version. Also fix the texture indexing (since map values are 1 for wall, but texture array index should be 0 for first texture). Actually we have 4 wall textures, floor and ceiling textures separate. We'll keep as is but ensure we don't index out of bounds.

Let's edit the file with a new castRay function and also fix the texture selection. We'll also improve the wallX calculation. Let's do a multi-edit. First, locate the castRay function and the draw3DView function. Let's replace both with improved versions. We'll also adjust the texture generation to ensure we have enough textures for walls (4) and floor/ceiling separate.

I'll write the edit now
edit {"edits":[{"newText":" // Raycasting rendering\n function castRay(angle) {\n const rayDirX = Math.cos(angle);\n
toolResult

toolResult

Successfully replaced 3 block(s) in index.html.

assistant
thinking
Now we also need to fix the enemy sprite rendering to be more visible (maybe draw a simple shape). Also need to add a missing fullscreen button key handler (F key). Let's add that to the keydown event. Also ensure mouse look works with vertical movement? Not needed for Wolfenstein style.

Let's add F key for fullscreen and maybe improve instructions. Also fix the crosshair visibility (it's red plus sign). Might need to adjust z-index.

We'll also fix the collision detection so player doesn't get stuck when moving diagonally into corners.

Let's edit the keydown section to add F key. Also adjust the instructions paragraph. Let's do another edit. First find the keydown event listener. We'll replace that block. Let's also add a check for 'f' key. Let's read a bit more to locate the exact lines. Use grep? Let's just open the file and search. We'll do a bash to find line numbers. But easier: we'll edit by looking at the content we have. Let's extract the keydown event listener block. We'll replace it with updated version that includes F key and also prevent default for 'f'.

We'll also add a keyup for 'f' maybe not needed.

Let's do another edit. We'll also adjust the instructions text to mention F for fullscreen (already there). Good.

Now we also need to ensure the game loop doesn't continue when game over? Actually we can keep it running but stop player updates? We'll keep as is, but the game over screen will block input.

Let's also add a simple sound effect for shooting? Not required but could be fun with Web Audio. However requirement says no external resources; we can generate sounds procedurally. But maybe skip for now.

Let's also fix the enemy sprite drawing: we can draw a simple enemy shape using a circle or a rectangle with eyes. We'll keep it simple.

Now we need to ensure the raycasting works with the new texture indexing (0-3). We changed to (ray.mapX + ray.mapY) % 4. That's fine.

Now let's make sure the floor and ceiling textures are not used for walls (they are separate indices 4 and 5). We'll keep them unused for now but we could implement floor and ceiling texturing later. Not needed.

Let's write a final edit to improve enemy drawing and add F key. Let's find the enemy drawing part in draw3DView. We'll replace the rectangle with a more detailed sprite using a small canvas pattern. But maybe keep simple.

Let's first add F key handling. We'll edit the keydown event listener block. Let's locate the exact oldText. We'll search for "window.addEventListener('keydown'". Let's do a quick read with offset. Let's use bash to get lines.
bash {"command":"grep -n \"window.addEventListener('keydown'\" index.html"}
toolResult

toolResult

725: window.addEventListener('keydown', (e) => {

assistant
thinking
Let's read lines around 725. Let's read from 720 to 760
read {"path":"index.html"}
toolResult

toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wolfenstein 3D Style Raycasting Maze Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', monospace; } body { background: #000; color: #fff; overflow: hidden; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } #gameContainer { position: relative; width: 100%; max-width: 1024px; height: 80vh; margin: 0 auto; } canvas { display: block; background: black; width: 100%; height: 100%; image-rendering: pixelated; image-rendering: crisp-edges; } #ui { position: absolute; top: 10px; left: 10px; color: #ff0; font-size: 16px; text-shadow: 2px 2px 0 #000; z-index: 10; pointer-events: none; } #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #f00; font-size: 24px; z-index: 10; pointer-events: none; } #hud { position: absolute; bottom: 10px; left: 10px; right: 10px; display: flex; justify-content: space-between; align-items: flex-end; color: #fff; font-size: 18px; text-shadow: 2px 2px 0 #000; z-index: 10; pointer-events: none; } .health, .ammo, .score { background: rgba(0, 0, 0, 0.6); padding: 5px 10px; border-radius: 5px; border: 2px solid #444; } #instructions { margin-top: 10px; text-align: center; color: #aaa; font-size: 14px; max-width: 800px; line-height: 1.4; } #fullscreenBtn { margin-top: 10px; padding: 8px 16px; background: #333; color: #fff; border: 2px solid #666; border-radius: 5px; cursor: pointer; font-size: 14px; transition: all 0.2s; } #fullscreenBtn:hover { background: #555; } #gameOver { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.85); color: #f00; padding: 40px; border-radius: 10px; border: 4px solid #f00; text-align: center; z-index: 100; display: none; } #gameOver h2 { font-size: 48px; margin-bottom: 20px; } #gameOver button { padding: 15px 30px; background: #333; color: #fff; border: 2px solid #f00; border-radius: 5px; cursor: pointer; font-size: 20px; transition: all 0.2s; } #gameOver button:hover { background: #555; } </style> </head> <body> <h1>Wolfenstein 3D Style Raycasting Shooter</h1> <div id="gameContainer"> <canvas id="gameCanvas"></canvas> <div id="ui"> <div>FPS: <span id="fps">0</span></div> <div>Pos: <span id="pos">0,0</span></div> <div>Angle: <span id="angle">0</span></div> </div> <div id="crosshair">+</div> <div id="hud"> <div class="health">Health: <span id="healthValue">100</span></div> <div class="ammo">Ammo: <span id="ammoValue">30</span></div> <div class="score">Score: <span id="scoreValue">0</span></div> </div> <div id="gameOver"> <h2>GAME OVER</h2> <p>You have been defeated!</p> <p>Final Score: <span id="finalScore">0</span></p> <button onclick="resetGame()">PLAY AGAIN</button> </div> </div> <div id="instructions"> <p>WASD: Move | Mouse/Arrows: Turn | Left Click: Shoot | R: Reload | F: Fullscreen</p> <p>Navigate the maze, shoot enemies, avoid getting hit!</p> </div> <button id="fullscreenBtn" onclick="toggleFullscreen()">Toggle Fullscreen</button> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); let canvasWidth = canvas.width = canvas.parentElement.clientWidth; let canvasHeight = canvas.height = canvas.parentElement.clientHeight; const FOV = Math.PI / 3; // 60 degrees const MAX_DEPTH = 20; const WALL_HEIGHT = 1.0; const CELL_SIZE = 64; const PLAYER_HEIGHT = 0.5; const PLAYER_RADIUS = 0.2; const PLAYER_SPEED = 0.05; const PLAYER_TURN_SPEED = 0.04; const MOUSE_SENSITIVITY = 0.002; // Game state let player = { x: 1.5 * CELL_SIZE, y: 1.5 * CELL_SIZE, angle: 0, health: 100, ammo: 30, score: 0, movingForward: false, movingBackward: false, strafeLeft: false, strafeRight: false, turningLeft: false, turningRight: false, shooting: false, shootCooldown: 0, reloading: false, reloadTime: 0 }; // Maze grid (1 = wall, 0 = empty, 2 = enemy spawn) const MAP_SIZE = 16; let map = []; // Procedural generation function generateMaze() { map = []; // Fill with walls initially for (let y = 0; y < MAP_SIZE; y++) { map[y] = []; for (let x = 0; x < MAP_SIZE; x++) { // Border walls if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) { map[y][x] = 1; } else { // Random walls with some passages map[y][x] = Math.random() > 0.7 ? 1 : 0; } } } // Ensure player start area is clear const startX = Math.floor(player.x / CELL_SIZE); const startY = Math.floor(player.y / CELL_SIZE); for (let y = startY - 2; y <= startY + 2; y++) { for (let x = startX - 2; x <= startX + 2; x++) { if (x >= 0 && x < MAP_SIZE && y >= 0 && y < MAP_SIZE) { map[y][x] = 0; } } } // Add some enemy spawn points for (let i = 0; i < 8; i++) { let x, y; do { x = Math.floor(Math.random() * (MAP_SIZE - 4)) + 2; y = Math.floor(Math.random() * (MAP_SIZE - 4)) + 2; } while (map[y][x] !== 0 || (Math.abs(x - startX) < 4 && Math.abs(y - startY) < 4)); map[y][x] = 2; } } // Enemy class class Enemy { constructor(x, y) { this.x = x * CELL_SIZE + CELL_SIZE / 2; this.y = y * CELL_SIZE + CELL_SIZE / 2; this.health = 100; this.speed = 0.02; this.attackCooldown = 0; this.state = 'idle'; // idle, chasing, attacking this.angle = Math.random() * Math.PI * 2; this.textureId = Math.floor(Math.random() * 4); this.walkCycle = 0; } update(playerX, playerY) { const dx = playerX - this.x; const dy = playerY - this.y; const dist = Math.sqrt(dx * dx + dy * dy); const angleToPlayer = Math.atan2(dy, dx); if (dist < 300) { this.state = 'chasing'; // Move towards player this.angle = angleToPlayer; this.x += Math.cos(this.angle) * this.speed * CELL_SIZE; this.y += Math.sin(this.angle) * this.speed * CELL_SIZE; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (map[cellY][cellX] === 1) { // Bounce back this.x -= Math.cos(this.angle) * this.speed * CELL_SIZE * 2; this.y -= Math.sin(this.angle) * this.speed * CELL_SIZE * 2; this.angle += Math.PI / 2; } if (dist < 50) { this.state = 'attacking'; if (this.attackCooldown <= 0) { player.health -= 10; this.attackCooldown = 60; // ~1 second at 60fps } } } else { this.state = 'idle'; // Random wandering if (Math.random() < 0.02) { this.angle += (Math.random() - 0.5) * Math.PI; } this.x += Math.cos(this.angle) * this.speed * CELL_SIZE * 0.3; this.y += Math.sin(this.angle) * this.speed * CELL_SIZE * 0.3; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (map[cellY][cellX] === 1) { this.angle += Math.PI / 2; } } if (this.attackCooldown > 0) this.attackCooldown--; this.walkCycle = (this.walkCycle + 0.1) % (Math.PI * 2); } draw(ctx, x, y, width, height) { // Draw enemy sprite in 2D minimap ctx.fillStyle = '#f00'; ctx.fillRect(x - 3, y - 3, 6, 6); } } let enemies = []; // Projectile class class Projectile { constructor(x, y, angle) { this.x = x; this.y = y; this.angle = angle; this.speed = 10; this.distance = 0; this.maxDistance = 500; this.active = true; } update() { this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; this.distance += this.speed; // Check collision with walls const cellX = Math.floor(this.x / CELL_SIZE); const cellY = Math.floor(this.y / CELL_SIZE); if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX] === 1) { this.active = false; return; } } // Check collision with enemies for (let i = enemies.length - 1; i >= 0; i--) { const enemy = enemies[i]; const dx = enemy.x - this.x; const dy = enemy.y - this.y; if (Math.sqrt(dx * dx + dy * dy) < 20) { enemy.health -= 35; this.active = false; if (enemy.health <= 0) { player.score += 100; enemies.splice(i, 1); } break; } } if (this.distance > this.maxDistance) this.active = false; } } let projectiles = []; // Texture generation const textures = []; function generateTextures() { const textureSize = 64; // Generate 4 wall textures for (let t = 0; t < 4; t++) { const texture = document.createElement('canvas'); texture.width = textureSize; texture.height = textureSize; const tctx = texture.getContext('2d'); // Generate procedural wall texture const hue = Math.floor(Math.random() * 360); const saturation = 30 + Math.random() * 40; const lightness = 20 + Math.random() * 30; // Base color tctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`; tctx.fillRect(0, 0, textureSize, textureSize); // Add some noise/pattern tctx.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness + 10}%)`; for (let i = 0; i < 200; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); tctx.fillRect(x, y, 2, 2); } // Add brick-like pattern for some textures if (t < 2) { tctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness - 10}%)`; tctx.lineWidth = 2; // Horizontal lines for (let y = 0; y < textureSize; y += 16) { tctx.beginPath(); tctx.moveTo(0, y); tctx.lineTo(textureSize, y); tctx.stroke(); } // Vertical lines with offset every other row for (let y = 0; y < textureSize; y += 16) { for (let x = (y / 16) % 2 === 0 ? 0 : 16; x < textureSize; x += 32) { tctx.beginPath(); tctx.moveTo(x, y); tctx.lineTo(x, y + 16); tctx.stroke(); } } } textures.push(texture); } // Generate floor texture (index 4) const floorTexture = document.createElement('canvas'); floorTexture.width = textureSize; floorTexture.height = textureSize; const fctx = floorTexture.getContext('2d'); fctx.fillStyle = '#333'; fctx.fillRect(0, 0, textureSize, textureSize); fctx.fillStyle = '#444'; for (let i = 0; i < 50; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); fctx.fillRect(x, y, 4, 4); } textures.push(floorTexture); // Generate ceiling texture (index 5) const ceilingTexture = document.createElement('canvas'); ceilingTexture.width = textureSize; ceilingTexture.height = textureSize; const cctx = ceilingTexture.getContext('2d'); cctx.fillStyle = '#111'; cctx.fillRect(0, 0, textureSize, textureSize); cctx.fillStyle = '#222'; for (let i = 0; i < 30; i++) { const x = Math.floor(Math.random() * textureSize); const y = Math.floor(Math.random() * textureSize); cctx.fillRect(x, y, 6, 2); } textures.push(ceilingTexture); } // Raycasting rendering function castRay(angle) { const rayDirX = Math.cos(angle); const rayDirY = Math.sin(angle); // Player position in map coordinates let mapX = Math.floor(player.x / CELL_SIZE); let mapY = Math.floor(player.y / CELL_SIZE); // Length of ray from current position to next x or y-side const deltaDistX = Math.abs(1 / rayDirX); const deltaDistY = Math.abs(1 / rayDirY); let sideDistX, sideDistY; let stepX, stepY; // Calculate step and initial sideDist if (rayDirX < 0) { stepX = -1; sideDistX = (player.x / CELL_SIZE - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x / CELL_SIZE) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (player.y / CELL_SIZE - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y / CELL_SIZE) * deltaDistY; } let hit = 0; let side; // 0 = x-side, 1 = y-side // DDA algorithm while (hit === 0) { // Jump to next map square if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check if ray has hit a wall if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) { break; } if (map[mapY][mapX] === 1) hit = 1; } // Calculate distance projected on camera direction let perpWallDist; if (side === 0) { perpWallDist = (mapX - player.x / CELL_SIZE + (1 - stepX) / 2) / rayDirX; } else { perpWallDist = (mapY - player.y / CELL_SIZE + (1 - stepY) / 2) / rayDirY; } return { distance: perpWallDist, side: side, mapX: mapX, mapY: mapY, hit: hit }; } function draw3DView() { // Draw ceiling ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvasWidth, canvasHeight / 2); // Draw floor ctx.fillStyle = '#333'; ctx.fillRect(0, canvasHeight / 2, canvasWidth, canvasHeight / 2); // Raycasting for each column for (let x = 0; x < canvasWidth; x++) { // Calculate ray angle const cameraX = 2 * x / canvasWidth - 1; const rayAngle = player.angle + FOV / 2 * cameraX; const ray = castRay(rayAngle); if (ray.hit) { // Calculate line height const lineHeight = canvasHeight / ray.distance; // Calculate texture coordinates let wallX; if (ray.side === 0) { wallX = player.y / CELL_SIZE + ray.distance * Math.sin(rayAngle); } else { wallX = player.x / CELL_SIZE + ray.distance * Math.cos(rayAngle); } wallX -= Math.floor(wallX); // Select texture (based on map value and side) // map value 1 -> wall, use texture 0-3 based on position const texIndex = (ray.mapX + ray.mapY) % 4; const texture = textures[texIndex]; // Texture X coordinate let texX = Math.floor(wallX * texture.width); if (ray.side === 1) texX = texture.width - texX - 1; // Draw vertical stripe const drawStart = Math.max(0, -lineHeight / 2 + canvasHeight / 2); const drawEnd = Math.min(canvasHeight, lineHeight / 2 + canvasHeight / 2); // Sample texture and draw const texHeight = texture.height; const step = texHeight / lineHeight; let texPos = (drawStart - canvasHeight / 2 + lineHeight / 2) * step; for (let y = drawStart; y < drawEnd; y++) { const texY = Math.floor(texPos) % texHeight; const pixelData = texture.getContext('2d').getImageData(texX, texY, 1, 1).data; // Darken y-side walls for depth effect let r = pixelData[0]; let g = pixelData[1]; let b = pixelData[2]; if (ray.side === 1) { r = r * 0.7; g = g * 0.7; b = b * 0.7; } ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.fillRect(x, y, 1, 1); texPos += step; } } } // Draw enemies as sprites (billboarding) for (const enemy of enemies) { // Calculate angle between player and enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToEnemy = Math.atan2(dy, dx); const angleDiff = angleToEnemy - player.angle; // Normalize angle difference let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; // Check if enemy is within FOV if (Math.abs(normalizedAngleDiff) < FOV / 2) { // Calculate screen position const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const spriteHeight = canvasHeight / dist * CELL_SIZE * 0.8; const spriteWidth = spriteHeight; // Draw enemy sprite (simplified rectangle for now) const spriteY = canvasHeight / 2 - spriteHeight / 2; // Create enemy sprite with color ctx.fillStyle = `hsl(0, 80%, ${50 + Math.sin(enemy.walkCycle) * 10}%)`; ctx.fillRect(screenX - spriteWidth / 2, spriteY, spriteWidth, spriteHeight); // Draw health bar const healthWidth = spriteWidth * (enemy.health / 100); ctx.fillStyle = '#f00'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, spriteWidth, 4); ctx.fillStyle = '#0f0'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, healthWidth, 4); } } } // Draw projectiles ctx.fillStyle = '#ff0'; for (const proj of projectiles) { const dx = proj.x - player.x; const dy = proj.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToProj = Math.atan2(dy, dx); const angleDiff = angleToProj - player.angle; let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; if (Math.abs(normalizedAngleDiff) < FOV / 2) { const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const projSize = Math.max(2, 10 / dist); ctx.fillRect(screenX - projSize / 2, canvasHeight / 2 - projSize / 2, projSize, projSize); } } } // Draw weapon drawWeapon(); } function drawWeapon() { // Simple weapon rendering at bottom center const weaponWidth = canvasWidth * 0.3; const weaponHeight = canvasHeight * 0.4; const weaponX = canvasWidth / 2 - weaponWidth / 2; const weaponY = canvasHeight - weaponHeight + 20; // Weapon base ctx.fillStyle = '#444'; ctx.fillRect(weaponX, weaponY, weaponWidth, weaponHeight); // Barrel ctx.fillStyle = '#222'; ctx.fillRect(canvasWidth / 2 - 5, weaponY - 30, 10, 40); // Trigger ctx.fillStyle = '#666'; ctx.fillRect(canvasWidth / 2 + 20, weaponY + weaponHeight - 20, 10, 15); // Muzzle flash when shooting if (player.shooting && player.shootCooldown > 0) { const flashSize = 30 + Math.random() * 20; ctx.fillStyle = '#ff0'; ctx.fillRect(canvasWidth / 2 - flashSize / 2, weaponY - 60, flashSize, flashSize); } } function drawMiniMap() { const miniMapSize = 150; const cellSize = miniMapSize / MAP_SIZE; const offsetX = 10; const offsetY = canvasHeight - miniMapSize - 10; // Background ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(offsetX, offsetY, miniMapSize, miniMapSize); // Draw walls for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 1) { ctx.fillStyle = '#888'; ctx.fillRect(offsetX + x * cellSize, offsetY + y * cellSize, cellSize, cellSize); } } } // Draw enemies for (const enemy of enemies) { const mapX = enemy.x / CELL_SIZE; const mapY = enemy.y / CELL_SIZE; ctx.fillStyle = '#f00'; ctx.fillRect(offsetX + mapX * cellSize - 2, offsetY + mapY * cellSize - 2, 4, 4); } // Draw player ctx.fillStyle = '#0f0'; const playerMapX = player.x / CELL_SIZE; const playerMapY = player.y / CELL_SIZE; ctx.fillRect(offsetX + playerMapX * cellSize - 3, offsetY + playerMapY * cellSize - 3, 6, 6); // Draw player direction ctx.strokeStyle = '#0f0'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(offsetX + playerMapX * cellSize, offsetY + playerMapY * cellSize); ctx.lineTo( offsetX + playerMapX * cellSize + Math.cos(player.angle) * 15, offsetY + playerMapY * cellSize + Math.sin(player.angle) * 15 ); ctx.stroke(); } // Input handling const keys = {}; window.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; // Shooting with spacebar if (e.key === ' ' && player.ammo > 0 && player.shootCooldown <= 0 && !player.reloading) { player.shooting = true; player.shootCooldown = 10; player.ammo--; projectiles.push(new Projectile( player.x + Math.cos(player.angle) * 20, player.y + Math.sin(player.angle) * 20, player.angle )); } // Reload with R if (e.key === 'r' && player.ammo < 30 && !player.reloading) { player.reloading = true; player.reloadTime = 60; // 1 second } // Prevent arrow keys from scrolling if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(e.key.toLowerCase())) { e.preventDefault(); } }); window.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; if (e.key === ' ') { player.shooting = false; } }); // Mouse movement for looking let mouseLocked = false; let lastMouseX = 0; canvas.addEventListener('click', () => { canvas.requestPointerLock(); }); document.addEventListener('pointerlockchange', () => { mouseLocked = document.pointerLockElement === canvas; }); document.addEventListener('mousemove', (e) => { if (mouseLocked) { player.angle += e.movementX * MOUSE_SENSITIVITY; // Keep angle normalized player.angle %= Math.PI * 2; } lastMouseX = e.clientX; }); // Mouse click for shooting canvas.addEventListener('mousedown', (e) => { if (e.button === 0 && player.ammo > 0 && player.shootCooldown <= 0 && !player.reloading) { player.shooting = true; player.shootCooldown = 10; player.ammo--; projectiles.push(new Projectile( player.x + Math.cos(player.angle) * 20, player.y + Math.sin(player.angle) * 20, player.angle )); } }); canvas.addEventListener('mouseup', (e) => { if (e.button === 0) { player.shooting = false; } }); // Update game state function update() { // Player movement const moveSpeed = PLAYER_SPEED * CELL_SIZE; const turnSpeed = PLAYER_TURN_SPEED; if (keys['w'] || keys['arrowup']) { player.x += Math.cos(player.angle) * moveSpeed; player.y += Math.sin(player.angle) * moveSpeed; checkCollision(); } if (keys['s'] || keys['arrowdown']) { player.x -= Math.cos(player.angle) * moveSpeed; player.y -= Math.sin(player.angle) * moveSpeed; checkCollision(); } if (keys['a']) { // Strafe left player.x += Math.cos(player.angle - Math.PI / 2) * moveSpeed * 0.7; player.y += Math.sin(player.angle - Math.PI / 2) * moveSpeed * 0.7; checkCollision(); } if (keys['d']) { // Strafe right player.x += Math.cos(player.angle + Math.PI / 2) * moveSpeed * 0.7; player.y += Math.sin(player.angle + Math.PI / 2) * moveSpeed * 0.7; checkCollision(); } if (keys['arrowleft']) { player.angle -= turnSpeed; } if (keys['arrowright']) { player.angle += turnSpeed; } // Keep angle normalized player.angle %= Math.PI * 2; // Update cooldowns if (player.shootCooldown > 0) player.shootCooldown--; if (player.reloading) { player.reloadTime--; if (player.reloadTime <= 0) { player.ammo = 30; player.reloading = false; } } // Update projectiles for (let i = projectiles.length - 1; i >= 0; i--) { projectiles[i].update(); if (!projectiles[i].active) { projectiles.splice(i, 1); } } // Update enemies for (const enemy of enemies) { enemy.update(player.x, player.y); } // Spawn new enemies if needed if (enemies.length < 5 && Math.random() < 0.005) { // Find a spawn point for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 2) { // Check distance from player const dist = Math.sqrt( Math.pow(player.x - (x * CELL_SIZE + CELL_SIZE / 2), 2) + Math.pow(player.y - (y * CELL_SIZE + CELL_SIZE / 2), 2) ); if (dist > 200) { enemies.push(new Enemy(x, y)); break; } } } } } // Update UI document.getElementById('pos').textContent = `${Math.floor(player.x)}, ${Math.floor(player.y)}`; document.getElementById('angle').textContent = `${(player.angle * 180 / Math.PI).toFixed(1)}°`; document.getElementById('healthValue').textContent = player.health; document.getElementById('ammoValue').textContent = player.ammo; document.getElementById('scoreValue').textContent = player.score; // Check game over if (player.health <= 0) { gameOver(); } } function checkCollision() { const cellX = Math.floor(player.x / CELL_SIZE); const cellY = Math.floor(player.y / CELL_SIZE); // Check collision with walls if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX] === 1) { // Push player back player.x -= Math.cos(player.angle) * PLAYER_SPEED * CELL_SIZE * 2; player.y -= Math.sin(player.angle) * PLAYER_SPEED * CELL_SIZE * 2; } } // Keep player within bounds player.x = Math.max(PLAYER_RADIUS * CELL_SIZE, Math.min(player.x, MAP_SIZE * CELL_SIZE - PLAYER_RADIUS * CELL_SIZE)); player.y = Math.max(PLAYER_RADIUS * CELL_SIZE, Math.min(player.y, MAP_SIZE * CELL_SIZE - PLAYER_RADIUS * CELL_SIZE)); } function gameOver() { document.getElementById('gameOver').style.display = 'block'; document.getElementById('finalScore').textContent = player.score; } function resetGame() { player = { x: 1.5 * CELL_SIZE, y: 1.5 * CELL_SIZE, angle: 0, health: 100, ammo: 30, score: 0, movingForward: false, movingBackward: false, strafeLeft: false, strafeRight: false, turningLeft: false, turningRight: false, shooting: false, shootCooldown: 0, reloading: false, reloadTime: 0 }; enemies = []; projectiles = []; generateMaze(); spawnEnemies(); document.getElementById('gameOver').style.display = 'none'; } function spawnEnemies() { for (let y = 0; y < MAP_SIZE; y++) { for (let x = 0; x < MAP_SIZE; x++) { if (map[y][x] === 2) { // Only spawn if far from player const dist = Math.sqrt( Math.pow(player.x - (x * CELL_SIZE + CELL_SIZE / 2), 2) + Math.pow(player.y - (y * CELL_SIZE + CELL_SIZE / 2), 2) ); if (dist > 200) { enemies.push(new Enemy(x, y)); } } } } } // Fullscreen toggle function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(console.log); } else { document.exitFullscreen(); } } // Resize handling window.addEventListener('resize', () => { canvasWidth = canvas.width = canvas.parentElement.clientWidth; canvasHeight = canvas.height = canvas.parentElement.clientHeight; }); // Game loop let lastTime = 0; let fps = 0; let frameCount = 0; let lastFpsUpdate = 0; function gameLoop(timestamp) { const deltaTime = timestamp - lastTime; lastTime = timestamp; // Update FPS counter frameCount++; if (timestamp - lastFpsUpdate >= 1000) { fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate)); document.getElementById('fps').textContent = fps; frameCount = 0; lastFpsUpdate = timestamp; } update(); // Clear canvas ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Draw 3D view draw3DView(); // Draw minimap drawMiniMap(); requestAnimationFrame(gameLoop); } // Initialize function init() { generateTextures(); generateMaze(); spawnEnemies(); requestAnimationFrame(gameLoop); } window.addEventListener('load', init); </script> </body> </html>

assistant
thinking
Now we need to add F key handling. Let's edit the keydown event listener block. We'll replace lines 725 to maybe 750. Let's find the exact block. We'll replace from "window.addEventListener('keydown', (e) => {" to the closing brace before "window.addEventListener('keyup'". Let's locate the keyup line. Let's search for "window.addEventListener('keyup'". Let's get line numbers again.
bash {"command":"grep -n \"window.addEventListener('keyup'\" index.html"}
toolResult

toolResult

752: window.addEventListener('keyup', (e) => {

assistant
thinking
So keydown block is from line 725 to line 751. Let's extract that block and replace it with enhanced version. Let's write the edit:
edit {"edits":[{"newText":" window.addEventListener('keydown', (e) => {\n keys[e.key.toLowerCase()] = true;\n \n
toolResult

toolResult

Successfully replaced 1 block(s) in index.html.

assistant
thinking
Great. Now we need to improve enemy sprite drawing to be more interesting. Let's replace the simple rectangle with a basic enemy shape (maybe a circle with eyes). We'll also add a simple animation using walkCycle. We'll edit the draw3DView function again but only the enemy drawing part. Let's find the enemy drawing lines. We'll replace from "// Draw enemies as sprites (billboarding)" to the end of that block (just before "// Draw projectiles"). Let's locate that section. We'll read lines around 600-650. Let's search for "Draw enemies as sprites". Let's grep again
bash {"command":"grep -n \"Draw enemies as sprites\" index.html"}
toolResult

toolResult

582: // Draw enemies as sprites (billboarding)

assistant
thinking
Now we need to see the block. Let's read from line 580 to maybe 650. Let's do a quick read with offset. Let's use bash to get lines 580-650. But we can just replace the block we already know. However we need to be careful to replace only the enemy drawing part, not the projectile drawing. Let's write a new edit that replaces from line 582 to line 620 (or until the line before "// Draw projectiles").

Let's capture the exact old text first. Let's read from line 580 to line 630. Let's write a temporary script but we can approximate. We'll do a more precise edit: use the exact oldText that we can copy from the file. Let's extract using bash with sed. Let's do:
bash {"command":"sed -n '582,630p' index.html"}
toolResult

toolResult

// Draw enemies as sprites (billboarding) for (const enemy of enemies) { // Calculate angle between player and enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToEnemy = Math.atan2(dy, dx); const angleDiff = angleToEnemy - player.angle; // Normalize angle difference let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; // Check if enemy is within FOV if (Math.abs(normalizedAngleDiff) < FOV / 2) { // Calculate screen position const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const spriteHeight = canvasHeight / dist * CELL_SIZE * 0.8; const spriteWidth = spriteHeight; // Draw enemy sprite (simplified rectangle for now) const spriteY = canvasHeight / 2 - spriteHeight / 2; // Create enemy sprite with color ctx.fillStyle = `hsl(0, 80%, ${50 + Math.sin(enemy.walkCycle) * 10}%)`; ctx.fillRect(screenX - spriteWidth / 2, spriteY, spriteWidth, spriteHeight); // Draw health bar const healthWidth = spriteWidth * (enemy.health / 100); ctx.fillStyle = '#f00'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, spriteWidth, 4); ctx.fillStyle = '#0f0'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, healthWidth, 4); } } } // Draw projectiles ctx.fillStyle = '#ff0'; for (const proj of projectiles) { const dx = proj.x - player.x; const dy = proj.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToProj = Math.atan2(dy, dx);

assistant
thinking
Great. Now we need to replace from "// Draw enemies as sprites (billboarding)" up to the line just before "// Draw projectiles". Let's find the exact line number where "// Draw projectiles" starts. Let's search again:
bash {"command":"grep -n \"Draw projectiles\" index.html"}
toolResult

toolResult

622: // Draw projectiles

assistant
thinking
So we need to replace lines 582 to 621 (inclusive). Let's get the exact oldText from line 582 to line 621. Let's use sed to capture. Let's write a small script but we can just copy from the earlier output and add the rest lines up to line 621. Let's read from 582 to 621. Let's do:
bash {"command":"sed -n '582,621p' index.html"}
toolResult

toolResult

// Draw enemies as sprites (billboarding) for (const enemy of enemies) { // Calculate angle between player and enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_DEPTH * CELL_SIZE) { const angleToEnemy = Math.atan2(dy, dx); const angleDiff = angleToEnemy - player.angle; // Normalize angle difference let normalizedAngleDiff = angleDiff; while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= Math.PI * 2; while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += Math.PI * 2; // Check if enemy is within FOV if (Math.abs(normalizedAngleDiff) < FOV / 2) { // Calculate screen position const screenX = (normalizedAngleDiff + FOV / 2) / FOV * canvasWidth; const spriteHeight = canvasHeight / dist * CELL_SIZE * 0.8; const spriteWidth = spriteHeight; // Draw enemy sprite (simplified rectangle for now) const spriteY = canvasHeight / 2 - spriteHeight / 2; // Create enemy sprite with color ctx.fillStyle = `hsl(0, 80%, ${50 + Math.sin(enemy.walkCycle) * 10}%)`; ctx.fillRect(screenX - spriteWidth / 2, spriteY, spriteWidth, spriteHeight); // Draw health bar const healthWidth = spriteWidth * (enemy.health / 100); ctx.fillStyle = '#f00'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, spriteWidth, 4); ctx.fillStyle = '#0f0'; ctx.fillRect(screenX - spriteWidth / 2, spriteY - 10, healthWidth, 4); } } }

TAKE IT TO CODE

This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.

RUN

modeldeepseek-v3.2
vendorDeepSeek
harnesspi-agent
generated2026-07-22

METRICS

runtime
tokens in191k
tokens out19k
cached231k
cost / run$0.0500