Wolfenstein raycaster · nemotron-3-super-120b-a12b
Output saved · Hit the output limit Cost / run $0.0141 Runtime 516.4s
The model's last reply hit its output-token limit. The output is the HTML saved before that.
Prompt for this run
Build a playable first-person raycasting maze shooter on a canvas in a Wolfenstein-inspired style. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Render a textured grid maze through a raycaster, with perspective wall heights and visible enemies. Generate textures procedurally; do not load external assets. - Use W/S to move forward/backward, A/D to strafe, Left/Right arrows to turn, and Space to shoot. Display the controls. Mouse turning is optional and must not be required. - Use solid wall collision and a small level with a reachable exit and at least three enemies. Enemies must be occluded by walls; shots cannot damage an enemy through a wall. - Give the player health. Enemies can damage the player under a clear attack rule. Show health and remaining enemy count; zero health causes Game Over. - Defeating all enemies unlocks the exit; reaching it shows a win state. Provide Restart to restore the level, player, enemies, and health. Verify: Test movement and turning near the starting area, check collision against a nearby wall, and fire to inspect weapon feedback. Sample the scene for stable wall rendering, then use Restart and check the initial viewpoint and health.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
METRICS
Transcript · 9 messages
Build a playable first-person raycasting maze shooter on a canvas in a Wolfenstein-inspired style. Use HTML, CSS, and JavaScript as needed. Write it to `index.html` in the current working directory using your file tools. Inline your own CSS and JavaScript. Do not use external assets, CDNs, or network requests. No third-party libraries are provided. Required behavior: - Render a textured grid maze through a raycaster, with perspective wall heights and visible enemies. Generate textures procedurally; do not load external assets. - Use W/S to move forward/backward, A/D to strafe, Left/Right arrows to turn, and Space to shoot. Display the controls. Mouse turning is optional and must not be required. - Use solid wall collision and a small level with a reachable exit and at least three enemies. Enemies must be occluded by walls; shots cannot damage an enemy through a wall. - Give the player health. Enemies can damage the player under a clear attack rule. Show health and remaining enemy count; zero health causes Game Over. - Defeating all enemies unlocks the exit; reaching it shows a win state. Provide Restart to restore the level, player, enemies, and health. Verify: Test movement and turning near the starting area, check collision against a nearby wall, and fire to inspect weapon feedback. Sample the scene for stable wall rendering, then use Restart and check the initial viewpoint and health.
thinking
We need to build a raycasting maze shooter similar to Wolfenstein 3D. Use canvas for rendering. Must be a single index.html with inline CSS/JS.
Plan: Create a simple grid-based level, raycaster to render walls and sprites (enemies). Implement player movement with collision detection. Implement shooting: raycast from player to check for enemy hit (line-of-sight not blocked by walls). Enemies can damage player if within range and line of sight? We'll define simple: enemy shoots back if player in front and within certain distance and no wall blocking? But requirement: "Enemies can damage the player under a clear attack rule." We can implement that enemies have a cooldown and if player is in front within certain angle/distance and no wall blocking, they hit player.
We need to show health and remaining enemy count. Defeating all enemies unlocks exit; reaching it shows win state. Provide Restart button.
We must verify movement, turning, collision, shooting feedback, wall rendering, restart.
We'll implement procedural textures: generate random patterns for walls (e.g., checkerboard or noise). For simplicity, we can assign each wall a color based on direction (N/S/E/W) and shade based on distance.
Enemies: simple sprite (maybe a rectangle) drawn using sprite scaling based on distance. We'll need to sort enemies by distance for correct rendering.
Let's outline code structure.
We'll have:
- Constants: MAP_SIZE, TILE_SIZE, FOV, etc.
- Map: 2D array of integers (0 empty, 1 wall).
- Player: x, y, angle, health.
- Enemies: array of objects {x, y, health, alive}.
- Exit: tile coordinates.
- Raycaster: for each column of canvas, compute ray angle, step through map to find wall hit, compute distance, texture coordinate, draw vertical slice.
- For sprite rendering: transform enemy position to camera space, compute sprite screen position and height, draw vertical stripe using enemy texture (maybe a simple color).
- Shooting: when space pressed, cast a ray from player at current angle, check first enemy hit within certain range and not blocked by wall (we can reuse raycasting but stop at first hit: if hit enemy before wall, register hit). If enemy hit, reduce enemy health; if <=0, mark dead.
- Enemy AI: simple: if player in line of sight and within range, after cooldown, damage player.
- Collision: when moving, check new position against map (if tile at new position is wall, block).
- Input: keydown/keyup for W,A,S,D, ArrowLeft/ArrowRight, Space.
- Render loop: clear canvas, cast rays, render walls, render sprites, draw HUD (health, enemy count), draw controls text.
- Win condition: if all enemies dead and player reaches exit tile (distance < 0.5?), show win message.
- Game Over: if health <=0.
- Restart: reset level, player, enemies, health.
We'll need to generate textures procedurally: we can create two textures: wall texture (maybe a 64x64 checker pattern) and enemy texture (maybe a simple face). We'll generate using canvas offscreen.
Implementation details:
We'll use a single canvas, width=800, height=600.
We'll create offscreen textures: wallTex (64x64) with a pattern; enemyTex (64x64) with a simple shape.
Raycaster algorithm: typical DDA.
We'll need to handle sprite casting: transform sprite relative to player, compute spriteX on screen, spriteHeight, then draw vertical stripe using texture.
We'll need to sort enemies by distance (far to near) for correct overlay.
Let's start writing code.
We'll write to index.html.
First, check current directory: we are at /tmp/job-5wFaGF. We'll write index.html there.
Let's start by creating the file.
We'll use write tool to create index.html.
But we need to be careful: we need to output the full HTML.
We'll do stepwise: first create a basic skeleton, then edit to add JS and CSS.
Better to write the whole file at once.
Let's craft the HTML.
We'll include:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Raycasting Maze Shooter</title>
<style>
/* CSS */
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
Health: <span id="health">100</span> | Enemies: <span id="enemies">3</span>
</div>
<div id="controls">Controls: W/A/S/D move, ArrowLeft/ArrowRight turn, Space shoot</div>
<div id="message" style="display:none;"></div>
<button id="restartBtn" style="display:none;">Restart</button>
<script>
/* JavaScript */
</script>
</body>
</html>
We'll need to style canvas to be block, center, etc.
Now implement JS.
Let's write the JS in a single script tag.
We'll need to wait for DOM load.
We'll define constants:
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const TILE_SIZE = 64;
const MAP_WIDTH = 8;
const MAP_HEIGHT = 8;
const MAP = [
[1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,1],
[1,0,1,0,1,0,0,1],
[1,0,0,0,0,0,0,1],
[1,0,1,0,1,0,0,1],
[1,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1]
];
But we need at least three enemies and an exit. Let's define exit at (7,7) maybe but that's wall. We'll set exit at (1,1) after walls? Actually we need a reachable exit. Let's design a small maze with open area.
Better to generate a simple maze: we can hardcode.
Let's design:
Map 10x10:
We'll create walls around border, and some inner walls.
Let's define:
const MAP = [
[1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,0,0,0,0,1],
[1,0,1,0,1,0,1,1,0,1],
[1,0,1,0,0,0,0,0,0,1],
[1,0,1,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,1,0,1,1,1,0,1,1],
[1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,0,1,1],
[1,1,1,1,1,1,1,1,1,1]
];
That's 10x10 with walls (1) and empty (0). We'll place enemies in empty cells, exit at (1,8) maybe.
But we need to ensure exit is reachable.
Let's set exit at (8,1) (x=8,y=1) which is empty? Check MAP[1][8] = row1 col8: row index 1, col index 8: MAP[1][8] = 0 (since row1: [1,0,0,0,1,0,0,0,0,1] => index8 is 0). Good.
We'll place enemies at (2,2), (5,5), (3,7) maybe.
But need to ensure they are not inside walls.
Let's compute later.
We'll store exit tile as {x: 8, y: 1}.
Now player start position: choose a free tile, e.g., (1,1) (row1 col1) which is 0.
Set player.x = 1.5 * TILE_SIZE? Actually we want floating point coordinates in world units where 1 unit = TILE_SIZE. So we can set player.x = 1.5, player.y = 1.5 (in tile units). Then multiply by TILE_SIZE for pixel positions.
Simpler: keep coordinates in world units where 1 = TILE_SIZE. So map coordinates are tile indices. Player position in same units.
Thus player.x = 1.5, player.y = 1.5.
Angle: 0 radians pointing right? We'll set angle = 0 (pointing to positive x). But typical raycaster uses angle 0 pointing up? We'll decide: angle 0 pointing right, increase angle counterclockwise? We'll need to adjust movement: forward = cos(angle), sin(angle). We'll set angle = 0 pointing right, then forward dx = Math.cos(angle), dy = Math.sin(angle). That's fine.
FOV: 60 degrees = Math.PI/3.
Raycaster: number of rays = canvas.width (or we can do less for performance, but we'll do each column).
We'll compute ray angle for each column: rayAngle = playerAngle - FOV/2 + (x / canvas.width) * FOV.
Now DDA: we need to compute step and sideDist.
We'll implement typical algorithm.
We'll also need to compute texture X coordinate based on where the ray hit wall.
We'll generate wall texture: we can create a 64x64 canvas with a pattern: maybe alternating bricks.
Simplify: we can compute color based on map value and side (NS/EW). For shading, we can darken based on distance.
But requirement: "Generate textures procedurally; do not load external assets." So we need to generate a texture image. We'll create an offscreen canvas and fill with a procedural pattern (e.g., random noise or checker). We'll then use that texture for all walls.
Similarly for enemy sprite: we can generate a simple enemy texture (maybe a red circle).
Let's create wallTexture: size 64x64, fill with a checker pattern of two shades.
We'll also create enemyTexture: maybe a face.
Now for sprite rendering: we need to transform enemy position to camera space.
Standard sprite casting:
- Compute relative position: spriteX = enemy.x - player.x; spriteY = enemy.y - player.y.
- Transform to camera space:
invDet = 1.0 / (planeX * playerDirY - playerDirX * planeY); where playerDir is (cos angle, sin angle), plane is perpendicular to dir (for FOV). Actually we need camera plane: planeX = -sin(angle) * (FOV/2)? Wait typical: playerDir = (cos angle, sin angle); plane = (-sin angle, cos angle) * (FOV/2). Actually plane is perpendicular to dir, same length as FOV/2? In wolfenstein, plane.x = -dir.y * planeW, plane.y = dir.x * planeW where planeW = tan(FOV/2). We'll keep simple: planeX = -Math.sin(angle) * (FOV/2); planeY = Math.cos(angle) * (FOV/2). That gives plane perpendicular to dir with magnitude FOV/2.
But we can also use simpler: we can compute sprite screen position using:
// translate sprite position to relative to camera
float spriteX = enemy.x - player.x;
float spriteY = enemy.y - player.y;
// inverse of camera matrix
float invDet = 1.0 / (playerX * planeY - playerY * planeX); // Actually need correct.
Let's recall typical wolfenstein sprite casting:
Let playerPos = (playerX, playerY)
let playerDir = (cos a, sin a)
let playerPlane = (-sin a * planeW, cos a * planeW) where planeW = tan(FOV/2)
Then:
float spriteX = enemy.x - playerX;
float spriteY = enemy.y - playerY;
float invDet = 1.0 / (playerX * playerPlaneY - playerY * playerPlaneX); // Actually determinant of [playerDir, playerPlane]? Wait.
Better to look up standard formula:
//transform sprite with the inverse camera matrix
// [ planeX dirX ] [spriteX] [?]
// [ planeY dirY ] [spriteY] [?]
Actually the camera matrix is [dirX planeX; dirY planeY]? Not sure.
Given time, we can simplify: we can render enemies as simple squares using raycasting? But requirement: visible enemies, need to be sprites.
Alternative: we can render enemies as scaled vertical stripes using raycasting? Not ideal.
We can implement a simple 3D sprite rendering using projection: compute distance to enemy, compute screen size, and draw a rectangle at appropriate screen position.
We can do:
- Compute distance from player to enemy: dist = sqrt((dx)^2 + (dy)^2)
- Compute angle difference between enemy direction and player angle: angleToEnemy = atan2(dy, dx) - playerAngle; normalize to [-pi, pi].
- If abs(angleToEnemy) < FOV/2, then enemy is within field of view.
- Screen x = (angleToEnemy / FOV) * 0.5 + 0.5) * canvas.width; // centered.
- Sprite height on screen = (TILE_SIZE / dist) * (canvas.height / 2) * some scale factor? Actually wall height projection: wallHeight = (TILE_SIZE / dist) * (canvas.height / 2) * something. For simplicity, we can set spriteHeight = (TILE_SIZE / dist) * canvas.height; // then clamp.
- Then draw a rectangle centered at screen x, with width = spriteHeight * aspect? We'll keep square sprite: width = spriteHeight.
- Then we need to draw enemy texture onto that rectangle. We can draw image of enemyTexture scaled to that rectangle.
We also need to hide enemies behind walls: we can raycast from player to enemy and check if any wall is closer than enemy distance. If yes, don't draw.
We'll implement a function isVisible(enemy) that casts a ray towards enemy and checks for wall collision before reaching enemy.
Similarly for shooting: we can cast a ray from player at current angle and check first hit: if enemy is hit before wall, register hit.
Now enemy AI: we can have each enemy have a timer; if player is in line of sight and within range (say distance < 5 tiles) and no wall blocking, then after cooldown (e.g., 2 seconds) they hit player for some damage.
We'll keep simple: enemy attacks every second if conditions met, dealing 10 damage.
Now we need to show health and enemy count.
Implement HUD.
Now implement restart: reset player position, angle, health, enemies alive, etc.
Now we need to test.
Let's start writing code.
We'll write the JS in a single script tag.
We'll use requestAnimationFrame for game loop.
Let's start with writing the file.
We'll use write tool to create index.html.
But we need to be careful: we might need to edit later. We'll write the whole file now.
Let's craft the HTML content.
We'll do:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Raycasting Maze Shooter</title>
<style>
body { margin:0; padding:0; background:#111; color:#fff; font-family:sans-serif; overflow:hidden; }
canvas { display:block; margin:0 auto; background:#000; }
#hud { position:absolute; top:10px; left:10px; }
#controls { position:absolute; bottom:10px; left:10px; font-size:14px; }
#message { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center; background:rgba(0,0,0,0.7); padding:20px; border-radius:10px; }
#restartBtn { position:absolute; top:60%; left:50%; transform:translate(-50%,-50%); padding:10px 20px; font-size:16px; cursor:pointer; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
Health: <span id="health">100</span> | Enemies: <span id="enemies">3</span>
</div>
<div id="controls">Controls: W/A/S/D move, ArrowLeft/ArrowRight turn, Space shoot</div>
<div id="message"></div>
<button id="restartBtn">Restart</button>
<script>
// Game code here
</script>
</body>
</html>
```
Now we need to fill the script.
Let's write the script step by step.
We'll define constants, create textures, initialize game state, input handling, game loop, rendering, etc.
We'll do it in one go.
Let's start writing the script content.
We'll use a big string and write via write tool.
But we need to be careful about escaping. We'll just write the file directly.
Let's proceed.
First, we'll write the file with a placeholder script, then we can edit if needed.
But we can write the whole thing now.
Let's do:
We'll use write tool to create index.html with the full content.
We'll need to generate the JS.
Let's start.
/tmp/job-5wFaGF/index.html (16578 bytes)Successfully wrote to /tmp/job-5wFaGF/index.html
thinking
We have written the file. Now we need to test it as per the requirements: verify movement, turning, collision, shooting feedback, wall rendering, restart. We'll use the browser tool to load the index.html and inspect. Let's first reload the page to see initial state. We'll use browser.action = 'reload'.
{"action":"reload"}{"console":["warning: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.","error: FOV is not defined"],"dom":"Health: 100 | Enemies: 3\nControls: W/A/S/D move, ArrowLeft/ArrowRight turn, Space shoot\nRestart","images":"model does not support images"}
thinking
There's an error: "FOV is not defined". Indeed, in the update function we used FOV but it's defined only in render? Actually we defined const FOV = Math.PI/3 inside render, but we also used it in update for shooting and enemy AI. We need to define FOV globally or at least in update. Also we used halfFOV in update but not defined. We need to fix: define FOV as a constant at top. Also we used shotDelay variable but defined shotDelay = 5; that's fine. Let's edit the file to fix these issues. We'll edit the script to add FOV constant and maybe halfFOV. We'll also need to ensure that the castRay function uses correct variable names (we used MAP_WIDTH, MAP_HEIGHT, MAP defined). Good. Let's edit the file. We'll replace the script tag content with corrected version. We'll do an edit operation: we need to find the script tag and replace its content. Since we wrote the whole file, we can replace the entire script content. Better to replace the whole script block. We'll locate the script tag: "<script>" to "</script>" and replace. Let's read the file to see exact content, then edit. We'll read the file first.
{"path":"/tmp/job-5wFaGF/index.html"}<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Raycasting Maze Shooter</title> <style> body { margin:0; padding:0; background:#111; color:#fff; font-family:sans-serif; overflow:hidden; } canvas { display:block; margin:0 auto; background:#000; } #hud { position:absolute; top:10px; left:10px; } #controls { position:absolute; bottom:10px; left:10px; font-size:14px; } #message { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center; background:rgba(0,0,0,0.7); padding:20px; border-radius:10px; } #restartBtn { position:absolute; top:60%; left:50%; transform:translate(-50%,-50%); padding:10px 20px; font-size:16px; cursor:pointer; } </style> </head> <body> <canvas id="gameCanvas"></canvas> <div id="hud"> Health: <span id="health">100</span> | Enemies: <span id="enemies">3</span> </div> <div id="controls">Controls: W/A/S/D move, ArrowLeft/ArrowRight turn, Space shoot</div> <div id="message"></div> <button id="restartBtn">Restart</button> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = 800; canvas.height = 600; const TILE_SIZE = 64; const MAP_WIDTH = 10; const MAP_HEIGHT = 10; const MAP = [ [1,1,1,1,1,1,1,1,1,1], [1,0,0,0,1,0,0,0,0,1], [1,0,1,0,1,0,1,1,0,1], [1,0,1,0,0,0,0,0,0,1], [1,0,1,1,1,0,1,1,0,1], [1,0,0,0,0,0,0,0,0,1], [1,1,1,0,1,1,1,0,1,1], [1,0,0,0,0,0,0,0,0,1], [1,0,1,1,1,1,1,0,1,1], [1,1,1,1,1,1,1,1,1,1] ]; // Textures const wallTexSize = 64; const wallTex = document.createElement('canvas'); wallTex.width = wallTexSize; wallTex.height = wallTexSize; const wallCtx = wallTex.getContext('2d'); // checker pattern for (let i = 0; i < wallTexSize; i++) { for (let j = 0; j < wallTexSize; j++) { const c = ((Math.floor(i/8) + Math.floor(j/8)) % 2) ? 200 : 100; wallCtx.fillStyle = `rgb(${c},${c},${c})`; wallCtx.fillRect(i, j, 1, 1); } } const enemyTexSize = 64; const enemyTex = document.createElement('canvas'); enemyTex.width = enemyTexSize; enemyTex.height = enemyTexSize; const enemyCtx = enemyTex.getContext('2d'); // simple enemy face enemyCtx.fillStyle = '#ff0000'; enemyCtx.beginPath(); enemyCtx.arc(enemyTexSize/2, enemyTexSize/2, enemyTexSize*0.4, 0, Math.PI*2); enemyCtx.fill(); enemyCtx.fillStyle = '#000000'; enemyCtx.beginPath(); enemyCtx.arc(enemyTexSize*0.3, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2); enemyCtx.fill(); enemyCtx.beginPath(); enemyCtx.arc(enemyTexSize*0.7, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2); enemyCtx.fill(); enemyCtx.beginPath(); enemyCtx.arc(enemyTexSize*0.5, enemyTexSize*0.6, enemyTexSize*0.2, 0, Math.PI); enemyCtx.strokeStyle = '#000000'; enemyCtx.lineWidth = 4; enemyCtx.stroke(); // Game state const player = { x: 1.5, // in tile units y: 1.5, angle: 0, // radians, pointing right health: 100, speed: 0.05, // tiles per frame rotSpeed: 0.05 // radians per frame }; const enemies = [ {x: 2.5, y: 2.5, health: 30, alive: true}, {x: 5.5, y: 5.5, health: 30, alive: true}, {x: 3.5, y: 7.5, health: 30, alive: true} ]; const exitTile = {x: 8, y: 1}; // in tile units let gameState = 'playing'; // 'playing', 'gameover', 'win' let shotCooldown = 0; const shotDelay = 5; // frames // Input const keys = {}; document.addEventListener('keydown', (e) => { keys[e.key] = true; }); document.addEventListener('keyup', (e) => { keys[e.key] = false; }); // Functions function isWall(x, y) { const mx = Math.floor(x); const my = Math.floor(y); if (mx < 0 || mx >= MAP_WIDTH || my < 0 || my >= MAP_HEIGHT) return true; return MAP[my][mx] === 1; } function castRay(rayAngle) { // DDA const sinA = Math.sin(rayAngle); const cosA = Math.cos(rayAngle); let mapX = Math.floor(player.x); let mapY = Math.floor(player.y); // length of ray from current position to next x or y-side let sideDistX, sideDistY; // length of ray from one x or y-side to next x or y-side const deltaDistX = Math.abs(1 / cosA); const deltaDistY = Math.abs(1 / sinA); let stepX, stepY; let hit = 0; // wall let side = 0; // 0 for x-side, 1 for y-side // calculate step and initial sideDist if (cosA < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; } if (sinA < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } // perform DDA 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_WIDTH || mapY < 0 || mapY >= MAP_HEIGHT) { hit = 1; // outside map considered wall break; } if (MAP[mapY][mapX] === 1) { hit = 1; } } // calculate distance let perpWallDist; if (side === 0) { perpWallDist = (mapX - player.x + (1 - stepX) / 2) / cosA; } else { perpWallDist = (mapY - player.y + (1 - stepY) / 2) / sinA; } // calculate wall x coordinate where the ray hit let wallX; if (side === 0) { wallX = player.y + perpWallDist * sinA; } else { wallX = player.x + perpWallDist * cosA; } wallX -= Math.floor(wallX); const texX = Math.floor(wallX * wallTexSize); if (side === 0 && cosA > 0) texX = wallTexSize - texX - 1; if (side === 1 && sinA < 0) texX = wallTexSize - texX - 1; return { distance: perpWallDist, side: side, texX: texX, mapX: mapX, mapY: mapY }; } function render() { // clear screen ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); // draw sky and floor (simple colors) ctx.fillStyle = '#70c1b3'; // sky ctx.fillRect(0, 0, canvas.width, canvas.height/2); ctx.fillStyle = '#92846e'; // floor ctx.fillRect(0, canvas.height/2, canvas.width, canvas.height/2); // cast rays const FOV = Math.PI/3; const halfFOV = FOV/2; for (let x = 0; x < canvas.width; x++) { const rayAngle = player.angle - halfFOV + (x / canvas.width) * FOV; const result = castRay(rayAngle); const perpDist = result.distance; // prevent div by zero if (perpDist < 0.0001) perpDist = 0.0001; // wall height const wallHeight = (TILE_SIZE / perpDist) * (canvas.height / 2); const drawStart = Math.max(0, Math.floor(canvas.height/2 - wallHeight/2)); const drawEnd = Math.min(canvas.height, Math.floor(canvas.height/2 + wallHeight/2)); // texture y coordinate step const texStep = wallTexSize / wallHeight; let texPos = (drawStart - canvas.height/2 + wallHeight/2) * texStep; const texX = result.texX; // draw wall slice for (let y = drawStart; y < drawEnd; y++) { const texY = Math.floor(texPos) & (wallTexSize-1); const pixel = wallCtx.getImageData(texX, texY, 1, 1).data; const darkness = 1 / (1 + perpDist * 0.05); // simple lighting const r = Math.floor(pixel[0] * darkness); const g = Math.floor(pixel[1] * darkness); const b = Math.floor(pixel[2] * darkness); ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillRect(x, y, 1, 1); texPos += texStep; } } // render sprites (enemies) // sort enemies by distance (far to near) const enemiesSorted = [...enemies].filter(e => e.alive).sort((a,b) => { const distA = Math.hypot(a.x - player.x, a.y - player.y); const distB = Math.hypot(b.x - player.x, b.y - player.y); return distB - distA; // farthest first }); for (const enemy of enemiesSorted) { const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.hypot(dx, dy); const angleToEnemy = Math.atan2(dy, dx) - player.angle; // normalize angle to [-pi, pi] while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI; while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI; const halfFOV = FOV/2; if (Math.abs(angleToEnemy) > halfFOV) continue; // outside FOV // check if enemy is behind a wall // cast ray towards enemy and see if wall is closer const blockRayAngle = player.angle + angleToEnemy; const blockResult = castRay(blockRayAngle); if (blockResult.distance < dist - 0.1) continue; // wall blocks // screen position const spriteScreenX = Math.floor((angleToEnemy / FOV + 0.5) * canvas.width); const spriteHeight = Math.abs((TILE_SIZE / dist) * canvas.height * 0.8); // scale factor const spriteWidth = spriteHeight; // square sprite const drawStartY = Math.max(0, Math.floor(canvas.height/2 - spriteHeight/2)); const drawEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + spriteHeight/2)); const drawStartX = Math.max(0, Math.floor(spriteScreenX - spriteWidth/2)); const drawEndX = Math.min(canvas.width, Math.floor(spriteScreenX + spriteWidth/2)); // draw enemy texture scaled to sprite // we'll draw column by column for simplicity const texWidth = enemyTexSize; const texHeight = enemyTexSize; for (let sx = drawStartX; sx < drawEndX; sx++) { const texX = Math.floor(((sx - drawStartX) / spriteWidth) * texWidth) % texWidth; for (let sy = drawStartY; sy < drawEndY; sy++) { const texY = Math.floor(((sy - drawStartY) / spriteHeight) * texHeight) % texHeight; const pixel = enemyCtx.getImageData(texX, texY, 1, 1).data; if (pixel[3] === 0) continue; // skip transparent (none in our texture) ctx.fillStyle = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`; ctx.fillRect(sx, sy, 1, 1); } } } // draw exit (simple flag) const exitDX = exitTile.x - player.x; const exitDY = exitTile.y - player.y; const exitDist = Math.hypot(exitDX, exitDY); const exitAngle = Math.atan2(exitDY, exitDX) - player.angle; while (exitAngle < -Math.PI) exitAngle += 2*Math.PI; while (exitAngle > Math.PI) exitAngle -= 2*Math.PI; if (Math.abs(exitAngle) < halfFOV) { const exitScreenX = Math.floor((exitAngle / FOV + 0.5) * canvas.width); const exitHeight = Math.abs((TILE_SIZE / exitDist) * canvas.height * 0.6); const exitWidth = exitHeight * 0.5; const exitStartY = Math.max(0, Math.floor(canvas.height/2 - exitHeight/2)); const exitEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + exitHeight/2)); const exitStartX = Math.max(0, Math.floor(exitScreenX - exitWidth/2)); const exitEndX = Math.min(canvas.width, Math.floor(exitScreenX + exitWidth/2)); ctx.fillStyle = '#00ff00'; for (let sx = exitStartX; sx < exitEndX; sx++) { for (let sy = exitStartY; sy < exitEndY; sy++) { ctx.fillRect(sx, sy, 1, 1); } } } // HUD document.getElementById('health').textContent = player.health; const aliveCount = enemies.filter(e => e.alive).length; document.getElementById('enemies').textContent = aliveCount; // message if (gameState === 'gameover') { document.getElementById('message').textContent = 'Game Over! Press Restart'; document.getElementById('message').style.display = 'block'; document.getElementById('restartBtn').style.display = 'block'; } else if (gameState === 'win') { document.getElementById('message').textContent = 'You Win! Press Restart'; document.getElementById('message').style.display = 'block'; document.getElementById('restartBtn').style.display = 'block'; } else { document.getElementById('message').style.display = 'none'; document.getElementById('restartBtn').style.display = 'none'; } } function update() { if (gameState !== 'playing') return; // shot cooldown if (shotCooldown > 0) shotCooldown--; // movement const moveSpeed = player.speed; const rotSpeed = player.rotSpeed; let dx = 0, dy = 0, da = 0; if (keys['w'] || keys['ArrowUp']) { dx += Math.cos(player.angle) * moveSpeed; dy += Math.sin(player.angle) * moveSpeed; } if (keys['s'] || keys['ArrowDown']) { dx -= Math.cos(player.angle) * moveSpeed; dy -= Math.sin(player.angle) * moveSpeed; } if (keys['a'] || keys['ArrowLeft']) { dx += Math.sin(player.angle) * moveSpeed; // strafe left dy -= Math.cos(player.angle) * moveSpeed; } if (keys['d'] || keys['ArrowRight']) { dx -= Math.sin(player.angle) * moveSpeed; // strafe right dy += Math.cos(player.angle) * moveSpeed; } if (keys['ArrowLeft']) da -= rotSpeed; // turn left if (keys['ArrowRight']) da += rotSpeed; // turn right // apply rotation player.angle += da; // keep angle in [0, 2π) if (player.angle < 0) player.angle += 2*Math.PI; if (player.angle >= 2*Math.PI) player.angle -= 2*Math.PI; // apply movement with collision const newX = player.x + dx; const newY = player.y + dy; if (!isWall(newX, player.y)) player.x = newX; if (!isWall(player.x, newY)) player.y = newY; // shooting if (keys[' '] && shotCooldown === 0) { shotCooldown = shotDelay; // cast ray from player at current angle const shotResult = castRay(player.angle); let hitEnemy = null; let hitDist = shotResult.distance; // check enemies for (const enemy of enemies) { if (!enemy.alive) continue; const ex = enemy.x - player.x; const ey = enemy.y - player.y; const dist = Math.hypot(ex, ey); const angleToEnemy = Math.atan2(ey, ex) - player.angle; while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI; while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI; const halfFOV = FOV/2; if (Math.abs(angleToEnemy) > halfFOV) continue; // check if enemy is in front of wall hit if (dist < hitDist) { // additional check: ensure enemy is roughly in line (within sprite width) // we'll accept if distance less than ray distance hitEnemy = enemy; hitDist = dist; } } if (hitEnemy) { hitEnemy.health -= 20; if (hitEnemy.health <= 0) { hitEnemy.alive = false; } } } // enemy AI for (const enemy of enemies) { if (!enemy.alive) continue; const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.hypot(dx, dy); if (dist > 6) continue; // only attack if close const angleToEnemy = Math.atan2(dy, dx) - player.angle; while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI; while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI; const halfFOV = FOV/2; if (Math.abs(angleToEnemy) > halfFOV) continue; // line of sight check const losAngle = player.angle + angleToEnemy; const losResult = castRay(losAngle); if (losResult.distance < dist - 0.1) continue; // blocked by wall // attack cooldown per enemy if (!enemy.lastAttack) enemy.lastAttack = 0; enemy.lastAttack--; if (enemy.lastAttack <= 0) { player.health -= 10; enemy.lastAttack = 30; // half second at 60fps if (player.health <= 0) { gameState = 'gameover'; } } } // check win condition: all enemies dead and player at exit const allDead = enemies.every(e => !e.alive); const atExit = Math.hypot(player.x - exitTile.x, player.y - exitTile.y) < 0.5; if (allDead && atExit) { gameState = 'win'; } } function gameLoop() { update(); render(); requestAnimationFrame(gameLoop); } // restart button document.getElementById('restartBtn').addEventListener('click', () => { // reset game state player.x = 1.5; player.y = 1.5; player.angle = 0; player.health = 100; enemies.forEach(e => { e.health = 30; e.alive = true; delete e.lastAttack; }); gameState = 'playing'; }); // start loop gameLoop(); </script> </body> </html>
thinking
Now we need to fix the FOV issue. In the update function we reference FOV and halfFOV but they are not defined there. They are defined inside render() only. We need to define them globally or at least in update.
Let's add global constants at top: const FOV = Math.PI/3; const HALF_FOV = FOV/2;
Then use them in update and render (render can still use them or compute). We'll edit the file accordingly.
We'll replace the script content with a corrected version.
We'll do an edit operation: we need to replace the entire script tag content. Let's do that.
We'll write a new script content and replace.
But we need to be careful: we need to match the exact old script tag content to replace. Since we have the whole file, we can replace from "<script>" to "</script>" inclusive? The edit tool requires oldText to match exactly. We'll extract the old script content (including the tags) and replace with new script content.
Let's get the old script content from the file we just read. We'll extract the lines between <script> and </script> including the tags.
We can do a simple replace: we'll create new script content and replace the old one.
Let's construct new script content.
We'll copy the old script and add the constants at the top after the other constants.
We'll also need to update render and update to use the constants.
Let's write the new script.
We'll do:
```html
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const TILE_SIZE = 64;
const MAP_WIDTH = 10;
const MAP_HEIGHT = 10;
const FOV = Math.PI/3;
const HALF_FOV = FOV/2;
const MAP = [ ... ];
// ... rest same but replace FOV and halfFOV references with constants.
```
We'll need to replace occurrences of `const FOV = Math.PI/3;` inside render with just using the constant? Actually we can keep the line but it's redundant; we can remove it. Similarly for halfFOV.
We'll edit the script accordingly.
Let's produce the new script content and replace.
We'll use edit tool with oldText being the entire script block from the original file, and newText being our corrected script.
Let's first get the exact old script content (including the <script> and </script> tags). We'll copy from the read output.
From the read output, the script starts at line after </style>? Actually we have the whole file. Let's copy the script block exactly as it appears.
We'll extract from the read output:
```
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const TILE_SIZE = 64;
const MAP_WIDTH = 10;
const MAP_HEIGHT = 10;
const MAP = [
[1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,0,0,0,0,1],
[1,0,1,0,1,0,1,1,0,1],
[1,0,1,0,0,0,0,0,0,1],
[1,0,1,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,1,0,1,1,1,0,1,1],
[1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,0,1,1],
[1,1,1,1,1,1,1,1,1,1]
];
// Textures
const wallTexSize = 64;
const wallTex = document.createElement('canvas');
wallTex.width = wallTexSize;
wallTex.height = wallTexSize;
const wallCtx = wallTex.getContext('2d');
// checker pattern
for (let i = 0; i < wallTexSize; i++) {
for (let j = 0; j < wallTexSize; j++) {
const c = ((Math.floor(i/8) + Math.floor(j/8)) % 2) ? 200 : 100;
wallCtx.fillStyle = `rgb(${c},${c},${c})`;
wallCtx.fillRect(i, j, 1, 1);
}
}
const enemyTexSize = 64;
const enemyTex = document.createElement('canvas');
enemyTex.width = enemyTexSize;
enemyTex.height = enemyTexSize;
const enemyCtx = enemyTex.getContext('2d');
// simple enemy face
enemyCtx.fillStyle = '#ff0000';
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize/2, enemyTexSize/2, enemyTexSize*0.4, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.fillStyle = '#000000';
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.3, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.7, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.5, enemyTexSize*0.6, enemyTexSize*0.2, 0, Math.PI);
enemyCtx.strokeStyle = '#000000';
enemyCtx.lineWidth = 4;
enemyCtx.stroke();
// Game state
const player = {
x: 1.5, // in tile units
y: 1.5,
angle: 0, // radians, pointing right
health: 100,
speed: 0.05, // tiles per frame
rotSpeed: 0.05 // radians per frame
};
const enemies = [
{x: 2.5, y: 2.5, health: 30, alive: true},
{x: 5.5, y: 5.5, health: 30, alive: true},
{x: 3.5, y: 7.5, health: 30, alive: true}
];
const exitTile = {x: 8, y: 1}; // in tile units
let gameState = 'playing'; // 'playing', 'gameover', 'win'
let shotCooldown = 0;
const shotDelay = 5; // frames
// Input
const keys = {};
document.addEventListener('keydown', (e) => { keys[e.key] = true; });
document.addEventListener('keyup', (e) => { keys[e.key] = false; });
// Functions
function isWall(x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (mx < 0 || mx >= MAP_WIDTH || my < 0 || my >= MAP_HEIGHT) return true;
return MAP[my][mx] === 1;
}
function castRay(rayAngle) {
// DDA
const sinA = Math.sin(rayAngle);
const cosA = Math.cos(rayAngle);
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
// length of ray from current position to next x or y-side
let sideDistX, sideDistY;
// length of ray from one x or y-side to next x or y-side
const deltaDistX = Math.abs(1 / cosA);
const deltaDistY = Math.abs(1 / sinA);
let stepX, stepY;
let hit = 0; // wall
let side = 0; // 0 for x-side, 1 for y-side
// calculate step and initial sideDist
if (cosA < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
}
if (sinA < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
}
// perform DDA
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_WIDTH || mapY < 0 || mapY >= MAP_HEIGHT) {
hit = 1; // outside map considered wall
break;
}
if (MAP[mapY][mapX] === 1) {
hit = 1;
}
}
// calculate distance
let perpWallDist;
if (side === 0) {
perpWallDist = (mapX - player.x + (1 - stepX) / 2) / cosA;
} else {
perpWallDist = (mapY - player.y + (1 - stepY) / 2) / sinA;
}
// calculate wall x coordinate where the ray hit
let wallX;
if (side === 0) {
wallX = player.y + perpWallDist * sinA;
} else {
wallX = player.x + perpWallDist * cosA;
}
wallX -= Math.floor(wallX);
const texX = Math.floor(wallX * wallTexSize);
if (side === 0 && cosA > 0) texX = wallTexSize - texX - 1;
if (side === 1 && sinA < 0) texX = wallTexSize - texX - 1;
return {
distance: perpWallDist,
side: side,
texX: texX,
mapX: mapX,
mapY: mapY
};
}
function render() {
// clear screen
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw sky and floor (simple colors)
ctx.fillStyle = '#70c1b3'; // sky
ctx.fillRect(0, 0, canvas.width, canvas.height/2);
ctx.fillStyle = '#92846e'; // floor
ctx.fillRect(0, canvas.height/2, canvas.width, canvas.height/2);
// cast rays
const FOV = Math.PI/3;
const halfFOV = FOV/2;
for (let x = 0; x < canvas.width; x++) {
const rayAngle = player.angle - halfFOV + (x / canvas.width) * FOV;
const result = castRay(rayAngle);
const perpDist = result.distance;
// prevent div by zero
if (perpDist < 0.0001) perpDist = 0.0001;
// wall height
const wallHeight = (TILE_SIZE / perpDist) * (canvas.height / 2);
const drawStart = Math.max(0, Math.floor(canvas.height/2 - wallHeight/2));
const drawEnd = Math.min(canvas.height, Math.floor(canvas.height/2 + wallHeight/2));
// texture y coordinate step
const texStep = wallTexSize / wallHeight;
let texPos = (drawStart - canvas.height/2 + wallHeight/2) * texStep;
const texX = result.texX;
// draw wall slice
for (let y = drawStart; y < drawEnd; y++) {
const texY = Math.floor(texPos) & (wallTexSize-1);
const pixel = wallCtx.getImageData(texX, texY, 1, 1).data;
const darkness = 1 / (1 + perpDist * 0.05); // simple lighting
const r = Math.floor(pixel[0] * darkness);
const g = Math.floor(pixel[1] * darkness);
const b = Math.floor(pixel[2] * darkness);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, 1, 1);
texPos += texStep;
}
}
// render sprites (enemies)
// sort enemies by distance (far to near)
const enemiesSorted = [...enemies].filter(e => e.alive).sort((a,b) => {
const distA = Math.hypot(a.x - player.x, a.y - player.y);
const distB = Math.hypot(b.x - player.x, b.y - player.y);
return distB - distA; // farthest first
});
for (const enemy of enemiesSorted) {
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.hypot(dx, dy);
const angleToEnemy = Math.atan2(dy, dx) - player.angle;
// normalize angle to [-pi, pi]
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
const halfFOV = FOV/2;
if (Math.abs(angleToEnemy) > halfFOV) continue; // outside FOV
// check if enemy is behind a wall
// cast ray towards enemy and see if wall is closer
const blockRayAngle = player.angle + angleToEnemy;
const blockResult = castRay(blockRayAngle);
if (blockResult.distance < dist - 0.1) continue; // wall blocks
// screen position
const spriteScreenX = Math.floor((angleToEnemy / FOV + 0.5) * canvas.width);
const spriteHeight = Math.abs((TILE_SIZE / dist) * canvas.height * 0.8); // scale factor
const spriteWidth = spriteHeight; // square sprite
const drawStartY = Math.max(0, Math.floor(canvas.height/2 - spriteHeight/2));
const drawEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + spriteHeight/2));
const drawStartX = Math.max(0, Math.floor(spriteScreenX - spriteWidth/2));
const drawEndX = Math.min(canvas.width, Math.floor(spriteScreenX + spriteWidth/2));
// draw enemy texture scaled to sprite
// we'll draw column by column for simplicity
const texWidth = enemyTexSize;
const texHeight = enemyTexSize;
for (let sx = drawStartX; sx < drawEndX; sx++) {
const texX = Math.floor(((sx - drawStartX) / spriteWidth) * texWidth) % texWidth;
for (let sy = drawStartY; sy < drawEndY; sy++) {
const texY = Math.floor(((sy - drawStartY) / spriteHeight) * texHeight) % texHeight;
const pixel = enemyCtx.getImageData(texX, texY, 1, 1).data;
if (pixel[3] === 0) continue; // skip transparent (none in our texture)
ctx.fillStyle = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;
ctx.fillRect(sx, sy, 1, 1);
}
}
}
// draw exit (simple flag)
const exitDX = exitTile.x - player.x;
const exitDY = exitTile.y - player.y;
const exitDist = Math.hypot(exitDX, exitDY);
const exitAngle = Math.atan2(exitDY, exitDX) - player.angle;
while (exitAngle < -Math.PI) exitAngle += 2*Math.PI;
while (exitAngle > Math.PI) exitAngle -= 2*Math.PI;
if (Math.abs(exitAngle) < halfFOV) {
const exitScreenX = Math.floor((exitAngle / FOV + 0.5) * canvas.width);
const exitHeight = Math.abs((TILE_SIZE / exitDist) * canvas.height * 0.6);
const exitWidth = exitHeight * 0.5;
const exitStartY = Math.max(0, Math.floor(canvas.height/2 - exitHeight/2));
const exitEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + exitHeight/2));
const exitStartX = Math.max(0, Math.floor(exitScreenX - exitWidth/2));
const exitEndX = Math.min(canvas.width, Math.floor(exitScreenX + exitWidth/2));
ctx.fillStyle = '#00ff00';
for (let sx = exitStartX; sx < exitEndX; sx++) {
for (let sy = exitStartY; sy < exitEndY; sy++) {
ctx.fillRect(sx, sy, 1, 1);
}
}
}
// HUD
document.getElementById('health').textContent = player.health;
const aliveCount = enemies.filter(e => e.alive).length;
document.getElementById('enemies').textContent = aliveCount;
// message
if (gameState === 'gameover') {
document.getElementById('message').textContent = 'Game Over! Press Restart';
document.getElementById('message').style.display = 'block';
document.getElementById('restartBtn').style.display = 'block';
} else if (gameState === 'win') {
document.getElementById('message').textContent = 'You Win! Press Restart';
document.getElementById('message').style.display = 'block';
document.getElementById('restartBtn').style.display = 'block';
} else {
document.getElementById('message').style.display = 'none';
document.getElementById('restartBtn').style.display = 'none';
}
}
function update() {
if (gameState !== 'playing') return;
// shot cooldown
if (shotCooldown > 0) shotCooldown--;
// movement
const moveSpeed = player.speed;
const rotSpeed = player.rotSpeed;
let dx = 0, dy = 0, da = 0;
if (keys['w'] || keys['ArrowUp']) {
dx += Math.cos(player.angle) * moveSpeed;
dy += Math.sin(player.angle) * moveSpeed;
}
if (keys['s'] || keys['ArrowDown']) {
dx -= Math.cos(player.angle) * moveSpeed;
dy -= Math.sin(player.angle) * moveSpeed;
}
if (keys['a'] || keys['ArrowLeft']) {
dx += Math.sin(player.angle) * moveSpeed; // strafe left
dy -= Math.cos(player.angle) * moveSpeed;
}
if (keys['d'] || keys['ArrowRight']) {
dx -= Math.sin(player.angle) * moveSpeed; // strafe right
dy += Math.cos(player.angle) * moveSpeed;
}
if (keys['ArrowLeft']) da -= rotSpeed; // turn left
if (keys['ArrowRight']) da += rotSpeed; // turn right
// apply rotation
player.angle += da;
// keep angle in [0, 2π)
if (player.angle < 0) player.angle += 2*Math.PI;
if (player.angle >= 2*Math.PI) player.angle -= 2*Math.PI;
// apply movement with collision
const newX = player.x + dx;
const newY = player.y + dy;
if (!isWall(newX, player.y)) player.x = newX;
if (!isWall(player.x, newY)) player.y = newY;
// shooting
if (keys[' '] && shotCooldown === 0) {
shotCooldown = shotDelay;
// cast ray from player at current angle
const shotResult = castRay(player.angle);
let hitEnemy = null;
let hitDist = shotResult.distance;
// check enemies
for (const enemy of enemies) {
if (!enemy.alive) continue;
const ex = enemy.x - player.x;
const ey = enemy.y - player.y;
const dist = Math.hypot(ex, ey);
const angleToEnemy = Math.atan2(ey, ex) - player.angle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
const halfFOV = FOV/2;
if (Math.abs(angleToEnemy) > halfFOV) continue;
// check if enemy is in front of wall hit
if (dist < hitDist) {
// additional check: ensure enemy is roughly in line (within sprite width)
// we'll accept if distance less than ray distance
hitEnemy = enemy;
hitDist = dist;
}
}
if (hitEnemy) {
hitEnemy.health -= 20;
if (hitEnemy.health <= 0) {
hitEnemy.alive = false;
}
}
}
// enemy AI
for (const enemy of enemies) {
if (!enemy.alive) continue;
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > 6) continue; // only attack if close
const angleToEnemy = Math.atan2(dy, dx) - player.angle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
const halfFOV = FOV/2;
if (Math.abs(angleToEnemy) > halfFOV) continue;
// line of sight check
const losAngle = player.angle + angleToEnemy;
const losResult = castRay(losAngle);
if (losResult.distance < dist - 0.1) continue; // blocked by wall
// attack cooldown per enemy
if (!enemy.lastAttack) enemy.lastAttack = 0;
enemy.lastAttack--;
if (enemy.lastAttack <= 0) {
player.health -= 10;
enemy.lastAttack = 30; // half second at 60fps
if (player.health <= 0) {
gameState = 'gameover';
}
}
}
// check win condition: all enemies dead and player at exit
const allDead = enemies.every(e => !e.alive);
const atExit = Math.hypot(player.x - exitTile.x, player.y - exitTile.y) < 0.5;
if (allDead && atExit) {
gameState = 'win';
}
}
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// restart button
document.getElementById('restartBtn').addEventListener('click', () => {
// reset game state
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
player.health = 100;
enemies.forEach(e => {
e.health = 30;
e.alive = true;
delete e.lastAttack;
});
gameState = 'playing';
});
// start loop
gameLoop();
</script>
```
Now we need to replace this entire block with a corrected version where we add FOV and HALF_FOV constants and remove the duplicate definitions inside render and update.
Let's create the new script content.
We'll write:
```html
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const TILE_SIZE = 64;
const MAP_WIDTH = 10;
const MAP_HEIGHT = 10;
const FOV = Math.PI/3;
const HALF_FOV = FOV/2;
const MAP = [
[1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,0,0,0,0,1],
[1,0,1,0,1,0,1,1,0,1],
[1,0,1,0,0,0,0,0,0,1],
[1,0,1,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,1,0,1,1,1,0,1,1],
[1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,0,1,1],
[1,1,1,1,1,1,1,1,1,1]
];
// Textures
const wallTexSize = 64;
const wallTex = document.createElement('canvas');
wallTex.width = wallTexSize;
wallTex.height = wallTexSize;
const wallCtx = wallTex.getContext('2d');
// checker pattern
for (let i = 0; i < wallTexSize; i++) {
for (let j = 0; j < wallTexSize; j++) {
const c = ((Math.floor(i/8) + Math.floor(j/8)) % 2) ? 200 : 100;
wallCtx.fillStyle = `rgb(${c},${c},${c})`;
wallCtx.fillRect(i, j, 1, 1);
}
}
const enemyTexSize = 64;
const enemyTex = document.createElement('canvas');
enemyTex.width = enemyTexSize;
enemyTex.height = enemyTexSize;
const enemyCtx = enemyTex.getContext('2d');
// simple enemy face
enemyCtx.fillStyle = '#ff0000';
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize/2, enemyTexSize/2, enemyTexSize*0.4, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.fillStyle = '#000000';
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.3, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.7, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.5, enemyTexSize*0.6, enemyTexSize*0.2, 0, Math.PI);
enemyCtx.strokeStyle = '#000000';
enemyCtx.lineWidth = 4;
enemyCtx.stroke();
// Game state
const player = {
x: 1.5, // in tile units
y: 1.5,
angle: 0, // radians, pointing right
health: 100,
speed: 0.05, // tiles per frame
rotSpeed: 0.05 // radians per frame
};
const enemies = [
{x: 2.5, y: 2.5, health: 30, alive: true},
{x: 5.5, y: 5.5, health: 30, alive: true},
{x: 3.5, y: 7.5, health: 30, alive: true}
];
const exitTile = {x: 8, y: 1}; // in tile units
let gameState = 'playing'; // 'playing', 'gameover', 'win'
let shotCooldown = 0;
const shotDelay = 5; // frames
// Input
const keys = {};
document.addEventListener('keydown', (e) => { keys[e.key] = true; });
document.addEventListener('keyup', (e) => { keys[e.key] = false; });
// Functions
function isWall(x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (mx < 0 || mx >= MAP_WIDTH || my < 0 || my >= MAP_HEIGHT) return true;
return MAP[my][mx] === 1;
}
function castRay(rayAngle) {
// DDA
const sinA = Math.sin(rayAngle);
const cosA = Math.cos(rayAngle);
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
// length of ray from current position to next x or y-side
let sideDistX, sideDistY;
// length of ray from one x or y-side to next x or y-side
const deltaDistX = Math.abs(1 / cosA);
const deltaDistY = Math.abs(1 / sinA);
let stepX, stepY;
let hit = 0; // wall
let side = 0; // 0 for x-side, 1 for y-side
// calculate step and initial sideDist
if (cosA < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
}
if (sinA < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
}
// perform DDA
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_WIDTH || mapY < 0 || mapY >= MAP_HEIGHT) {
hit = 1; // outside map considered wall
break;
}
if (MAP[mapY][mapX] === 1) {
hit = 1;
}
}
// calculate distance
let perpWallDist;
if (side === 0) {
perpWallDist = (mapX - player.x + (1 - stepX) / 2) / cosA;
} else {
perpWallDist = (mapY - player.y + (1 - stepY) / 2) / sinA;
}
// calculate wall x coordinate where the ray hit
let wallX;
if (side === 0) {
wallX = player.y + perpWallDist * sinA;
} else {
wallX = player.x + perpWallDist * cosA;
}
wallX -= Math.floor(wallX);
const texX = Math.floor(wallX * wallTexSize);
if (side === 0 && cosA > 0) texX = wallTexSize - texX - 1;
if (side === 1 && sinA < 0) texX = wallTexSize - texX - 1;
return {
distance: perpWallDist,
side: side,
texX: texX,
mapX: mapX,
mapY: mapY
};
}
function render() {
// clear screen
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw sky and floor (simple colors)
ctx.fillStyle = '#70c1b3'; // sky
ctx.fillRect(0, 0, canvas.width, canvas.height/2);
ctx.fillStyle = '#92846e'; // floor
ctx.fillRect(0, canvas.height/2, canvas.width, canvas.height/2);
// cast rays
for (let x = 0; x < canvas.width; x++) {
const rayAngle = player.angle - HALF_FOV + (x / canvas.width) * FOV;
const result = castRay(rayAngle);
const perpDist = result.distance;
// prevent div by zero
if (perpDist < 0.0001) perpDist = 0.0001;
// wall height
const wallHeight = (TILE_SIZE / perpDist) * (canvas.height / 2);
const drawStart = Math.max(0, Math.floor(canvas.height/2 - wallHeight/2));
const drawEnd = Math.min(canvas.height, Math.floor(canvas.height/2 + wallHeight/2));
// texture y coordinate step
const texStep = wallTexSize / wallHeight;
let texPos = (drawStart - canvas.height/2 + wallHeight/2) * texStep;
const texX = result.texX;
// draw wall slice
for (let y = drawStart; y < drawEnd; y++) {
const texY = Math.floor(texPos) & (wallTexSize-1);
const pixel = wallCtx.getImageData(texX, texY, 1, 1).data;
const darkness = 1 / (1 + perpDist * 0.05); // simple lighting
const r = Math.floor(pixel[0] * darkness);
const g = Math.floor(pixel[1] * darkness);
const b = Math.floor(pixel[2] * darkness);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, 1, 1);
texPos += texStep;
}
}
// render sprites (enemies)
// sort enemies by distance (far to near)
const enemiesSorted = [...enemies].filter(e => e.alive).sort((a,b) => {
const distA = Math.hypot(a.x - player.x, a.y - player.y);
const distB = Math.hypot(b.x - player.x, b.y - player.y);
return distB - distA; // farthest first
});
for (const enemy of enemiesSorted) {
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.hypot(dx, dy);
const angleToEnemy = Math.atan2(dy, dx) - player.angle;
// normalize angle to [-pi, pi]
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
if (Math.abs(angleToEnemy) > HALF_FOV) continue; // outside FOV
// check if enemy is behind a wall
// cast ray towards enemy and see if wall is closer
const blockRayAngle = player.angle + angleToEnemy;
const blockResult = castRay(blockRayAngle);
if (blockResult.distance < dist - 0.1) continue; // wall blocks
// screen position
const spriteScreenX = Math.floor((angleToEnemy / FOV + 0.5) * canvas.width);
const spriteHeight = Math.abs((TILE_SIZE / dist) * canvas.height * 0.8); // scale factor
const spriteWidth = spriteHeight; // square sprite
const drawStartY = Math.max(0, Math.floor(canvas.height/2 - spriteHeight/2));
const drawEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + spriteHeight/2));
const drawStartX = Math.max(0, Math.floor(spriteScreenX - spriteWidth/2));
const drawEndX = Math.min(canvas.width, Math.floor(spriteScreenX + spriteWidth/2));
// draw enemy texture scaled to sprite
// we'll draw column by column for simplicity
const texWidth = enemyTexSize;
const texHeight = enemyTexSize;
for (let sx = drawStartX; sx < drawEndX; sx++) {
const texX = Math.floor(((sx - drawStartX) / spriteWidth) * texWidth) % texWidth;
for (let sy = drawStartY; sy < drawEndY; sy++) {
const texY = Math.floor(((sy - drawStartY) / spriteHeight) * texHeight) % texHeight;
const pixel = enemyCtx.getImageData(texX, texY, 1, 1).data;
if (pixel[3] === 0) continue; // skip transparent (none in our texture)
ctx.fillStyle = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;
ctx.fillRect(sx, sy, 1, 1);
}
}
}
// draw exit (simple flag)
const exitDX = exitTile.x - player.x;
const exitDY = exitTile.y - player.y;
const exitDist = Math.hypot(exitDX, exitDY);
const exitAngle = Math.atan2(exitDY, exitDX) - player.angle;
while (exitAngle < -Math.PI) exitAngle += 2*Math.PI;
while (exitAngle > Math.PI) exitAngle -= 2*Math.PI;
if (Math.abs(exitAngle) < HALF_FOV) {
const exitScreenX = Math.floor((exitAngle / FOV + 0.5) * canvas.width);
const exitHeight = Math.abs((TILE_SIZE / exitDist) * canvas.height * 0.6);
const exitWidth = exitHeight * 0.5;
const exitStartY = Math.max(0, Math.floor(canvas.height/2 - exitHeight/2));
const exitEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + exitHeight/2));
const exitStartX = Math.max(0, Math.floor(exitScreenX - exitWidth/2));
const exitEndX = Math.min(canvas.width, Math.floor(exitScreenX + exitWidth/2));
ctx.fillStyle = '#00ff00';
for (let sx = exitStartX; sx < exitEndX; sx++) {
for (let sy = exitStartY; sy < exitEndY; sy++) {
ctx.fillRect(sx, sy, 1, 1);
}
}
}
// HUD
document.getElementById('health').textContent = player.health;
const aliveCount = enemies.filter(e => e.alive).length;
document.getElementById('enemies').textContent = aliveCount;
// message
if (gameState === 'gameover') {
document.getElementById('message').textContent = 'Game Over! Press Restart';
document.getElementById('message').style.display = 'block';
document.getElementById('restartBtn').style.display = 'block';
} else if (gameState === 'win') {
document.getElementById('message').textContent = 'You Win! Press Restart';
document.getElementById('message').style.display = 'block';
document.getElementById('restartBtn').style.display = 'block';
} else {
document.getElementById('message').style.display = 'none';
document.getElementById('restartBtn').style.display = 'none';
}
}
function update() {
if (gameState !== 'playing') return;
// shot cooldown
if (shotCooldown > 0) shotCooldown--;
// movement
const moveSpeed = player.speed;
const rotSpeed = player.rotSpeed;
let dx = 0, dy = 0, da = 0;
if (keys['w'] || keys['ArrowUp']) {
dx += Math.cos(player.angle) * moveSpeed;
dy += Math.sin(player.angle) * moveSpeed;
}
if (keys['s'] || keys['ArrowDown']) {
dx -= Math.cos(player.angle) * moveSpeed;
dy -= Math.sin(player.angle) * moveSpeed;
}
if (keys['a'] || keys['ArrowLeft']) {
dx += Math.sin(player.angle) * moveSpeed; // strafe left
dy -= Math.cos(player.angle) * moveSpeed;
}
if (keys['d'] || keys['ArrowRight']) {
dx -= Math.sin(player.angle) * moveSpeed; // strafe right
dy += Math.cos(player.angle) * moveSpeed;
}
if (keys['ArrowLeft']) da -= rotSpeed; // turn left
if (keys['ArrowRight']) da += rotSpeed; // turn right
// apply rotation
player.angle += da;
// keep angle in [0, 2π)
if (player.angle < 0) player.angle += 2*Math.PI;
if (player.angle >= 2*Math.PI) player.angle -= 2*Math.PI;
// apply movement with collision
const newX = player.x + dx;
const newY = player.y + dy;
if (!isWall(newX, player.y)) player.x = newX;
if (!isWall(player.x, newY)) player.y = newY;
// shooting
if (keys[' '] && shotCooldown === 0) {
shotCooldown = shotDelay;
// cast ray from player at current angle
const shotResult = castRay(player.angle);
let hitEnemy = null;
let hitDist = shotResult.distance;
// check enemies
for (const enemy of enemies) {
if (!enemy.alive) continue;
const ex = enemy.x - player.x;
const ey = enemy.y - player.y;
const dist = Math.hypot(ex, ey);
const angleToEnemy = Math.atan2(ey, ex) - player.angle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
if (Math.abs(angleToEnemy) > HALF_FOV) continue;
// check if enemy is in front of wall hit
if (dist < hitDist) {
// additional check: ensure enemy is roughly in line (within sprite width)
// we'll accept if distance less than ray distance
hitEnemy = enemy;
hitDist = dist;
}
}
if (hitEnemy) {
hitEnemy.health -= 20;
if (hitEnemy.health <= 0) {
hitEnemy.alive = false;
}
}
}
// enemy AI
for (const enemy of enemies) {
if (!enemy.alive) continue;
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > 6) continue; // only attack if close
const angleToEnemy = Math.atan2(dy, dx) - player.angle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
if (Math.abs(angleToEnemy) > HALF_FOV) continue;
// line of sight check
const losAngle = player.angle + angleToEnemy;
const losResult = castRay(losAngle);
if (losResult.distance < dist - 0.1) continue; // blocked by wall
// attack cooldown per enemy
if (!enemy.lastAttack) enemy.lastAttack = 0;
enemy.lastAttack--;
if (enemy.lastAttack <= 0) {
player.health -= 10;
enemy.lastAttack = 30; // half second at 60fps
if (player.health <= 0) {
gameState = 'gameover';
}
}
}
// check win condition: all enemies dead and player at exit
const allDead = enemies.every(e => !e.alive);
const atExit = Math.hypot(player.x - exitTile.x, player.y - exitTile.y) < 0.5;
if (allDead && atExit) {
gameState = 'win';
}
}
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// restart button
document.getElementById('restartBtn').addEventListener('click', () => {
// reset game state
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
player.health = 100;
enemies.forEach(e => {
e.health = 30;
e.alive = true;
delete e.lastAttack;
});
gameState = 'playing';
});
// start loop
gameLoop();
</script>
```
Now we need to replace the old script block with this new one.
We'll use edit tool: path to file, edits array with one entry: oldText = the old script block (including <script> and </script>), newText = the new script block.
Let's get the old script block exactly from the file we read earlier. We'll copy it from the read output (including the <script> and </script> lines). We'll need to include the indentation and newlines exactly.
We'll construct the oldScript string.
But we can also just replace the whole file with a new version using write, but we already have the file and we want to keep the rest (HTML, CSS) unchanged. Using edit is safer.
Let's do edit.
We'll need to ensure the oldText matches exactly. We'll copy from the read output we got earlier, but we need to include the exact characters.
Let's copy the script block from the read output we saw earlier (the whole file). We'll extract the script part.
From the read output, the script starts at line 13? Actually after the </style> line and before </body>. Let's just copy the entire script tag content we saw.
We'll create a string:
```
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const TILE_SIZE = 64;
const MAP_WIDTH = 10;
const MAP_HEIGHT = 10;
const MAP = [
[1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,0,0,0,0,1],
[1,0,1,0,1,0,1,1,0,1],
[1,0,1,0,0,0,0,0,0,1],
[1,0,1,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,1],
[1,1,1,0,1,1,1,0,1,1],
[1,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,0,1,1],
[1,1,1,1,1,1,1,1,1,1]
];
// Textures
const wallTexSize = 64;
const wallTex = document.createElement('canvas');
wallTex.width = wallTexSize;
wallTex.height = wallTexSize;
const wallCtx = wallTex.getContext('2d');
// checker pattern
for (let i = 0; i < wallTexSize; i++) {
for (let j = 0; j < wallTexSize; j++) {
const c = ((Math.floor(i/8) + Math.floor(j/8)) % 2) ? 200 : 100;
wallCtx.fillStyle = `rgb(${c},${c},${c})`;
wallCtx.fillRect(i, j, 1, 1);
}
}
const enemyTexSize = 64;
const enemyTex = document.createElement('canvas');
enemyTex.width = enemyTexSize;
enemyTex.height = enemyTexSize;
const enemyCtx = enemyTex.getContext('2d');
// simple enemy face
enemyCtx.fillStyle = '#ff0000';
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize/2, enemyTexSize/2, enemyTexSize*0.4, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.fillStyle = '#000000';
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.3, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.7, enemyTexSize*0.3, enemyTexSize*0.08, 0, Math.PI*2);
enemyCtx.fill();
enemyCtx.beginPath();
enemyCtx.arc(enemyTexSize*0.5, enemyTexSize*0.6, enemyTexSize*0.2, 0, Math.PI);
enemyCtx.strokeStyle = '#000000';
enemyCtx.lineWidth = 4;
enemyCtx.stroke();
// Game state
const player = {
x: 1.5, // in tile units
y: 1.5,
angle: 0, // radians, pointing right
health: 100,
speed: 0.05, // tiles per frame
rotSpeed: 0.05 // radians per frame
};
const enemies = [
{x: 2.5, y: 2.5, health: 30, alive: true},
{x: 5.5, y: 5.5, health: 30, alive: true},
{x: 3.5, y: 7.5, health: 30, alive: true}
];
const exitTile = {x: 8, y: 1}; // in tile units
let gameState = 'playing'; // 'playing', 'gameover', 'win'
let shotCooldown = 0;
const shotDelay = 5; // frames
// Input
const keys = {};
document.addEventListener('keydown', (e) => { keys[e.key] = true; });
document.addEventListener('keyup', (e) => { keys[e.key] = false; });
// Functions
function isWall(x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (mx < 0 || mx >= MAP_WIDTH || my < 0 || my >= MAP_HEIGHT) return true;
return MAP[my][mx] === 1;
}
function castRay(rayAngle) {
// DDA
const sinA = Math.sin(rayAngle);
const cosA = Math.cos(rayAngle);
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
// length of ray from current position to next x or y-side
let sideDistX, sideDistY;
// length of ray from one x or y-side to next x or y-side
const deltaDistX = Math.abs(1 / cosA);
const deltaDistY = Math.abs(1 / sinA);
let stepX, stepY;
let hit = 0; // wall
let side = 0; // 0 for x-side, 1 for y-side
// calculate step and initial sideDist
if (cosA < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
}
if (sinA < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
}
// perform DDA
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_WIDTH || mapY < 0 || mapY >= MAP_HEIGHT) {
hit = 1; // outside map considered wall
break;
}
if (MAP[mapY][mapX] === 1) {
hit = 1;
}
}
// calculate distance
let perpWallDist;
if (side === 0) {
perpWallDist = (mapX - player.x + (1 - stepX) / 2) / cosA;
} else {
perpWallDist = (mapY - player.y + (1 - stepY) / 2) / sinA;
}
// calculate wall x coordinate where the ray hit
let wallX;
if (side === 0) {
wallX = player.y + perpWallDist * sinA;
} else {
wallX = player.x + perpWallDist * cosA;
}
wallX -= Math.floor(wallX);
const texX = Math.floor(wallX * wallTexSize);
if (side === 0 && cosA > 0) texX = wallTexSize - texX - 1;
if (side === 1 && sinA < 0) texX = wallTexSize - texX - 1;
return {
distance: perpWallDist,
side: side,
texX: texX,
mapX: mapX,
mapY: mapY
};
}
function render() {
// clear screen
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw sky and floor (simple colors)
ctx.fillStyle = '#70c1b3'; // sky
ctx.fillRect(0, 0, canvas.width, canvas.height/2);
ctx.fillStyle = '#92846e'; // floor
ctx.fillRect(0, canvas.height/2, canvas.width, canvas.height/2);
// cast rays
const FOV = Math.PI/3;
const halfFOV = FOV/2;
for (let x = 0; x < canvas.width; x++) {
const rayAngle = player.angle - halfFOV + (x / canvas.width) * FOV;
const result = castRay(rayAngle);
const perpDist = result.distance;
// prevent div by zero
if (perpDist < 0.0001) perpDist = 0.0001;
// wall height
const wallHeight = (TILE_SIZE / perpDist) * (canvas.height / 2);
const drawStart = Math.max(0, Math.floor(canvas.height/2 - wallHeight/2));
const drawEnd = Math.min(canvas.height, Math.floor(canvas.height/2 + wallHeight/2));
// texture y coordinate step
const texStep = wallTexSize / wallHeight;
let texPos = (drawStart - canvas.height/2 + wallHeight/2) * texStep;
const texX = result.texX;
// draw wall slice
for (let y = drawStart; y < drawEnd; y++) {
const texY = Math.floor(texPos) & (wallTexSize-1);
const pixel = wallCtx.getImageData(texX, texY, 1, 1).data;
const darkness = 1 / (1 + perpDist * 0.05); // simple lighting
const r = Math.floor(pixel[0] * darkness);
const g = Math.floor(pixel[1] * darkness);
const b = Math.floor(pixel[2] * darkness);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, 1, 1);
texPos += texStep;
}
}
// render sprites (enemies)
// sort enemies by distance (far to near)
const enemiesSorted = [...enemies].filter(e => e.alive).sort((a,b) => {
const distA = Math.hypot(a.x - player.x, a.y - player.y);
const distB = Math.hypot(b.x - player.x, b.y - player.y);
return distB - distA; // farthest first
});
for (const enemy of enemiesSorted) {
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.hypot(dx, dy);
const angleToEnemy = Math.atan2(dy, dx) - player.angle;
// normalize angle to [-pi, pi]
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
const halfFOV = FOV/2;
if (Math.abs(angleToEnemy) > halfFOV) continue; // outside FOV
// check if enemy is behind a wall
// cast ray towards enemy and see if wall is closer
const blockRayAngle = player.angle + angleToEnemy;
const blockResult = castRay(blockRayAngle);
if (blockResult.distance < dist - 0.1) continue; // wall blocks
// screen position
const spriteScreenX = Math.floor((angleToEnemy / FOV + 0.5) * canvas.width);
const spriteHeight = Math.abs((TILE_SIZE / dist) * canvas.height * 0.8); // scale factor
const spriteWidth = spriteHeight; // square sprite
const drawStartY = Math.max(0, Math.floor(canvas.height/2 - spriteHeight/2));
const drawEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + spriteHeight/2));
const drawStartX = Math.max(0, Math.floor(spriteScreenX - spriteWidth/2));
const drawEndX = Math.min(canvas.width, Math.floor(spriteScreenX + spriteWidth/2));
// draw enemy texture scaled to sprite
// we'll draw column by column for simplicity
const texWidth = enemyTexSize;
const texHeight = enemyTexSize;
for (let sx = drawStartX; sx < drawEndX; sx++) {
const texX = Math.floor(((sx - drawStartX) / spriteWidth) * texWidth) % texWidth;
for (let sy = drawStartY; sy < drawEndY; sy++) {
const texY = Math.floor(((sy - drawStartY) / spriteHeight) * texHeight) % texHeight;
const pixel = enemyCtx.getImageData(texX, texY, 1, 1).data;
if (pixel[3] === 0) continue; // skip transparent (none in our texture)
ctx.fillStyle = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;
ctx.fillRect(sx, sy, 1, 1);
}
}
}
// draw exit (simple flag)
const exitDX = exitTile.x - player.x;
const exitDY = exitTile.y - player.y;
const exitDist = Math.hypot(exitDX, exitDY);
const exitAngle = Math.atan2(exitDY, exitDX) - player.angle;
while (exitAngle < -Math.PI) exitAngle += 2*Math.PI;
while (exitAngle > Math.PI) exitAngle -= 2*Math.PI;
if (Math.abs(exitAngle) < halfFOV) {
const exitScreenX = Math.floor((exitAngle / FOV + 0.5) * canvas.width);
const exitHeight = Math.abs((TILE_SIZE / exitDist) * canvas.height * 0.6);
const exitWidth = exitHeight * 0.5;
const exitStartY = Math.max(0, Math.floor(canvas.height/2 - exitHeight/2));
const exitEndY = Math.min(canvas.height, Math.floor(canvas.height/2 + exitHeight/2));
const exitStartX = Math.max(0, Math.floor(exitScreenX - exitWidth/2));
const exitEndX = Math.min(canvas.width, Math.floor(exitScreenX + exitWidth/2));
ctx.fillStyle = '#00ff00';
for (let sx = exitStartX; sx < exitEndX; sx++) {
for (let sy = exitStartY; sy < exitEndY; sy++) {
ctx.fillRect(sx, sy, 1, 1);
}
}
}
// HUD
document.getElementById('health').textContent = player.health;
const aliveCount = enemies.filter(e => e.alive).length;
document.getElementById('enemies').textContent = aliveCount;
// message
if (gameState === 'gameover') {
document.getElementById('message').textContent = 'Game Over! Press Restart';
document.getElementById('message').style.display = 'block';
document.getElementById('restartBtn').style.display = 'block';
} else if (gameState === 'win') {
document.getElementById('message').textContent = 'You Win! Press Restart';
document.getElementById('message').style.display = 'block';
document.getElementById('restartBtn').style.display = 'block';
} else {
document.getElementById('message').style.display = 'none';
document.getElementById('restartBtn').style.display = 'none';
}
}
function update() {
if (gameState !== 'playing') return;
// shot cooldown
if (shotCooldown > 0) shotCooldown--;
// movement
const moveSpeed = player.speed;
const rotSpeed = player.rotSpeed;
let dx = 0, dy = 0, da = 0;
if (keys['w'] || keys['ArrowUp']) {
dx += Math.cos(player.angle) * moveSpeed;
dy += Math.sin(player.angle) * moveSpeed;
}
if (keys['s'] || keys['ArrowDown']) {
dx -= Math.cos(player.angle) * moveSpeed;
dy -= Math.sin(player.angle) * moveSpeed;
}
if (keys['a'] || keys['ArrowLeft']) {
dx += Math.sin(player.angle) * moveSpeed; // strafe left
dy -= Math.cos(player.angle) * moveSpeed;
}
if (keys['d'] || keys['ArrowRight']) {
dx -= Math.sin(player.angle) * moveSpeed; // strafe right
dy += Math.cos(player.angle) * moveSpeed;
}
if (keys['ArrowLeft']) da -= rotSpeed; // turn left
if (keys['ArrowRight']) da += rotSpeed; // turn right
// apply rotation
player.angle += da;
// keep angle in [0, 2π)
if (player.angle < 0) player.angle += 2*Math.PI;
if (player.angle >= 2*Math.PI) player.angle -= 2*Math.PI;
// apply movement with collision
const newX = player.x + dx;
const newY = player.y + dy;
if (!isWall(newX, player.y)) player.x = newX;
if (!isWall(player.x, newY)) player.y = newY;
// shooting
if (keys[' '] && shotCooldown === 0) {
shotCooldown = shotDelay;
// cast ray from player at current angle
const shotResult = castRay(player.angle);
let hitEnemy = null;
let hitDist = shotResult.distance;
// check enemies
for (const enemy of enemies) {
if (!enemy.alive) continue;
const ex = enemy.x - player.x;
const ey = enemy.y - player.y;
const dist = Math.hypot(ex, ey);
const angleToEnemy = Math.atan2(ey, ex) - player.angle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
const halfFOV = FOV/2;
if (Math.abs(angleToEnemy) > halfFOV) continue;
// check if enemy is in front of wall hit
if (dist < hitDist) {
// additional check: ensure enemy is roughly in line (within sprite width)
// we'll accept if distance less than ray distance
hitEnemy = enemy;
hitDist = dist;
}
}
if (hitEnemy) {
hitEnemy.health -= 20;
if (hitEnemy.health <= 0) {
hitEnemy.alive = false;
}
}
}
// enemy AI
for (const enemy of enemies) {
if (!enemy.alive) continue;
const dx = enemy.x - player.x;
const dy = enemy.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > 6) continue; // only attack if close
const angleToEnemy = Math.atan2(dy, dx) - player.angle;
while (angleToEnemy < -Math.PI) angleToEnemy += 2*Math.PI;
while (angleToEnemy > Math.PI) angleToEnemy -= 2*Math.PI;
const halfFOV = FOV/2;
if (Math.abs(angleToEnemy) > halfFOV) continue;
// line of sight check
const losAngle = player.angle + angleToEnemy;
const losResult = castRay(losAngle);
if (losResult.distance < dist - 0.1) continue; // blocked by wall
// attack cooldown per enemy
if (!enemy.lastAttack) enemy.lastAttack = 0;
enemy.lastAttack--;
if (enemy.lastAttack <= 0) {
player.health -= 10;
enemy.lastAttack = 30; // half second at 60fps
if (player.health <= 0) {
gameState = 'gameover';
}
}
}
// check win condition: all enemies dead and player at exit
const allDead = enemies.every(e => !e.alive);
const atExit = Math.hypot(player.x - exitTile.x, player.y - exitTile.y) < 0.5;
if (allDead && atExit) {
gameState = 'win';
}
}
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// restart button
document.getElementById('restartBtn').addEventListener('click', () => {
// reset game state
player.x = 1.5;
player.y = 1.5;
player.angle = 0;
player.health = 100;
enemies.forEach(e => {
e.health = 30;
e.alive = true;
delete e.lastAttack;
});
gameState = 'playing';
});
// start loop
gameLoop();
</script>
```
Now we need to replace this with the new script.
Let's perform the edit.
We'll use edit tool.