diff --git a/ascii_minimap_renderer.js b/ascii_minimap_renderer.js
index cbd8e59..21583c7 100755
--- a/ascii_minimap_renderer.js
+++ b/ascii_minimap_renderer.js
@@ -3,60 +3,60 @@ 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");
- }
+ /**
+ * @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 {AsciiWindow} */
+ this.window = aWindow;
- /** @type {TileMap} */
- this.map = map;
+ /** @type {TileMap} */
+ this.map = map;
- /** @type {number} how far we can see on the minimap */
- this.distance = (aWindow.width - 1) / 2;
+ /** @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
- }
+ 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;
+ /**
+ * @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);
+ 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);
+ 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();
- }
+ this.window.put(wndPosV.x, wndPosV.y, this.map.get(x, y).minimapChar, this.fg, this.bg);
+ }
+ }
+ this.window.put(this.distance, this.distance, "@", "#44F");
+ this.window.commitToDOM();
+ }
}
if (Math.PI < 0 && AsciiWindow && TileMap && Vector2i) {
- ("STFU Linda");
+ ("STFU Linda");
}
diff --git a/frontend/ascii_dungeon_crawler.html b/frontend/ascii_dungeon_crawler.html
index 32a7abf..aebe732 100755
--- a/frontend/ascii_dungeon_crawler.html
+++ b/frontend/ascii_dungeon_crawler.html
@@ -87,9 +87,9 @@
############################################################
## ################# ########################
## # # ################# # ## ########################
-## # ################# # ## ################
-## # # S ################# # ## #### ####
-## # # ## # #### # # ####
+## #S ################# # ## ################
+## # # ################# # ## #### ####
+## M # # ## # #### # # ####
###### #################### ## #### # ####
###### #################### # ## # # #### ####
###### #################### # ####
diff --git a/frontend/ascii_dungeon_crawler.js b/frontend/ascii_dungeon_crawler.js
index aa95658..73f2978 100755
--- a/frontend/ascii_dungeon_crawler.js
+++ b/frontend/ascii_dungeon_crawler.js
@@ -1,17 +1,23 @@
import { Vector2i, Orientation, RelativeMovement, PI_OVER_TWO } from "./ascii_types.js";
-import { FirstPersonRenderer } from "./ascii_first_person_renderer.js";
+import { DefaultRendererOptions, FirstPersonRenderer } from "./ascii_first_person_renderer.js";
import { MiniMapRenderer } from "../ascii_minimap_renderer.js";
import { Texture } from "./ascii_textureloader.js";
import { AsciiWindow } from "./ascii_window.js";
import { TileMap } from "./ascii_tile_map.js";
import eobWallUrl1 from "./eob1.png";
-import eobWallUrl2 from "./eob2.png";
+import gnollSpriteUrl from "./gnoll.png";
import { sprintf } from "sprintf-js";
class Player {
+ /** @protected */
_posV = new Vector2i();
+
+ /** @protected */
_directionV = new Vector2i(0, 1);
+ /** @type {number} number of milliseconds to sleep before next gameLoop. */
+ delay = 0;
+
get x() {
return this._posV.x;
}
@@ -75,11 +81,6 @@ class DungeonCrawler {
}
constructor() {
- /** @type {number} Number of times per second we poll for controller inputs */
- this.pollsPerSec = 60;
- /** @type {number} */
- this.debounce = 0;
-
/** @constant @readonly */
this.keys = {
/** @constant @readonly */
@@ -106,84 +107,87 @@ class DungeonCrawler {
/** @readonly */
this.rendering = {
- enabled: true,
- ticker: 0,
- maxDepth: 5,
- fov: Math.PI / 3, // 60 degrees, increase maybe?
- view: new AsciiWindow(document.getElementById("viewport"), 120, 50),
+ /** @type {FirstPersonRenderer} */ firstPersonRenderer: null,
+ /** @type {MiniMapRenderer} */ miniMapRenderer: null,
- /** @type {FirstPersonRenderer} */
- renderer: null,
+ firstPersonWindow: new AsciiWindow(document.getElementById("viewport"), 100, 45), // MAGIC CONSTANTS
+ minimapWindow: new AsciiWindow(document.getElementById("minimap"), 9, 9), // MAGIC CONSTANT
+
+ options: DefaultRendererOptions,
};
/** @readonly @type {MiniMapRenderer} */
- this.minimap;
-
- /**
- * @typedef Player
- * @type {object}
- * @property {number} x integer. Player's x-coordinate on the grid.
- * @property {number} y integer. Player's y-coordinate on the grid.
- */
this.player = new Player();
this.setupControls();
-
this.loadMap();
- this.updateCompass();
- this.rendering.view.commitToDOM();
this.render(this.player.x, this.player.y, this.player.orientation * PI_OVER_TWO);
+ this.renderCompass();
+
+ //
+ // Start the game loop
+ //
this.gameLoop();
}
- render(posX = this.player.x, posY = this.player.y, angle = this.player.angle) {
- if (!this.rendering.renderer) {
+ /**
+ * Render a first person view of the camera in a given position and orientation.
+ *
+ * @param {number} camX the x-coordinate of the camera (in map coordinates)
+ * @param {number} camY the y-coordinate of the camera (in map coordinates)
+ * @param {number} angle the orientation of the camera in radians around the unit circle.
+ */
+ render(camX = this.player.x, camY = this.player.y, angle = this.player.angle) {
+ if (!this.rendering.firstPersonRenderer) {
console.log("Renderer not ready yet");
return;
}
- this.rendering.renderer.renderFrame(
- posX + 0.5, // add .5 to get camera into center of cell
- posY + 0.5, // add .5 to get camera into center of cell
+ this.rendering.firstPersonRenderer.renderFrame(
+ camX + 0.5, // add .5 to get camera into center of cell
+ camY + 0.5, // add .5 to get camera into center of cell
angle,
);
}
+ renderMinimap() {
+ this.rendering.miniMapRenderer.draw(this.player.x, this.player.y, this.player.orientation);
+ }
+
loadMap() {
const mapString = document.getElementById("mapText").value;
this.map = TileMap.fromText(mapString);
this.player._posV = this.map.findFirst({ startLocation: true });
+
if (!this.player._posV) {
throw new Error("Could not find a start location for the player");
}
- console.log(this.map.getAreaAround(this.player.x, this.player.y, 5).toString());
- const minimapElement = document.getElementById("minimap");
- const minimapWindow = new AsciiWindow(minimapElement, 9, 9); // MAGIC NUMBERS: width and height of the minimap
- this.minimap = new MiniMapRenderer(minimapWindow, this.map);
+ this.rendering.miniMapRenderer = new MiniMapRenderer(this.rendering.minimapWindow, this.map);
- const textureUrls = [eobWallUrl1, eobWallUrl2];
- const textureCount = textureUrls.length;
- const textures = [];
+ const textureUrls = [eobWallUrl1, gnollSpriteUrl];
+ const textures = new Array(textureUrls.length).fill();
+ let textureLoadCount = 0;
- textureUrls.forEach((url) => {
+ textureUrls.forEach((url, textureId) => {
Texture.fromSource(url).then((texture) => {
- textures.push(texture);
+ textures[textureId] = texture;
- if (textures.length < textureCount) {
+ if (textureLoadCount > textureUrls.length) {
return;
}
- this.rendering.renderer = new FirstPersonRenderer(
- this.rendering.view,
+ textureLoadCount++;
+
+ this.rendering.firstPersonRenderer = new FirstPersonRenderer(
+ this.rendering.firstPersonWindow,
this.map,
- this.rendering.fov,
- this.rendering.maxDepth,
textures,
+ this.rendering.options,
);
this.render();
- this.minimap.draw(this.player.x, this.player.y, this.player.orientation);
+ this.renderMinimap();
console.debug("renderer ready", texture);
});
@@ -213,7 +217,6 @@ class DungeonCrawler {
//
this.player._directionV.rotateCCW(quarterTurns);
- this.updateCompass();
}
/** @type {RelativeMovement} Direction the player is going to move */
@@ -231,14 +234,14 @@ class DungeonCrawler {
//
// We cant move into walls
if (this.map.isWall(targetV.x, targetV.y)) {
- this.debounce = (this.pollsPerSec / 5) | 0;
console.info(
"bumped into wall at %s (mypos: %s), direction=%d",
targetV,
this.player._posV,
this.player.angle,
);
- return;
+ this.delay += 250; // MAGIC NUMBER: Pause for a tenth of a second after hitting a wall
+ return false;
}
this.animation = {
@@ -254,7 +257,7 @@ class DungeonCrawler {
};
this.player._posV = targetV;
- this.updateCompass(); // technically not necessary, but Im anticipating the need + compensating for my bad memory.
+ return true;
}
setupControls() {
@@ -291,50 +294,39 @@ class DungeonCrawler {
},
true,
);
-
- const ticks = Math.round(1000 / this.pollsPerSec);
- this.keys.interval = setInterval(() => {
- this.handleKeyboardInput();
- }, ticks);
}
handleKeyboardInput() {
- if (this.debounce > 0) {
- this.debounce--;
- return;
- }
-
- if (this.isAnimating) {
- return;
- }
-
//
// Check each key we can handle.
for (let key of this.keys.names) {
if (this.keys.pressed[key]) {
- this.debounce = Math.floor(this.animation.fps * this.animation.animationDuration) - 1;
const keyHandler = this.keys.handlers[key];
- keyHandler();
- return;
+ return keyHandler();
}
}
+ return false;
}
+ /**
+ * @returns {boolean} true if an animation is in progress
+ */
handleAnimation() {
//
// Guard: only animate if called for
if (!this.isAnimating) {
this.animation = {};
- return;
+ return false;
}
//
- // Guard, stop animation if it took too long
+ // Guard: stop animation if it took too long
if (this.animation.targetTime <= performance.now()) {
this.render(this.player.x, this.player.y, this.player.angle);
+ this.renderMinimap();
+ this.renderCompass();
this.animation = {};
- this.minimap.draw(this.player.x, this.player.y, this.player.orientation);
- return;
+ return false;
}
const a = this.animation;
@@ -344,44 +336,68 @@ class DungeonCrawler {
const animX = a.targetX - a.startX; // how much this animation causes us to move in the x-direction
const animA = a.targetAngle - a.startAngle; // how much this animation causes us to rotate in total
const animT = a.targetTime - a.startTime; // how long (in ms) this animation is supposed to take.
-
- const deltaT = (nowT - a.startTime) / animT;
- if (deltaT > 1) {
- throw new Error("Not supposed to happen!");
- }
+ const progress = Math.min((nowT - a.startTime) / animT, 1);
// render
this.render(
- a.startX + animX * deltaT, //
- a.startY + animY * deltaT, //
- a.startAngle + animA * deltaT, //
+ a.startX + animX * progress, //
+ a.startY + animY * progress, //
+ a.startAngle + animA * progress, //
);
+
+ return true;
}
gameLoop() {
//
- // We're not animating, so we chill out for 50 msec
- if (!this.isAnimating) {
- setTimeout(() => this.gameLoop(), 50); // MAGIC NUMBER
+ // Has something in the game logic told us to chill out?
+ //
+ if (this.delay) {
+ setTimeout(() => this.gameLoop(), this.delay);
+ this.delay = 0;
return;
}
- this.handleAnimation();
+ //
+ // Are we animating ?
+ // Then render a single frame, and then chill out for 20ms.
+ // Do not process keyboard input while animating
+ //
+ if (this.handleAnimation()) {
+ setTimeout(() => this.gameLoop(), 20);
+ return;
+ }
- requestAnimationFrame(() => this.gameLoop());
+ //
+ // Has a key been pressed that we need to react to?
+ // Then queue up a new gameLoop call to be executed
+ // as soon as possible.
+ //
+ // NOTE: this happens inside a microtask to ensure
+ // that the call stack does not get too big and that
+ // each single call to gameLoop does not take too
+ // long
+ //
+ if (this.handleKeyboardInput()) {
+ queueMicrotask(() => this.gameLoop());
+ return;
+ }
+
+ //
+ // Are we idling?
+ // Then only check for new events every 20ms to use less power
+ //
+ setTimeout(() => this.gameLoop(), 50); // MAGIC NUMBER
}
- updateCompass() {
+ renderCompass() {
//
//
// Update the compass
- document.getElementById("compass").textContent = sprintf(
- "%s %s (%d --> %.2f [%dº])",
+ document.getElementById("compass").innerHTML = sprintf(
+ "
%s
%s
",
this.player._posV,
Object.keys(Orientation)[this.player.orientation].toLowerCase(),
- this.player.orientation,
- this.player.orientation * PI_OVER_TWO,
- this.player.orientation * 90,
);
}
}
diff --git a/frontend/ascii_first_person_renderer.js b/frontend/ascii_first_person_renderer.js
index 87140f6..1a403c2 100755
--- a/frontend/ascii_first_person_renderer.js
+++ b/frontend/ascii_first_person_renderer.js
@@ -1,20 +1,65 @@
-import { TileMap } from "./ascii_tile_map.js";
+import { NRGBA } from "./ascii_textureloader.js";
+import { TileMap, Tile } from "./ascii_tile_map.js";
import { AsciiWindow } from "./ascii_window.js";
+/**
+ * Which side of a tile did the ray strike
+ */
export const Side = {
X_AXIS: 0,
Y_AXIS: 1,
};
+class RayCollision {
+ mapX = 0;
+ mapY = 0;
+ rayLength = 0;
+ side = Side.X_AXIS;
+ /** @type {Tile} */
+ tile;
+}
+
+class RayCastResult {
+ hitWall = false;
+ hitSprite = false;
+ wallCollision = new RayCollision();
+
+ /** @type {RayCollision[]} */
+ collisions = [];
+}
+
+/**
+ * @typedef {object} FirstPersonRendererOptions
+ * @property {string} wallChar
+ * @property {NRGBA} floorColor
+ * @property {string} floorChar
+ * @property {NRGBA} ceilingColor
+ * @property {string} ceilingChar
+ * @property {number} viewDistance
+ * @property {number} fov
+ */
+
+/**
+ * @type {FirstPersonRendererOptions}
+ */
+export const DefaultRendererOptions = {
+ wallChar: "W",
+ floorColor: new NRGBA(0.365, 0.165, 0.065),
+ floorChar: "f",
+ ceilingColor: new NRGBA(0.3, 0.3, 0.3),
+ ceilingChar: "c",
+ fadeOutColor: new NRGBA(0.3, 0.3, 0.3),
+ viewDistance: 5,
+ fov: Math.PI / 3, // 60 degrees - good for spooky
+};
export class FirstPersonRenderer {
/**
* @param {AsciiWindow} aWindow the window we render onto.
* @param {TileMap} map
- * @param {number} fov field of view (in radians)
- * @param {number} maxDist maximum view distance.
- * @param {TexturePack} textures
+ * @param {Texture[]} textures
+ * @param {FirstPersonRendererOptions} options
*/
- constructor(aWindow, map, fov, maxDist, textures) {
+ constructor(aWindow, map, textures, options) {
/** @constant @readonly @type {TileMap} */
this.map = map;
@@ -22,91 +67,161 @@ export class FirstPersonRenderer {
this.window = aWindow;
/** @constant @readonly @type {number} */
- this.fov = fov;
+ this.fov = options.fov ?? DefaultRendererOptions.fov;
/** @constant @readonly @type {number} */
- this.maxDist = maxDist;
+ this.viewDistance = options.viewDistance ?? DefaultRendererOptions.viewDistance;
/** @constant @readonly @type {Texture[]} */
this.textures = textures;
+
+ /** @constant @readonly @type {string} */
+ this.wallChar = options.wallChar ?? DefaultRendererOptions.wallChar;
+ /** @constant @readonly @type {NRGBA} */
+ this.floorColor = options.floorColor ?? DefaultRendererOptions.floorColor;
+ /** @constant @readonly @type {string} */
+ this.floorChar = options.floorChar ?? DefaultRendererOptions.floorChar;
+ /** @constant @readonly @type {NRGBA} */
+ this.ceilingColor = options.ceilingColor ?? DefaultRendererOptions.ceilingColor;
+ /** @constant @readonly @type {string} */
+ this.ceilingChar = options.ceilingChar ?? DefaultRendererOptions.ceilingChar;
+
+ /**
+ * Pre-computed colors to use when drawing floors, ceilings and "fadeout"
+ *
+ * There is one entry for every screen row.
+ * Each entry contains a color to use when drawing floors, ceilings, and "fadeout".
+ *
+ * @constant @readonly @type {Array>}
+ */
+ this.shades = [];
+
+ /**
+ * Pre-compute the shades variable
+ */
+ this.computeShades();
+ }
+
+ computeShades() {
+ const screenHeight = this.window.height;
+ const halfScreenHeight = screenHeight / 2;
+ const lineHeight = Math.floor(screenHeight / this.viewDistance);
+ const minY = Math.floor(-lineHeight / 2 + halfScreenHeight); // if y lower than minY, then we're painting ceiling
+ const maxY = Math.floor(lineHeight / 2 + halfScreenHeight); // if y higher than maxY then we're painting floor
+
+ for (let y = 0; y < screenHeight; y++) {
+ if (y < minY) {
+ //
+ // y is smaller than minY. This means we're painting above
+ // the walls, i.e. painting the ceiling.
+ // The closer y is to minY, the farther away this part of the
+ // ceiling is.
+ //
+ // High diff => near
+ // Low diff => far
+ //
+ const diff = minY - y;
+ this.shades.push([this.ceilingChar, this.ceilingColor.mulledRGB(diff / minY).toCSS()]);
+ } else if (y >= maxY) {
+ //
+ // Floor
+ //
+ const diff = y - maxY;
+ this.shades.push([this.floorChar, this.floorColor.mulledRGB(diff / minY).toCSS()]);
+ } else {
+ //
+ // The darkness at the end of the tunnel
+ //
+ this.shades.push([" ", "#000"]);
+ }
+ }
}
renderFrame(posX, posY, dirAngle, commit = true) {
+ const benchmarkStart = performance.now();
const screenWidth = this.window.width;
+ /** @type {Map {
+ coordsCheckedFrame.set(idx, tile);
+ });
//
- // Did we hit something?
- //
- if (!hit) {
- // we did not hit anything. Either the ray went out of bounds,
- // or it went too far, so move on to next pseudo-pixel
- this.renderNoHitCol(x);
- continue;
- }
+ // Render a single screen column
+ this.renderColumn(x, ray, rayDirX, rayDirY, angleOffset);
+ }
- //
- // Our ray hit a wall, render it.
- this.renderHitCol(x, hit, rayDirX, rayDirY, angleOffset);
+ const renderTime = performance.now() - benchmarkStart;
+
+ // Did it take more than 5ms to render the scene?
+ if (renderTime > 5) {
+ console.log("Rendering took a long time", { renderTime });
}
if (commit) {
- this.window.commitToDOM();
- }
- }
-
- /**
- * Render a vertical column of pixels on the screen at the x coordinate.
- * This occurs when the ray did not hit anything.
- *
- * @protected
- */
- renderNoHitCol(x) {
- const screenHeight = this.window.height;
- const halfScreenHieght = screenHeight / 2;
- const lineHeight = Math.floor(screenHeight / this.maxDist);
- let minY = Math.floor(-lineHeight / 2 + halfScreenHieght);
- let maxY = Math.floor(lineHeight / 2 + halfScreenHieght);
-
- for (let y = 0; y < screenHeight; y++) {
- if (y < minY) {
- this.window.put(x, y, "c", "#333"); // ceiling
- } else if (y > maxY) {
- this.window.put(x, y, "f", "#b52"); // floor
- } else {
- const char = ["·", "÷", "'", "~"][(y + x) % 4];
- this.window.put(x, y, char, "#222"); // the far distance
- }
+ requestAnimationFrame(() => {
+ const benchmarkStart = performance.now();
+ this.window.commitToDOM();
+ const commitTime = performance.now() - benchmarkStart;
+ if (commitTime > 5) {
+ console.log("Updating DOM took a long time:", { commitTime });
+ }
+ });
}
}
/**
* Render a column on the screen where the ray hit a wall.
+ * @param {number} x
+ * @param {RayCastResult} ray
+ * @param {number} rayDirX
+ * @param {number} rayDirY
+ * @param {number} angleOffset for far (in radians) is this column from the middle of the screen
+ *
* @protected
*/
- renderHitCol(x, hit, rayDirX, rayDirY, angleOffset) {
- const { rayLength, side, textureOffsetX, mapX, mapY } = hit;
+ 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;
+ }
- const tile = this.map.get(mapX, mapY);
- const safeDistance = Math.max(rayLength * Math.cos(angleOffset), 1e-9); // Avoid divide by zero
+ const { rayLength, side, sampleU, tile: wallTile } = ray.collisions[0];
+
+ const distance = Math.max(rayLength * Math.cos(angleOffset), 1e-12); // Avoid divide by zero
//
// Calculate perspective.
//
const screenHeight = this.window.height;
- const halfScreenHieght = screenHeight / 2;
- const lineHeight = Math.floor(screenHeight / safeDistance);
- let minY = Math.floor(-lineHeight / 2 + halfScreenHieght);
- let maxY = Math.floor(lineHeight / 2 + halfScreenHieght);
- let unsafeMinY = minY; // can be lower than zero
+ 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;
@@ -115,30 +230,26 @@ export class FirstPersonRenderer {
maxY = screenHeight - 1;
}
+ //
// Pick texture (here grid value decides which texture)
- const texture = this.textures[tile.textureId % this.textures.length];
-
- // X coord on texture
- let sampleU = textureOffsetX;
-
- if (side === 0 && rayDirX > 0) {
- sampleU = 1 - sampleU;
- }
- if (side === 1 && rayDirY < 0) {
- sampleU = 1 - sampleU;
- }
+ //
+ const wallTexture = this.textures[wallTile.textureId];
for (let y = 0; y < screenHeight; y++) {
//
// Are we hitting the ceiling?
//
- if (y < minY) {
- this.window.put(x, y, "c", "#333");
+ if (y < minY || y > maxY) {
+ const [char, color] = this.shades[y];
+ this.window.put(x, y, char, color);
continue;
}
-
- if (y > maxY) {
- this.window.put(x, y, "f", "#b52");
+ if (y === minY) {
+ this.window.put(x, y, "m", "#0F0");
+ continue;
+ }
+ if (y === maxY) {
+ this.window.put(x, y, "M", "#F00");
continue;
}
@@ -146,44 +257,39 @@ export class FirstPersonRenderer {
// Map screen y to texture y
let sampleV = (y - unsafeMinY) / lineHeight; // y- coordinate of the texture point to sample
- const color = texture.sample(sampleU, sampleV);
+ const color = wallTexture.sample(sampleU, sampleV);
//
// North-south walls are shaded differently from east-west walls
- let shade = side === Side.X_AXIS ? 0.7 : 1.0; // MAGIC NUMBERS
+ let shade = side === Side.X_AXIS ? 0.8 : 1.0; // MAGIC NUMBERS
//
// Dim walls that are far away
- shade = shade / (1 + rayLength * 0.1);
+ const lightLevel = 1 - rayLength / this.viewDistance;
//
// Darken the image
- color.mulRGB(shade);
+ color.mulRGB(shade * lightLevel);
- // const distancePalette = ["█", "▓", "▒", "░", " "];
- const distancePalette = ["#", "#", "#", "%", "+", "÷", " ", " "];
- const char = distancePalette[rayLength | 0];
-
- this.window.put(x, y, char, color.toCSS());
+ this.window.put(x, y, this.wallChar, color.toCSS());
}
}
/**
* @param {number} camX x-coordinate of the camera (is the same
* @param {number} camY y-coordinate of the camera
- * @parma {number} dirX x-coordinate of the normalized vector of the viewing direction of the camera.
- * @parma {number} dirX y-coordinate of the normalized vector of the viewing direction of the camera.
+ * @param {number} dirX x-coordinate of the normalized vector of the viewing direction of the camera.
+ * @param {number} dirX y-coordinate of the normalized vector of the viewing direction of the camera.
+ * @param {Set} coodsChecked
+ *
+ * @returns {RayCastResult}
+ *
*/
- castRay(camX, camY, dirX, dirY) {
+ castRay(camX, camY, dirX, dirY, coordsChecked) {
// Current map square
let mapX = Math.floor(camX);
let mapY = Math.floor(camY);
- if (dirX === 0 || dirY === 0) {
- console.log("Divide by zero is incoming", { dirX, dirY });
- }
-
- Number.MAX_SAFE_INTEGER;
// Length of ray from one x or y-side to next x or y-side
const deltaDistX = dirX === 0 ? 1e15 : Math.abs(1 / dirX);
const deltaDistY = dirY === 0 ? 1e15 : Math.abs(1 / dirY);
@@ -193,6 +299,7 @@ export class FirstPersonRenderer {
let stepY; // When DDA takes a vertical step (on the map), how far should it move?
let sideDistX; // How far has the ray moved horizontally (on the map) ?
let sideDistY; // How far has the ray moved vertically (on the map) ?
+ let side = Side.X_AXIS;
//
// Calculate how to move along the x-axis
@@ -214,30 +321,32 @@ export class FirstPersonRenderer {
sideDistY = (mapY + 1.0 - camY) * deltaDistY; // we've moved from the camera to the top edge of the tile
}
- //
- // Did the ray hit a wall ?
- //
- let hit = false;
-
- //
- // Did the ray hit a wall on a horizontal edge or a vertical edge?
- //
- let side = Side.X_AXIS;
+ /**
+ * Any sprites the ray has hit on its way.
+ * They are ordered in reverse order of closeness to the camera,
+ * so that if they are drawn in their array ordering, they will
+ * appear in the correct order on the screen.
+ *
+ * @type {RayCastResult}
+ */
+ const result = new RayCastResult();
// DDA loop
- while (!hit) {
+ while (!result.hitWall) {
//
- // Check if ray is longer than maxDist
- if (Math.min(sideDistX, sideDistY) > this.maxDist) {
- return false; // ray got too long, no hit, exit early
+ // Check if ray is longer than viewDistance
+ if (Math.min(sideDistX, sideDistY) > this.viewDistance) {
+ return result;
}
//
// Check for out of bounds
if (mapX < 0 || mapX >= this.map.width || mapY < 0 || mapY >= this.map.height) {
- return false; // ray got outside the map, no hit, exit early
+ return result;
}
+ let wallDist, sampleU;
+
//
// Should we step in the x- or y-direction
// DDA dictates we always move along the shortest vector
@@ -248,6 +357,13 @@ export class FirstPersonRenderer {
sideDistX += deltaDistX;
mapX += stepX;
side = Side.X_AXIS;
+ // Ray hit the east or west edge of the wall-tile
+ wallDist = (mapX - camX + (1 - stepX) / 2) / dirX;
+ sampleU = (camY + wallDist * dirY) % 1;
+
+ if (dirX > 0) {
+ sampleU = 1 - sampleU;
+ }
} else {
//
// Move vertically
@@ -255,72 +371,73 @@ export class FirstPersonRenderer {
sideDistY += deltaDistY;
mapY += stepY;
side = Side.Y_AXIS;
+ // Ray hit the north or south edge of the wall-tile
+ wallDist = (mapY - camY + (1 - stepY) / 2) / dirY;
+ sampleU = (camX + wallDist * dirX) % 1;
+ if (dirY < 0) {
+ sampleU = 1 - sampleU;
+ }
+ }
+
+ const tile = this.map.get(mapX, mapY);
+ coordsChecked.set(this.map.tileIdx(mapX, mapY), tile);
+
+ 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();
+ result.collisions.unshift(collision);
+
+ collision.mapX = mapX;
+ collision.mapY = mapY;
+ collision.rayLength = rayLength;
+ collision.tile = tile;
+ collision.sampleU = sampleU;
+ collision.side = side;
+ if (result.sprite) {
+ collision.sprite = true;
+ }
+ if (result.wall) {
+ collision.wall = true;
+ return;
+ }
}
//
- // Check if ray hit a wall
- if (this.map.isWall(mapX, mapY)) {
- //
- // Ray hit a wall, proceed to the rest of the algorithm.
- //
- hit = true;
+ // --------------------------
+ // Add a Wall to the result
+ // (and return)
+ // --------------------------
+ if (tile.wall) {
+ result.hitWall = true;
+
+ //
+ // DELETE BELOW
+ result.wallCollision.tile = tile;
+ result.wallCollision.side = side;
+
+ result.wallCollision.mapX = mapX;
+ result.wallCollision.mapY = mapY;
+ result.wallCollision.rayLength = rayLength;
+ result.wallCollision.sampleU = sampleU;
+ //
+ return result;
}
}
-
- //
- // The distance to the wall, measured perpendicularly to the viewing angle
- // The perpendicular distance is used to avoid the fish-eye distortion
- // that would occur if we measured the Euclidean distance from the camera
- // to the where the ray impacted the wall. This makes sense when you realize
- // that, when looking directly at a wall, the shortest rays would be right in
- // front of the camera, making it seem as if the wall bowed outwards toward
- // the camera.
- //
- let perpWallDist;
-
- //
- // Where did we hit the wall. Measured as a normalized x-coordinate only;
- //
- let textureOffsetX;
-
- //
- // Determine both the perpendicular distance to the wall
- // and the x-coordinate (on the wall) where the ray hit it.
- //
- if (side === Side.X_AXIS) {
- //
- // Ray hit the left or right edge of the wall-tile
- //
- perpWallDist = (mapX - camX + (1 - stepX) / 2) / dirX;
- textureOffsetX = camY + perpWallDist * dirY;
- } else {
- //
- // Ray hit the upper or lower edge of the wall-tile
- //
- perpWallDist = (mapY - camY + (1 - stepY) / 2) / dirY;
- textureOffsetX = camX + perpWallDist * dirX;
- }
-
- //
- // Normalize textureOffsetX. We only want the fractional part.
- //
- textureOffsetX -= Math.floor(textureOffsetX);
-
- const rayLength = Math.hypot(
- perpWallDist * dirX, //
- perpWallDist * dirY, //
- );
-
- return {
- mapX,
- mapY,
- side,
- rayLength,
- textureOffsetX,
- };
}
}
-if (Math.PI < 0 && AsciiWindow && TileMap) {
+if (Math.PI < 0 && AsciiWindow && TileMap && Tile) {
("STFU Linda");
}
diff --git a/frontend/ascii_first_person_renderer_2.js b/frontend/ascii_first_person_renderer_2.js
deleted file mode 100644
index cf3a2a1..0000000
--- a/frontend/ascii_first_person_renderer_2.js
+++ /dev/null
@@ -1,289 +0,0 @@
-import { NRGBA, Texture } from "./ascii_textureloader";
-import { TileMap } from "./ascii_tile_map";
-import { AsciiWindow } from "./ascii_window";
-
-export class FirstPersonRenderer2 {
- constructor(aWindow, map, wallTex, floorTex, ceilTex) {
- if (!(aWindow instanceof AsciiWindow)) {
- throw new Error("Invalid type for aWindow");
- }
- if (!(map instanceof TileMap)) {
- throw new Error("Invalid type for map");
- }
- if (!(wallTex instanceof Texture && floorTex instanceof Texture && ceilTex instanceof Texture)) {
- throw new Error("Invalid type for texture");
- }
-
- /** @type {AsciiWindow} */
- this.window = aWindow;
-
- /** @type {TileMap} */
- this.map = map;
-
- /** @type {Texture} */
- this.wallTextures = wallTex;
- /** @type {Texture} */
- this.floorTexture = floorTex;
- /** @type {Texture} */
- this.ceilTexture = ceilTex;
-
- /** @type {number} */
- this.fov = Math.PI / 3; // 60 degrees
- /** @type {number} */
- this.viewDist = 5.0;
- /** @type {NRGBA} */
- this.fadeOutColor = new NRGBA(0.03, 0.03, 0.03);
- }
-
- renderFrame(map, px, py, pAngle, floorCtx, ceilCtx, wallCtx) {
- const setPixel = (x, y, color, char = "#") => {
- this.window.put(x, y, char, color);
- };
-
- const mapW = this.map.width;
- const mapH = this.map.height;
- const screenW = this.window.width;
- const screenH = this.window.height;
- const halfH = screenH / 2;
- const nearZero = 1e-9;
-
- const fov = this.fov;
- const viewDist = this.viewDist;
- const fadeOutColor = this.fadeOutColor;
-
- //
- // Texture image data and dimensions
- //
- const floorTex = floorCtx.canvas;
- const ceilTex = ceilCtx.canvas;
- const wallTex = wallCtx.canvas;
- const floorImg = floorCtx.getImageData(0, 0, floorTex.width, floorTex.height).data;
- const ceilImg = ceilCtx.getImageData(0, 0, ceilTex.width, ceilTex.height).data;
- const wallImg = wallCtx.getImageData(0, 0, wallTex.width, wallTex.height).data;
-
- //
- // For each screen column, cast a ray
- //
- for (let x = 0; x < screenW; x++) {
- //
- // compute ray angle by linear interpolation across FOV (angle-based)
- //
- // The Chad Method
- // const cameraX = (2 * x) / screenW - 1; // -1 .. 1
- // const rayAngle = pAngle + Math.atan(cameraX * Math.tan(fov / 2)); // approximate steer by angle
- //
- //
- // The Claude method - pretty sure it ONLY works when fov is 60º
- const rayAngle = pAngle - fov / 2 + (x / screenW) * fov;
-
- //
- // Direction vector for rayAngle
- //
- const dirX = Math.cos(rayAngle);
- const dirY = Math.sin(rayAngle);
-
- //
- // DDA init
- //
- let mapX = Math.floor(px);
- let mapY = Math.floor(py);
- let stepX;
- let stepY;
- let sideDistX;
- let sideDistY;
- const deltaDistX = Math.abs(1 / (dirX === 0 ? nearZero : dirX));
- const deltaDistY = Math.abs(1 / (dirY === 0 ? nearZero : dirY));
-
- //
- // Calculate how far to step for each cell of progress with the DDA algorithm
- // This depends on which quadrant of the coordinate system the ray is traversing
- //
- if (dirX < 0) {
- stepX = -1;
- sideDistX = (px - mapX) * deltaDistX;
- } else {
- stepX = 1;
- sideDistX = (mapX + 1.0 - px) * deltaDistX;
- }
- if (dirY < 0) {
- stepY = -1;
- sideDistY = (py - mapY) * deltaDistY;
- } else {
- stepY = 1;
- sideDistY = (mapY + 1.0 - py) * deltaDistY;
- }
-
- // DDA loop
- let hit = false;
- let side = 0;
- let rayLen = 0; // The length of the ray in steps (t-units), not map coordinate units.
- let steps = 0;
- const maxSteps = Math.ceil(viewDist * Math.max(deltaDistX, deltaDistY)) + Math.max(mapW, mapH); // safe cap
-
- while (steps++ < maxSteps) {
- //
- // Do the DDA thing
- // Lengthen the ray in one step that takes it
- // to the next tile border in either the x- or y-
- // direction, depending on which distance
- // is shorter.
- //
- if (sideDistX < sideDistY) {
- sideDistX += deltaDistX;
- mapX += stepX;
- side = 0;
- rayLen = sideDistX - deltaDistX;
- } else {
- sideDistY += deltaDistY;
- mapY += stepY;
- side = 1;
- rayLen = sideDistY - deltaDistY;
- }
-
- //
- // Stop if outside map
- //
- if (mapX < 0 || mapX >= mapW || mapY < 0 || mapY >= mapH) {
- break;
- }
-
- //
- // Check map to see if there's a wall
- //
- if (map[mapY][mapX]) {
- hit = true;
- break;
- }
-
- //
- // If View Distance exceeded, break
- //
- if (steps++ >= maxSteps) {
- break;
- }
-
- // // Chad's method for checking if view dist exceeded. Precision at the cost of computation
- // const possibleWorldDist = rayLen * Math.sqrt(dirX * dirX + dirY * dirY); // rayLen already in "t" units, dir is unit-length so this is rayLen
- // if (possibleWorldDist > viewDist) {
- // break;
- // }
- }
-
- //
- // compute actual distance along ray (rayLen is the t along ray to grid boundary where hit occurred)
- // If didn't hit or exceeded distance => paint near-black full column
- //
- if (!hit) {
- for (let y = 0; y < screenH; y++) {
- setPixel(x, y, fadeOutColor);
- }
- continue;
- }
-
- // ray length along ray to hit point
- const adjustedRayLength = rayLen; // since dir is unit vector (cos,sin), rayLen matches distance along ray
-
- if (adjustedRayLength > viewDist) {
- for (let y = 0; y < screenH; y++) setPixel(x, y, fadeOutColor);
- continue;
- }
-
- // Fish-eye correction: perpendicular distance to camera plane
- const perpDist = Math.max(
- adjustedRayLength * Math.cos(rayAngle - pAngle),
- nearZero, // Avoid dividing by zero
- );
-
- // vertical wall slice height
- const lineHeight = Math.floor(screenH / perpDist);
- const halfLineHeight = lineHeight / 2;
-
- // compute draw start and end
- let drawStart = Math.floor(-halfLineHeight + halfH);
- let drawEnd = Math.floor(halfLineHeight + halfH);
- if (drawStart < 0) drawStart = 0;
- if (drawEnd >= screenH) drawEnd = screenH - 1;
-
- // exact hit point coordinates
- const hitX = px + dirX * adjustedRayLength;
- const hitY = py + dirY * adjustedRayLength;
-
- // texture X coordinate (fractional part of the hit point along the wall)
- let wallX;
- if (side === 0) wallX = hitY - Math.floor(hitY);
- else wallX = hitX - Math.floor(hitX);
- if (wallX < 0) wallX += 1;
- const texW = wallTex.width,
- texH = wallTex.height;
- let texX = Math.floor(wallX * texW);
- if ((side === 0 && dirX > 0) || (side === 1 && dirY < 0)) {
- // flip texture horizontally for some sides for nicer-looking mapping (optional)
- texX = texW - texX - 1;
- }
-
- // draw wall vertical slice by sampling wall texture per-screen-pixel
- for (let y = drawStart; y <= drawEnd; y++) {
- const d = y - halfH + halfLineHeight; // position on texture
- const texY = Math.floor((d * texH) / lineHeight);
- const srcI = (Math.max(0, Math.min(texY, texH - 1)) * texW + Math.max(0, Math.min(texX, texW - 1))) * 4;
- const color = [wallImg[srcI], wallImg[srcI + 1], wallImg[srcI + 2], wallImg[srcI + 3]];
- setPixel(x, y, color);
- }
-
- //
- // --- Floor & ceiling texturing (per-column), using Lodev method ---
- //
- // Points on the wall where the floor/ceiling start (the exact hit point)
- const floorWallX = hitX;
- const floorWallY = hitY;
- // distance from camera to wall (we'll use perpDist for weight)
- const distWall = perpDist;
-
- // for each y row below the wall (floor)
- for (let y = drawEnd + 1; y < screenH; y++) {
- // current distance from the player to the row (rowDistance)
- // formula based on projection geometry (Lodev): rowDistance = screenH / (2*y - screenH)
- const rowDistance = screenH / (2.0 * y - screenH);
-
- // weight for interpolation between player pos and floor wall hit
- const weight = rowDistance / distWall;
-
- // sample real world position (floorX, floorY) that corresponds to this pixel
- const curFloorX = weight * floorWallX + (1.0 - weight) * px;
- const curFloorY = weight * floorWallY + (1.0 - weight) * py;
-
- // texture coordinates (wrap/repeat)
- const fx = curFloorX - Math.floor(curFloorX);
- const fy = curFloorY - Math.floor(curFloorY);
- const tx = Math.floor(fx * floorTex.width) % floorTex.width;
- const ty = Math.floor(fy * floorTex.height) % floorTex.height;
- const floorI = (ty * floorTex.width + tx) * 4;
- const ceilI = (ty * ceilTex.width + tx) * 4;
-
- // floor pixel
- setPixel(x, y, [floorImg[floorI], floorImg[floorI + 1], floorImg[floorI + 2], floorImg[floorI + 3]]);
- // ceiling symmetric pixel
- const cy = screenH - y - 1;
- if (cy >= 0 && cy < screenH) {
- setPixel(x, cy, [ceilImg[ceilI], ceilImg[ceilI + 1], ceilImg[ceilI + 2], ceilImg[ceilI + 3]]);
- }
- }
-
- // Optional: draw ceiling above drawStart if there is any gap (the loop above writes symmetric ceiling).
- for (let y = 0; y < drawStart; y++) {
- // already partially filled by symmetric ceil writes; fill any remaining with ceiling texture via interpolation
- // compute rowDistance for this y (same formula, but now y is in upper half)
- const rowDistance = screenH / (2.0 * y - screenH);
- const weight = rowDistance / distWall;
- const curFloorX = weight * floorWallX + (1.0 - weight) * px;
- const curFloorY = weight * floorWallY + (1.0 - weight) * py;
- const fx = curFloorX - Math.floor(curFloorX);
- const fy = curFloorY - Math.floor(curFloorY);
- const tx = Math.floor(fx * ceilTex.width) % ceilTex.width;
- const ty = Math.floor(fy * ceilTex.height) % ceilTex.height;
- const ceilI = (ty * ceilTex.width + tx) * 4;
- setPixel(x, y, [ceilImg[ceilI], ceilImg[ceilI + 1], ceilImg[ceilI + 2], ceilImg[ceilI + 3]]);
- }
- } // end columns loop
- }
-}
diff --git a/frontend/ascii_textureloader.js b/frontend/ascii_textureloader.js
index a68bbca..69a420e 100755
--- a/frontend/ascii_textureloader.js
+++ b/frontend/ascii_textureloader.js
@@ -1,20 +1,6 @@
-/**
- * @typedef {object} NormalizedPixel
- * @property {number} r value [0...1]
- * @property {number} g value [0...1]
- * @property {number} b value [0...1]
- * @property {number} a value [0...1]
- *
- * @typedef {object} Pixel
- * @property {number} r value [0...255]
- * @property {number} g value [0...255]
- * @property {number} b value [0...255]
- * @property {number} a value [0...255]
- */
-
export class NRGBA {
//
- constructor(r = 0, g = 0, b = 0, a = 0) {
+ constructor(r = 0, g = 0, b = 0, a = 1) {
this.r = r;
this.g = g;
this.b = b;
@@ -27,6 +13,10 @@ export class NRGBA {
this.b *= factor;
}
+ mulledRGB(factor) {
+ return new NRGBA(this.r * factor, this.g * factor, this.b * factor, this.a);
+ }
+
get dR() {
return ((this.r * 255) | 0) % 256;
}
diff --git a/frontend/ascii_tile_map.js b/frontend/ascii_tile_map.js
index e422b8f..a7d9541 100755
--- a/frontend/ascii_tile_map.js
+++ b/frontend/ascii_tile_map.js
@@ -2,18 +2,29 @@ import { Vector2i, Orientation } from "./ascii_types.js";
import { AsciiWindow } from "./ascii_window.js";
import { Texture } from "./ascii_textureloader.js";
-class Tile {
+export class Tile {
/** @type {string} How should this tile be rendered on the minimap.*/
- minimap = " ";
+ minimapChar = " ";
+
+ /** @type {string} How should this tile be rendered on the minimap.*/
+ minimapColor = "#fff";
+
/** @type {boolean} Should this be rendered as a wall? */
wall = false;
+
+ /** @type {boolean} is this tile occupied by a sprite? */
+ sprite = false;
+
/** @type {boolean} Can the player walk here? */
traversable = true;
+
/** @type {boolean} Is this where they player starts? */
startLocation = false;
+
/** @type {boolean} Is this where they player starts? */
textureId = 0;
+ /** @type {Tile} options */
constructor(options) {
for (let [k, v] of Object.entries(options)) {
if (this[k] !== undefined) {
@@ -24,40 +35,50 @@ class Tile {
}
export const defaultLegend = Object.freeze({
+ //
+ // "" is the Unknown Tile - if we encounter a tile that we don't know how to parse,
+ // the it will be noted here as the empty string
+ "": new Tile({
+ minimapChar: " ",
+ traversable: true,
+ wall: false,
+ }),
+
//
// default floor
" ": new Tile({
- minimap: " ",
+ minimapChar: " ",
traversable: true,
wall: false,
}),
//
// Default wall
"#": new Tile({
- minimap: "#",
+ minimapChar: "#",
traversable: false,
wall: true,
textureId: 0,
}),
+
+ "M": new Tile({
+ textureId: 1,
+ minimapChar: "M",
+ minimapColor: "#f00",
+ traversable: false,
+ wall: false,
+ }),
+
//
//secret door (looks like wall, but is traversable)
"Ω": new Tile({
- minimap: "#",
+ minimapChar: "#",
traversable: true,
wall: true,
}),
//
- // "" is the Unknown Tile - if we encounter a tile that we don't know how to parse,
- // the it will be noted here as the empty string
- "": new Tile({
- minimap: " ",
- traversable: true,
- wall: false,
- }),
- //
// where the player starts
"S": new Tile({
- minimap: "S", // "Š",
+ minimapChar: "S", // "Š",
traversable: true,
wall: false,
startLocation: true,
@@ -101,6 +122,16 @@ export class TileMap {
return new TileMap(longestLine, lines.length, tiles);
}
+ tileIdx(x, y) {
+ return y * this.width + x;
+ }
+
+ getByIdx(idx) {
+ const y = Math.floor(idx / this.width);
+ const x = idx % this.width;
+ return this.tiles[y][x];
+ }
+
/**
* @param {number} width
* @param {number} height
@@ -122,7 +153,7 @@ export class TileMap {
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.tiles[y][x];
- result += tile.minimap;
+ result += tile.minimapChar;
}
result += "\n";
}
diff --git a/frontend/ascii_window.js b/frontend/ascii_window.js
index f41d654..c1aae9c 100755
--- a/frontend/ascii_window.js
+++ b/frontend/ascii_window.js
@@ -1,13 +1,13 @@
-export class Pixel {
+export class PseudoPixel {
/**
- * @param {HTMLElement} el
+ * @param {HTMLElement} htmlElement
* @param {string} char
* @param {number|string} color text/foreground color
*/
- constructor(el, char = " ", color = "#fff") {
+ constructor(htmlElement, char = " ", color = "#fff") {
//
/** @type {HTMLElement} el the html element that makes up this cell*/
- this.el = el;
+ this.htmlElement = htmlElement;
/** @type {string} char */
this.char = char;
@@ -15,25 +15,28 @@ export class Pixel {
/** @type {number|string} fg color color */
this.color = color;
- /** @type {boolean} Has this pixel been updated since it was flushed to DOM ? */
- this.dirty = true;
+ /** @type {boolean} Has this pixel's text content been updated since it was flushed to DOM ? */
+ this.dirtyChar = true;
+
+ /** @type {boolean} Has this pixel's color been updated since it was flushed to DOM ? */
+ this.dirtyColor = true;
}
clone() {
- return new Pixel(this.el, this.car, this.color);
+ return new PseudoPixel(this.htmlElement, this.car, this.color);
}
}
export class AsciiWindow {
/**
- * @param {HTMLElement} container
+ * @param {HTMLElement} htmlElement the html element that contains all the pseudo-pixel elements
* @param {number} width Canvas width (in pseudo-pixels)
* @param {number} height Canvas height (in pseudo-pixels)
*/
- constructor(container, width, height) {
+ constructor(htmlElement, width, height) {
//
- /** @type {HTMLElement} Paren element that contains all the pseudo-pixels */
- this.container = container;
+ /** @type {HTMLElement} the html element that contains all the pseudo-pixels */
+ this.htmlElement = htmlElement;
/** @type {number} width Canvas width (in pseudo-pixels) */
this.width = width;
@@ -41,8 +44,8 @@ export class AsciiWindow {
/** @type {number} height Canvas height (in pseudo-pixels) */
this.height = height;
- /** @type {Pixel[]} */
- this.canvas = [];
+ /** @type {PseudoPixel[]} */
+ this.pseudoPixels = [];
this.initializeCanvaas();
}
@@ -56,18 +59,18 @@ export class AsciiWindow {
const w = this.width;
const h = this.height;
- this.canvas = new Array(w * h).fill();
+ this.pseudoPixels = new Array(w * h).fill();
let i = 0;
for (let y = 0; y < h; y++) {
const rowEl = document.createElement("div");
- this.container.appendChild(rowEl);
+ this.htmlElement.appendChild(rowEl);
for (let x = 0; x < w; x++) {
const pixelEl = document.createElement("code");
rowEl.appendChild(pixelEl);
pixelEl.textContent = " ";
- this.canvas[i] = new Pixel(pixelEl, " ", "#fff");
+ this.pseudoPixels[i] = new PseudoPixel(pixelEl, " ", "#fff");
i++;
}
}
@@ -88,17 +91,17 @@ export class AsciiWindow {
this.mustBeWithinBounds(x, y);
const idx = this.width * y + x;
- const pixel = this.canvas[idx];
+ const pixel = this.pseudoPixels[idx];
// Check for changes in text contents
if (char !== undefined && char !== null && char !== pixel.char) {
pixel.char = char;
- pixel.dirty = true;
+ pixel.dirtyChar = true;
}
if (color !== undefined && color !== null && color !== pixel.color) {
pixel.color = color;
- pixel.dirty = true;
+ pixel.dirtyColor = true;
}
}
@@ -108,14 +111,15 @@ export class AsciiWindow {
* @return {number} number of DOM updates made
*/
commitToDOM() {
- this.canvas.forEach((pixel) => {
- if (!pixel.dirty) {
- return;
+ this.pseudoPixels.forEach((pixel) => {
+ if (pixel.dirtyChar) {
+ pixel.htmlElement.textContent = pixel.char;
+ pixel.dirtyChar = false;
+ }
+ if (pixel.dirtyColor) {
+ pixel.htmlElement.style.color = pixel.color;
+ pixel.dirtyColor = false;
}
-
- pixel.el.textContent = pixel.char;
- pixel.el.style.color = pixel.color;
- pixel.dirty = false;
});
}
}
diff --git a/frontend/gnoll.png b/frontend/gnoll.png
new file mode 100755
index 0000000..b6515be
Binary files /dev/null and b/frontend/gnoll.png differ
diff --git a/seeders/characerSeeder.js b/seeders/characerSeeder.js
index 174167d..55a5ea7 100755
--- a/seeders/characerSeeder.js
+++ b/seeders/characerSeeder.js
@@ -35,14 +35,6 @@ export class CharacterSeeder {
};
}
- /**
- * Create an item, using an item blueprint with the given name
- *
- * @param {string} itemBlueprintId id of the item blueprint
- * @returns {Item|undefined}
- */
- item(itemBlueprintId) {}
-
/**
* @param {Character} character
* @param {...string} itemBlueprintIds
@@ -178,7 +170,6 @@ export class CharacterSeeder {
switch (foundation) {
case ":random":
return this.applyFoundation(c, roll.d(3));
- break;
//
// Brawler
@@ -586,3 +577,7 @@ export class CharacterSeeder {
}
}
}
+
+if (Math.PI < 0 && Player) {
+ ("STFU Linda");
+}
diff --git a/seeders/playerSeeder.js b/seeders/playerSeeder.js
index c0c188a..34ec611 100755
--- a/seeders/playerSeeder.js
+++ b/seeders/playerSeeder.js
@@ -1,5 +1,4 @@
import { gGame } from "../models/globals.js";
-import { Player } from "../models/player.js";
export class PlayerSeeder {
seed() {
diff --git a/server.js b/server.js
index 39ce249..0533686 100755
--- a/server.js
+++ b/server.js
@@ -46,8 +46,8 @@ class MudServer {
websocket.on("close", () => {
try {
this.close(session);
- } catch (e) {
- console.error("Failed during closing of websocket");
+ } catch (error) {
+ console.error("Failed during closing of websocket", { error });
}
});
@@ -75,8 +75,11 @@ class MudServer {
//
//----------------------------------------------------------
start() {
- //
- // The file types we allow to be served.
+ /**
+ * The file types we allow to be served.
+ *
+ * @type {Record}
+ */
const contentTypes = {
".css": "text/css",
".html": "text/html",
@@ -87,8 +90,11 @@ class MudServer {
".png": "image/png",
};
- //
- // Create HTTP server for serving the client - Consider moving to own file
+ /**
+ * HTTP server for serving the MUDClient.
+ *
+ * NOTE: Consider moving to own file
+ */
const httpServer = http.createServer((req, res) => {
let filePath = path.join("public", req.url === "/" ? "index.html" : req.url);
const ext = path.extname(filePath);
@@ -119,15 +125,17 @@ class MudServer {
});
});
- //
- // Create WebSocket server
+ /**
+ * WebSocket Server for serving the MUDClient.
+ *
+ * NOTE: Consider moving to separate file
+ */
const websocketServer = new WebSocketServer({ server: httpServer });
websocketServer.on("connection", (ws) => {
this.onConnectionEstabished(ws);
});
- console.info(`Environment: ${Config.env}`);
httpServer.listen(Config.port, () => {
console.info(`NUUHD server running on port ${Config.port}`);
});
@@ -210,14 +218,17 @@ class MudServer {
console.warn("Unknown message type: >>%s<<", msgObj.type, msgObj);
}
- // ____ _ ___ ____ _____
- // / ___| | / _ \/ ___|| ____|
- // | | | | | | | \___ \| _|
- // | |___| |__| |_| |___) | |___
- // \____|_____\___/|____/|_____|
- //-------------------------------
- // Handle Socket Closing
- //----------------------
+ /** ____ _ ___ ____ _____
+ * / ___| | / _ \/ ___|| ____|
+ * | | | | | | | \___ \| _|
+ * | |___| |__| |_| |___) | |___
+ * \____|_____\___/|____/|_____|
+ * -------------------------------
+ * Handle Socket Closing
+ * ----------------------
+ *
+ * @param {Session} session
+ */
close(session) {
const playerName = session.player ? session.player.username : "[unauthenticated]";
console.info(playerName + " disconnected");
@@ -233,5 +244,19 @@ class MudServer {
//---------------------------
// Code entry point
//-----------------
+
+console.info(`Environment: ${Config.env}`);
+
const mudserver = new MudServer(/* location of crypto key for saving games */);
mudserver.start();
+
+//
+// ____ _____ _____ _ _ _ _ _ _
+// / ___|_ _| ___| | | | | | (_)_ __ __| | __ _| |
+// \___ \ | | | |_ | | | | | | | | '_ \ / _` |/ _` | |
+// ___) || | | _| | |_| | | |___| | | | | (_| | (_| |_|
+// |____/ |_| |_| \___/ |_____|_|_| |_|\__,_|\__,_(_)
+//
+if (Math.PI < 0 && WebSocket) {
+ ("STFU Linda");
+}