diff --git a/ascii_minimap_renderer.js b/ascii_minimap_renderer.js new file mode 100755 index 0000000..cbd8e59 --- /dev/null +++ b/ascii_minimap_renderer.js @@ -0,0 +1,62 @@ +import { TileMap } from "./frontend/ascii_tile_map.js"; +import { Vector2i } from "./frontend/ascii_types.js"; +import { AsciiWindow } from "./frontend/ascii_window.js"; + +export class MiniMapRenderer { + /** + * @param {AsciiWindow} aWindow + * @param {TileMap} map + */ + constructor(aWindow, map) { + if (aWindow.width !== aWindow.height) { + console.log("Window now square", { width: aWindow.width, height: aWindow.height }); + throw new Error("Window must be square"); + } + if (aWindow.width % 2 === 0) { + console.log("Window dimension must be uneven", { width: aWindow.width, height: aWindow.height }); + throw new Error("Window dimension is even, it must be uneven"); + } + + /** @type {AsciiWindow} */ + this.window = aWindow; + + /** @type {TileMap} */ + this.map = map; + + /** @type {number} how far we can see on the minimap */ + this.distance = (aWindow.width - 1) / 2; + + this.fg = undefined; // Let the CSS of the parent element control the colors of the tiles + this.bg = undefined; // let the CSS of the parent element control the background colors of the tiles + } + + /** + * @param {number} centerX + * @param {number} centerY + * @param {Orientation} orientation + */ + draw(centerX, centerY, orientation) { + // these variables are the coordinates of the + // area of the map (not minimap) we are looking at + const minX = centerX - this.distance; + const maxX = centerX + this.distance; + const minY = centerY - this.distance; + const maxY = centerY + this.distance; + + const distanceV = new Vector2i(this.distance, this.distance); + + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + const wndPosV = new Vector2i(x - centerX, y - centerY).rotateCW(orientation + 1).add(distanceV); + + this.window.put(wndPosV.x, wndPosV.y, this.map.get(x, y).minimap, this.fg, this.bg); + } + } + this.window.put(this.distance, this.distance, "@", "#44F"); + this.window.commitToDOM(); + } +} + +if (Math.PI < 0 && AsciiWindow && TileMap && Vector2i) { + ("STFU Linda"); +} diff --git a/bench.js b/bench.js deleted file mode 100755 index 8ef5e2a..0000000 --- a/bench.js +++ /dev/null @@ -1,41 +0,0 @@ -function dataset() { - return new Array(4000).fill().map(() => "fusse".repeat(5)); -} - -const simCount = 100_000; - -const start = Date.now(); - -const target = "not found"; - -for (let sim = 0; sim < simCount; sim++) { - const ds = dataset(); - - // const len = ds.length; - // for (let i = 0; i < len; i++) { - // // pretend to do work on the data elements - // const el = ds[i]; - // if (el === target) { - // console.log("foo"); - // } - // } - - // ds.forEach((el) => { - // if (el === target) { - // console.log("foo"); - // } - // }); - - while (ds.length > 0) { - let el = ds.pop(); - if (el === target) { - console.log("foo"); - } - } -} - -console.log("time: %f msec", Date.now() - start); - -// for-loop : 8568 msec -// .forEach : 8551 msec -// pop() : 8765 msec diff --git a/dda.txt b/dda.txt new file mode 100644 index 0000000..262fda8 --- /dev/null +++ b/dda.txt @@ -0,0 +1,212 @@ +You said: +Im making a dungeon crawler where all the walls are guaranteed to be tile-aligned. It's wolfensteil graphics, so no need for real ray tracing, only vertical ray casting in 2d. Can you help me make a fast javascript ray-caster that does not just increment the rays a tiny bit every time. I think the algorithm may be called DDA. I also need to know where on the wall the ray hits, so I can apply the correct texture +ChatGPT said: + +Yes — you’re talking about the DDA (Digital Differential Analyzer) algorithm, which is the standard way to do fast 2D grid ray casting for Wolfenstein-style engines. + +The idea: instead of stepping pixel by pixel, you compute how far you need to go to reach the next vertical or next horizontal grid line, then keep stepping grid-by-grid until you hit a wall. + +Here’s a compact JavaScript implementation outline: + +// grid: 2D array of 0 = empty, 1 = wall +// posX, posY: player position in map coords (floats, tile units) +// dirX, dirY: ray direction (normalized vector) +// maxDist: optional cutoff distance +function castRay(grid, posX, posY, dirX, dirY, maxDist = 100) { + const mapWidth = grid[0].length; + const mapHeight = grid.length; + + // Current map square + let mapX = Math.floor(posX); + let mapY = Math.floor(posY); + + // Length of ray from one x or y-side to next x or y-side + const deltaDistX = Math.abs(1 / dirX); + const deltaDistY = Math.abs(1 / dirY); + + // Step direction (+1 or -1) and initial sideDist + let stepX, stepY; + let sideDistX, sideDistY; + + if (dirX < 0) { + stepX = -1; + sideDistX = (posX - mapX) * deltaDistX; + } else { + stepX = 1; + sideDistX = (mapX + 1.0 - posX) * deltaDistX; + } + + if (dirY < 0) { + stepY = -1; + sideDistY = (posY - mapY) * deltaDistY; + } else { + stepY = 1; + sideDistY = (mapY + 1.0 - posY) * deltaDistY; + } + + let hit = false; + let side = 0; // 0 = hit x-side, 1 = hit y-side + + // DDA loop + while (!hit) { + if (sideDistX < sideDistY) { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } else { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + + // Check for out of bounds + if (mapX < 0 || mapX >= mapWidth || mapY < 0 || mapY >= mapHeight) { + return null; // no hit + } + + // Check if ray hit a wall + if (grid[mapY][mapX] > 0) hit = true; + if (Math.min(sideDistX, sideDistY) > maxDist) return null; + } + + // Calculate distance to wall + let perpWallDist; + if (side === 0) { + perpWallDist = (mapX - posX + (1 - stepX) / 2) / dirX; + } else { + perpWallDist = (mapY - posY + (1 - stepY) / 2) / dirY; + } + + // Where on the wall did we hit? (texture coordinate) + let wallX; + if (side === 0) { + wallX = posY + perpWallDist * dirY; + } else { + wallX = posX + perpWallDist * dirX; + } + wallX -= Math.floor(wallX); // fractional part only [0..1) + + return { + mapX, mapY, // tile coords + side, // hit vertical or horizontal wall + distance: perpWallDist, + textureX: wallX // use this to index into texture + }; +} + +Key points: + +DDA stepping means you only move to the next grid line each time → very fast. + +side tells you if you hit a vertical or horizontal wall (so you can darken one side for fake lighting). + +textureX gives you the fractional offset into the wall tile (for correct texture mapping). + +👉 Then in your renderer, for each vertical screen stripe: + +Compute the ray direction from the camera plane. + +Call castRay. + +Use distance to determine column height. + +Use textureX to select the right column from your texture. + +Do you want me to also show you how to set up the main loop that casts all rays for the screen (like Wolfenstein’s projection plane math), or are you mainly looking for just the DDA core? + +You said: +yes please +ChatGPT said: + +Perfect — let’s wire the DDA ray-caster into a full “Wolfenstein” style projection loop. + +The basic camera model: + +Player has a position (posX, posY) and a direction vector (dirX, dirY). + +You also need a perpendicular camera plane (planeX, planeY) that defines the field of view. + +Typical setup: dirX=1, dirY=0 and planeX=0, planeY=0.66 → ~66° FOV. + +Then, for each screen column x, you build a ray direction, cast it with DDA, and draw the vertical stripe. + +Here’s a skeleton in JavaScript: + +function renderFrame(ctx, screenWidth, screenHeight, grid, posX, posY, dirX, dirY, planeX, planeY, textures) { + for (let x = 0; x < screenWidth; x++) { + // cameraX: -1 on left side, +1 on right side of screen + const cameraX = 2 * x / screenWidth - 1; + const rayDirX = dirX + planeX * cameraX; + const rayDirY = dirY + planeY * cameraX; + + // Cast ray using our DDA function + const hit = castRay(grid, posX, posY, rayDirX, rayDirY); + if (!hit) continue; // nothing hit (out of bounds) + + const { distance, side, textureX, mapX, mapY } = hit; + + // Projected wall height (avoid div by zero) + const lineHeight = Math.floor(screenHeight / (distance || 0.0001)); + + // Draw start and end of wall stripe + let drawStart = Math.floor(-lineHeight / 2 + screenHeight / 2); + let drawEnd = Math.floor(lineHeight / 2 + screenHeight / 2); + if (drawStart < 0) drawStart = 0; + if (drawEnd >= screenHeight) drawEnd = screenHeight - 1; + + // Pick texture (here grid value decides which texture) + const tex = textures[grid[mapY][mapX] % textures.length]; + const texWidth = tex.width, texHeight = tex.height; + + // X coord on texture + let texX = Math.floor(textureX * texWidth); + if (side === 0 && rayDirX > 0) texX = texWidth - texX - 1; + if (side === 1 && rayDirY < 0) texX = texWidth - texX - 1; + + // Draw vertical stripe from texture + const column = ctx.getImageData(texX, 0, 1, texHeight); // one column + for (let y = drawStart; y < drawEnd; y++) { + // Map screen y to texture y + const d = y * 256 - screenHeight * 128 + lineHeight * 128; + const texY = Math.floor(((d * texHeight) / lineHeight) / 256); + const texIndex = (texY * texWidth + texX) * 4; + + const r = column.data[texIndex]; + const g = column.data[texIndex + 1]; + const b = column.data[texIndex + 2]; + const a = column.data[texIndex + 3]; + + // Simple shading: darken if y-side + const shade = side === 1 ? 0.7 : 1.0; + + ctx.fillStyle = `rgba(${r * shade}, ${g * shade}, ${b * shade}, ${a / 255})`; + ctx.fillRect(x, y, 1, 1); + } + } +} + +What’s happening: + +Camera math +Each screen column x corresponds to a ray direction via (dirX + planeX*cameraX, dirY + planeY*cameraX). + +Ray casting +Calls the castRay function from earlier. + +Projection +Wall height = screenHeight / distance. + +Texturing + +textureX = fractional hit location along the wall. + +Flipped if hitting certain wall sides. + +Then we stretch the vertical texture column to fit the projected wall height. + +Shading +Optional: darken walls hit on y-sides to fake lighting. + +This is enough to render Wolfenstein-style walls! 🚀 + +👉 Do you want me to also show you how to manage floor/ceiling rendering (raycasting them too, or using vertical interpolation), or do you only need walls for now? diff --git a/frontend/ascii_dungeon_crawler.html b/frontend/ascii_dungeon_crawler.html index 11cf1a1..32a7abf 100755 --- a/frontend/ascii_dungeon_crawler.html +++ b/frontend/ascii_dungeon_crawler.html @@ -18,16 +18,25 @@ text-align: center; } - #viewport, - #minimap { + #viewport { font-size: 10px; - line-height: 10px; + line-height: 8px; white-space: pre; border: 2px solid #0f0; display: inline-block; - background-color: #000; - padding: 10px; - overflor: ignore; + padding: 2px; + border: 5px solid #666; + font-weight: bold; + } + #minimap { + font-size: 12px; + line-height: 12px; + white-space: pre; + display: inline-block; + padding: 2px; + border: 5px solid #666; + color: #666; + background-color: black; } #controls { @@ -39,60 +48,6 @@ margin-top: 20px; } - #minimap .player { - position: relative; /* anchor */ - color: #000; /* text blends into background */ - /* background-color: red; */ - } - #minimap .player.north::before { - content: "↑"; - /* content: "★"; */ - position: absolute; - inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */ - display: flex; /* center it */ - align-items: center; - justify-content: center; - pointer-events: none; /* don’t block clicks */ - font-size: 1.5em; /* bigger if you want */ - color: red; - } - #minimap .player.south::before { - content: "↓"; - /* content: "★"; */ - position: absolute; - inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */ - display: flex; /* center it */ - align-items: center; - justify-content: center; - pointer-events: none; /* don’t block clicks */ - font-size: 1.5em; /* bigger if you want */ - color: red; - } - #minimap .player.east::before { - content: "→"; - /* content: "★"; */ - position: absolute; - inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */ - display: flex; /* center it */ - align-items: center; - justify-content: center; - pointer-events: none; /* don’t block clicks */ - font-size: 1.5em; /* bigger if you want */ - color: red; - } - #minimap .player.west::before { - content: "←"; - /* content: "★"; */ - position: absolute; - inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */ - display: flex; /* center it */ - align-items: center; - justify-content: center; - pointer-events: none; /* don’t block clicks */ - font-size: 1.5em; /* bigger if you want */ - color: red; - } - textarea { background-color: #001100; color: #ccc; @@ -124,7 +79,7 @@
orientation
-
Load your map (# = walls, space = floor):
+ ←→↑↓