From 8f458fbc34f7affe612d2e19cb58a6094e3cf9ff Mon Sep 17 00:00:00 2001 From: Kim Ravn Hansen Date: Mon, 29 Sep 2025 08:46:57 +0200 Subject: [PATCH] asdqwde --- frontend/ascii_dungeon_crawler.js | 1 - frontend/ascii_first_person_renderer.js | 195 +++++++++++++----------- frontend/ascii_textureloader.js | 6 +- frontend/ascii_tile_map.js | 3 +- frontend/gnoll.png | Bin 6054 -> 8355 bytes 5 files changed, 107 insertions(+), 98 deletions(-) mode change 100755 => 100644 frontend/gnoll.png diff --git a/frontend/ascii_dungeon_crawler.js b/frontend/ascii_dungeon_crawler.js index 24e8e5c..d94cdbc 100755 --- a/frontend/ascii_dungeon_crawler.js +++ b/frontend/ascii_dungeon_crawler.js @@ -171,7 +171,6 @@ class DungeonCrawler { textureUrls.forEach((url, textureId) => { Texture.fromSource(url).then((texture) => { textures[textureId] = texture; - console.log("here", { textureId, texture, textures }); textureLoadCount++; if (textureLoadCount < textureUrls.length) { diff --git a/frontend/ascii_first_person_renderer.js b/frontend/ascii_first_person_renderer.js index 96d5943..d2587a5 100755 --- a/frontend/ascii_first_person_renderer.js +++ b/frontend/ascii_first_person_renderer.js @@ -142,23 +142,16 @@ export class FirstPersonRenderer { const screenWidth = this.window.width; /** @type {Map { - coordsCheckedFrame.set(idx, tile); - }); + const ray = this.castRay(posX, posY, rayDirX, rayDirY, coordsChecked); // // Render a single screen column @@ -195,83 +188,97 @@ export class FirstPersonRenderer { * @protected */ renderColumn(x, ray, rayDirX, rayDirY, angleOffset) { + // // + // // Check if we hit anything at all + // if (ray.collisions.length === 0) { + // // + // // We didn't hit anything. Just paint floor, wall, and darkness + // for (let y = 0; y < this.window.height; y++) { + // const [char, color] = this.shades[y]; + // this.window.put(x, y, char, color); + // } + // return; + // } // - // Check if we hit anything at all - if (ray.collisions.length === 0) { - // - // We didn't hit anything. Just paint floor, wall, and darkness - for (let y = 0; y < this.window.height; y++) { - const [char, color] = this.shades[y]; - this.window.put(x, y, char, color); - } - return; + // // ALTERNATIVE always paint floor and ceiling + for (let y = 0; y < this.window.height; y++) { + const [char, color] = this.shades[y]; + this.window.put(x, y, char, color); } - const { rayLength, side, sampleU, tile: wallTile } = ray.collisions[0]; + for (const { rayLength, side, sampleU, tile } of ray.collisions) { + let distance = Math.max(rayLength * Math.cos(angleOffset), 1e-12); // Avoid divide by zero - const distance = Math.max(rayLength * Math.cos(angleOffset), 1e-12); // Avoid divide by zero - - // - // Calculate perspective. - // - const screenHeight = this.window.height; - const lineHeight = Math.round(screenHeight / distance); // using round() because floor() gives aberrations when distance == (n + 0.500) - const halfScreenHeight = screenHeight / 2; - const halfLineHeight = lineHeight / 2; - - let minY = Math.floor(halfScreenHeight - halfLineHeight); - let maxY = Math.floor(halfScreenHeight + halfLineHeight); - let unsafeMinY = minY; // can be lower than zero - it happens when we get so close to a wall we cannot see top or bottom - - if (minY < 0) { - minY = 0; - } - if (maxY >= screenHeight) { - maxY = screenHeight - 1; - } - - // - // Pick texture (here grid value decides which texture) - // - const wallTexture = this.textures[wallTile.textureId]; - - for (let y = 0; y < screenHeight; y++) { // - // Are we hitting the ceiling? + // Calculate perspective. // - if (y < minY || y > maxY) { - const [char, color] = this.shades[y]; - this.window.put(x, y, char, color); - continue; + const screenHeight = this.window.height; + const lineHeight = Math.round(screenHeight / distance); // using round() because floor() gives aberrations when distance == (n + 0.500) + const halfScreenHeight = screenHeight / 2; + const halfLineHeight = lineHeight / 2; + + let minY = Math.floor(halfScreenHeight - halfLineHeight); + let maxY = Math.floor(halfScreenHeight + halfLineHeight); + let unsafeMinY = minY; // can be lower than zero - it happens when we get so close to a wall we cannot see top or bottom + + if (minY < 0) { + minY = 0; } - if (y === minY) { - this.window.put(x, y, "m", "#0F0"); - continue; - } - if (y === maxY) { - this.window.put(x, y, "M", "#F00"); - continue; + if (maxY >= screenHeight) { + maxY = screenHeight - 1; } // - // Map screen y to texture y - let sampleV = (y - unsafeMinY) / lineHeight; // y- coordinate of the texture point to sample - - const color = wallTexture.sample(sampleU, sampleV); - + // Pick texture (here grid value decides which texture) // - // North-south walls are shaded differently from east-west walls - let shade = side === Side.X_AXIS ? 0.8 : 1.0; // MAGIC NUMBERS + const texture = this.textures[tile.textureId]; - // - // Dim walls that are far away - const lightLevel = 1 - rayLength / this.viewDistance; + for (let y = 0; y < screenHeight; y++) { + // + // Are we hitting the ceiling? + // + if (y < minY || y > maxY) { + const [char, color] = this.shades[y]; + this.window.put(x, y, char, color); + continue; + } + // // DEBUG LINES + // if (y === minY) { + // this.window.put(x, y, "m", "#0F0"); + // continue; + // } + // if (y === maxY) { + // this.window.put(x, y, "M", "#F00"); + // continue; + // } - // - // Darken the image - color.mulRGB(shade * lightLevel); + // + // Map screen y to texture y + let sampleV = (y - unsafeMinY) / lineHeight; // y- coordinate of the texture point to sample - this.window.put(x, y, this.wallChar, color.toCSS()); + const color = texture.sample(sampleU, sampleV); + if (!Number.isFinite(color.a)) { + throw new Error("Waaat"); + } + + if (color.a === 0) { + continue; + } + + // + // North-south walls are shaded differently from east-west walls + let shade = side === Side.X_AXIS ? 0.8 : 1.0; // MAGIC NUMBERS + + // + // Dim walls that are far away + const lightLevel = 1 - rayLength / this.viewDistance; + + // + // Darken the image + color.mulRGB(shade * lightLevel); + + this.window.put(x, y, tile.sprite ? "#" : this.wallChar, color.toCSS()); // MAGIC CONSTANT "S" + } } } @@ -382,35 +389,37 @@ export class FirstPersonRenderer { const tile = this.map.get(mapX, mapY); coordsChecked.set(this.map.tileIdx(mapX, mapY), tile); + // + // -------------------------- + // No collision? Move on + // -------------------------- + if (!tile.collision) { + continue; + } + const rayLength = Math.hypot( wallDist * dirX, // wallDist * dirY, // ); // - // -------------------------- - // Add a Sprite to the result - // -------------------------- - if (tile.sprite || tile.wall) { - // - // Prepend the element to the array so rear-most sprites - // appear first in the array, - // enabling us to simply draw from back to front - const collision = new RayCollision(); - collision.mapX = mapX; - collision.mapY = mapY; - collision.rayLength = rayLength; - collision.tile = tile; - collision.sampleU = sampleU; - collision.side = side; - result.collisions.unshift(collision); - } + // Prepend the element to the array so rear-most sprites + // appear first in the array, + // enabling us to simply draw from back to front + const collision = new RayCollision(); + collision.mapX = mapX; + collision.mapY = mapY; + collision.rayLength = rayLength; + collision.tile = tile; + collision.sampleU = sampleU; + collision.side = side; + result.collisions.unshift(collision); // - // -------------------------- - // Add a Wall to the result - // (and return) - // -------------------------- + // -------------------------------- + // Algorithm stops if the ray hits + // a wall. + // ------------------------------- if (tile.wall) { result.hitWall = true; return result; diff --git a/frontend/ascii_textureloader.js b/frontend/ascii_textureloader.js index 69a420e..0f249e0 100755 --- a/frontend/ascii_textureloader.js +++ b/frontend/ascii_textureloader.js @@ -92,14 +92,14 @@ export class Texture { * @returns {NRGBA} */ sample(u, v) { - const x = Math.round(u * this.width); - const y = Math.round(v * this.height); + const x = Math.min(this.width - 1, Math.round(u * this.width)); + const y = Math.min(this.height - 1, Math.round(v * this.height)); const index = (y * this.width + x) * 4; return new NRGBA( this.data[index + 0] / 255, this.data[index + 1] / 255, this.data[index + 2] / 255, - 1, // this.data[index + 3] / 255, + this.data[index + 3] / 255, ); } } diff --git a/frontend/ascii_tile_map.js b/frontend/ascii_tile_map.js index 355a537..0eb6921 100755 --- a/frontend/ascii_tile_map.js +++ b/frontend/ascii_tile_map.js @@ -33,7 +33,7 @@ export class Tile { } } - get isCollision() { + get collision() { return this.wall || this.sprite; } } @@ -70,6 +70,7 @@ export const defaultLegend = Object.freeze({ minimapColor: "#f00", traversable: false, wall: false, + sprite: true, }), // diff --git a/frontend/gnoll.png b/frontend/gnoll.png old mode 100755 new mode 100644 index b6515be4e9dce5ae67d2953af282ec09535c083e..e40fabc439e10019112cf641d30fc47ae48f5519 GIT binary patch literal 8355 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 g21!IgR09A~RPH?_Mz~l20000