diff --git a/frontend/SourceCell.js b/frontend/SourceCell.js
index 2a915e0..1076a7e 100755
--- a/frontend/SourceCell.js
+++ b/frontend/SourceCell.js
@@ -41,7 +41,7 @@ export class SourceCell {
potentialNeighbours(other, direction) {
// sadly, we're not allowed to be friends with ourselves.
if (this === other) {
- console.log("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
+ console.warn("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
// throw new Error("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
}
return (
diff --git a/frontend/WfcCell.js b/frontend/WfcCell.js
index e29933e..67c2306 100755
--- a/frontend/WfcCell.js
+++ b/frontend/WfcCell.js
@@ -13,12 +13,12 @@ export class WfcCell {
*/
constructor(i, x, y, options) {
if (!options.length) {
- console.log("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
+ console.warn("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
throw Error("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
}
if (!(options[0] instanceof SourceCell)) {
- console.log("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
+ console.warn("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
throw Error("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
}
diff --git a/frontend/WfcGrid.js b/frontend/WfcGrid.js
index e6f1f52..173de98 100755
--- a/frontend/WfcGrid.js
+++ b/frontend/WfcGrid.js
@@ -34,7 +34,6 @@ export class WfcGrid {
}
reset() {
- console.log("Resetting Cells");
const [w, h] = [this.width, this.height];
const len = w * h;
this.cells = [];
@@ -44,7 +43,6 @@ export class WfcGrid {
this.cells.push(new WfcCell(i, x, y, this.sourceGrid.clone().cells));
}
- console.log("Done");
}
/**
@@ -87,7 +85,7 @@ export class WfcGrid {
});
if (this.lowEntropyCellIdCache.length === 0) {
- console.log("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy });
+ console.info("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy });
}
}
diff --git a/frontend/ascii_dungeon_crawler.html b/frontend/ascii_dungeon_crawler.html
index ffc534b..21403d4 100755
--- a/frontend/ascii_dungeon_crawler.html
+++ b/frontend/ascii_dungeon_crawler.html
@@ -42,8 +42,8 @@
}
#minimap {
grid-area: minimap;
- font-size: 12px;
- line-height: 11.5px;
+ font-size: 14px;
+ line-height: 13px;
white-space: pre;
display: inline-block;
padding: 2px;
@@ -96,46 +96,44 @@
diff --git a/frontend/ascii_dungeon_crawler.js b/frontend/ascii_dungeon_crawler.js
index 49e2b3c..1716fb1 100755
--- a/frontend/ascii_dungeon_crawler.js
+++ b/frontend/ascii_dungeon_crawler.js
@@ -40,6 +40,7 @@ class Player {
}
set orientation(o) {
+ console.log({ o });
//
// Sanitize o
o = ((o | 0) + 4) % 4;
@@ -137,19 +138,23 @@ class DungeonCrawler {
* @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");
+ if (!(this.rendering.firstPersonRenderer && this.rendering.firstPersonRenderer.ready)) {
+ console.warn("Renderer not ready yet");
return;
}
- this.rendering.firstPersonRenderer.renderFrame(
- camX, // add .5 to get camera into center of cell
- camY, // add .5 to get camera into center of cell
- angle,
- );
+ queueMicrotask(() => {
+ this.rendering.firstPersonRenderer.renderFrame(
+ camX, // add .5 to get camera into center of cell
+ camY, // add .5 to get camera into center of cell
+ angle,
+ );
+ });
}
renderMinimap() {
- this.rendering.miniMapRenderer.draw(this.player.x, this.player.y, this.player.orientation);
+ queueMicrotask(() => {
+ this.rendering.miniMapRenderer.draw(this.player.x, this.player.y, this.player.orientation);
+ });
}
loadMap() {
@@ -158,6 +163,8 @@ class DungeonCrawler {
this.map = TileMap.fromHumanText(mapString);
this.player._posV = this.map.findFirstV({ isStartLocation: true });
+ this.player.orientation = this.map.findFirstTile({ isStartLocation: true }).orientation;
+ console.log(this.player);
if (!this.player._posV) {
throw new Error("Could not find a start location for the player");
@@ -236,8 +243,8 @@ class DungeonCrawler {
// Bumping into a door will open/remove it.
// Bumping into stairs will go down/up (requires confirmation, unless disabled)
// Bumping into a wall sconce will pick up the torch (losing the light on the wall, but gaining a torch that lasts for X turns)
- // Bumping into a trap activates it.
- // Bumping into a treasure opens it.
+ // Bumping into a trap activates it (or reveals it if someone on the team detects it, or of a detect trap spell is running)
+ // Bumping into loot reveals it
console.info(
"bumped into %s at %s (mypos: %s), direction=%d",
@@ -328,10 +335,10 @@ class DungeonCrawler {
//
// Guard: stop animation if it took too long
if (this.animation.targetTime <= performance.now()) {
+ this.animation = {};
this.render(this.player.x, this.player.y, this.player.angle);
this.renderMinimap();
this.renderStatus();
- this.animation = {};
return false;
}
@@ -399,7 +406,7 @@ class DungeonCrawler {
renderStatus() {
//
//
- // Update the compass
+ // Update the compass and status
document.getElementById("status").innerHTML = sprintf(
[
"
",
diff --git a/frontend/ascii_first_person_renderer.js b/frontend/ascii_first_person_renderer.js
index b4f84f3..f474003 100755
--- a/frontend/ascii_first_person_renderer.js
+++ b/frontend/ascii_first_person_renderer.js
@@ -82,23 +82,48 @@ export class FirstPersonRenderer {
/** @type {THREE.Sprite[]} All roaming tiles that regularly needs their positions updated */
this.roamers = [];
+ /** @type {number} how many asynchronous function returns are we waiting for? */
+ this.openAsyncs = 0;
+ /** @type {boolean} Are we ready to render? (have all resources been loaded?) */
+ this.ready = false;
+ /** @type {function} called when the renderer is ready and all resources have been loaded */
+ this.onReady = null;
+
//
this.initMap();
//
this.renderer.setSize(this.asciiWidth * 1, this.asciiHeight * 1);
- this.renderFrame();
+
+ const waitForAsyncs = () => {
+ if (this.ready) {
+ return;
+ }
+ if (this.openAsyncs > 0) {
+ setTimeout(waitForAsyncs, 100);
+ return;
+ }
+
+ this.ready = true;
+ if (typeof this.onReady === "function") {
+ this.onReady();
+ return;
+ }
+
+ this.renderFrame();
+ };
+ setTimeout(waitForAsyncs, 100);
}
getTexture(textureId) {
- console.debug("fetching texture", { textureId });
let texture = this.textures.get(textureId);
if (!texture) {
- console.debug(" miss... loading texture", { textureId });
+ this.openAsyncs++;
texture = new THREE.TextureLoader().load(`${textureId}.png`, (t) => {
t.magFilter = THREE.NearestFilter; // no smoothing when scaling up
t.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
t.generateMipmaps = false; // don’t build mipmaps
+ this.openAsyncs--;
});
this.textures.set(textureId, texture);
}
@@ -111,12 +136,9 @@ export class FirstPersonRenderer {
}
getSpriteMaterial(textureId) {
- console.debug("fetching material", { textureId });
-
let material = this.spriteMaterials.get(textureId);
if (!material) {
- console.log("Creating material", { textureId });
material = new THREE.SpriteMaterial({
map: this.getTexture(textureId),
transparent: true,
@@ -150,7 +172,6 @@ export class FirstPersonRenderer {
this.mainCamera.lookAt(x, y - 1, 0);
this.torch.position.copy(this.mainCamera.position);
- console.log("Initial Camera Position:", this.mainCamera.position);
return;
}
@@ -184,7 +205,8 @@ export class FirstPersonRenderer {
// ---------------------------
const floorGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
const floorMat = new THREE.MeshStandardMaterial({
- color: this.floorColor /* side: THREE.DoubleSide */,
+ color: this.floorColor,
+ /* side: THREE.DoubleSide */
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.position.set(this.map.width / 2, this.map.height / 2, -0.5);
diff --git a/frontend/ascii_minimap.js b/frontend/ascii_minimap.js
index b3cf01a..2996c46 100755
--- a/frontend/ascii_minimap.js
+++ b/frontend/ascii_minimap.js
@@ -35,8 +35,6 @@ export class MiniMap {
* @param {Orientation} orientation
*/
draw(pX, pY, orientation) {
- console.log("Updating minimap", { px: pX, py: pY, orientation });
-
//
// 2D array of tiles that are visible
const visibleTiles = new Array(this.map.height).fill().map(() => new Array(this.map.width).fill(false));
@@ -151,12 +149,12 @@ export class MiniMap {
invertY = true;
break;
case Orientation.EAST:
+ invertY = true;
+ invertX = true;
switchXY = true;
break;
case Orientation.WEST:
switchXY = true;
- invertY = true;
- invertX = true;
break;
}
diff --git a/frontend/ascii_tile_map.js b/frontend/ascii_tile_map.js
index 21801f6..56f7a50 100755
--- a/frontend/ascii_tile_map.js
+++ b/frontend/ascii_tile_map.js
@@ -1,7 +1,35 @@
-import parseOptions, { ParsedCall } from "../utils/callParser.js";
-import { Tile } from "./ascii_tile_types.js";
+import parseOptions, { TileOptions } from "../utils/tileOptionsParser.js";
+import { Tile, WallTile } from "./ascii_tile_types.js";
import { Vector2i } from "./ascii_types.js";
+/**
+ * @typedef {object} TileWithCoords
+ * @property {Tile} tile
+ * @property {number} x
+ * @property {number} y
+ */
+
+/**
+ * @typedef {Map} TileCoordsHashTable
+ */
+
+/**
+ * @callback TileMapForEachCallback
+ * @param {Tile} tile
+ * @param {number} x
+ * @param {number} y
+ * @returns {undefined|any} If undefined is returned, the looping continues, but if anything else is returned, the loop halts, and the return value is passed along to the caller
+ */
+
+/**
+ * @readonly @constant @enum {string}
+ */
+export const CharType = {
+ SYSTEM: "internalMapChar",
+ MINIMAP: "minimapChar",
+ MINIMAP_REVEALED: "revealedMinimapChar",
+};
+
export class TileMap {
/**
* @param {string} str
@@ -22,7 +50,6 @@ export class TileMap {
if (y === 0) {
// Infer the width of the map from the first line
mapWidth = tileStr.length;
- console.log({ mapWidth });
}
// Create a new row in the 2d tiles array
@@ -32,8 +59,6 @@ export class TileMap {
const options = optionStr ? parseOptions(optionStr) : [];
let lineWidth = 0;
- options.length && console.log({ options, y });
-
tileStr.split("").forEach((char, x) => {
//
// Check if there are options in the queue that matches the current character
@@ -59,26 +84,26 @@ export class TileMap {
/**
* @param {Tile[][]} tiles
- * @param {Map} options
*/
constructor(tiles) {
- /** @constant @readonly @type {number} */
- this.height = tiles.length;
- /** @constant @readonly @type {number} */
- this.width = tiles[0].length;
- /** @constant @readonly @type {Tile[][]} */
- this.tiles = tiles;
-
- /** @type {Tile} when probing a coordinate outside the map, this is the tile that is returned */
- this.outOfBoundsWall = this.findFirstV({ looksLikeWall: true });
+ /** @type {number} */ this.height = tiles.length;
+ /** @type {number} */ this.width = tiles[0].length;
+ /** @type {Tile[][]} */ this.tiles = tiles;
+ /** @type {number} */ this.playerStartX = undefined;
+ /** @type {number} */ this.playerStartT = undefined;
+ /** @type {Tile} */ this.outOfBoundsWall = this.getReferenceWallTile();
}
- toString() {
+ /**
+ * @param {CharType} charType
+ * @returns {string}
+ */
+ toString(charType = CharType.SYSTEM) {
let result = "";
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.tiles[y][x];
- result += tile.minimapChar;
+ result += tile[charType];
}
result += "\n";
}
@@ -96,12 +121,12 @@ export class TileMap {
return this.tiles[y][x];
}
- get(x, y) {
+ get(x, y, outOfBounds = this.outOfBoundsWall) {
x |= 0;
y |= 0;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
- return this.outOfBoundsWall;
+ return outOfBounds;
}
return this.tiles[y][x];
@@ -178,7 +203,7 @@ export class TileMap {
* but _stops_ if fn() returns anything but `undefined`,
* and then that return value is returned from `forEach`
*
- * @param { (tile, x,y) => any|undefined ) } fn
+ * @param {TileMapForEachCallback} fn
* @returns any|undefined
*/
forEach(fn) {
@@ -192,23 +217,82 @@ export class TileMap {
}
}
- getArea(xMin, yMin, xMax, yMax) {
- if (xMin > xMax) {
- [xMin, xMax] = [xMax, xMin];
- }
- if (yMin > yMax) {
- [yMin, yMax] = [yMax, yMin];
+ /**
+ * @returns {number}
+ */
+ getTraversableTileCount() {
+ let sum = 0;
+
+ this.forEach((tile) => {
+ if (tile.isTraversable) {
+ sum++;
+ }
+ });
+
+ return sum;
+ }
+
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {typeof Tile} tileClass
+ * @returns {TileWithCoords[]}
+ */
+ getCardinalAdjacentTiles(x, y, tileClass) {
+ /** @type {TileWithCoords[]} */
+ const result = [];
+
+ const testCoords = [
+ [x + 1, y],
+ [x - 1, y],
+ [x, y + 1],
+ [x, y + 1],
+ ];
+
+ for (const [_x, _y] of testCoords) {
+ const _tile = this.get(_x, _y, false);
+
+ if (_tile === false) {
+ // _x, _y was out of bounds, do not add it to result
+ continue;
+ }
+
+ if (tileClass && !(_tile instanceof tileClass)) {
+ // _tile was of invalid type, do not add it to result
+ continue;
+ }
+
+ result.push({ tile: _tile, x: _x, y: _y });
}
- const w = xMax - xMin + 1;
- const h = yMax - yMin + 1;
+ return result;
+ }
+
+ /**
+ * @param {number} minX
+ * @param {number} minY
+ * @param {number} maxX
+ * @param {number} maxY
+ *
+ * @returns {TileMap}
+ */
+ getArea(minX, minY, maxX, maxY) {
+ if (minX > maxX) {
+ [minX, maxX] = [maxX, minX];
+ }
+ if (minY > maxY) {
+ [minY, maxY] = [maxY, minY];
+ }
+
+ const w = maxX - minX + 1;
+ const h = maxY - minY + 1;
let iX = 0;
let iY = 0;
const tiles = new Array(h).fill().map(() => new Array(w));
- for (let y = yMin; y <= yMax; y++) {
- for (let x = xMin; x <= xMax; x++) {
+ for (let y = minY; y <= maxY; y++) {
+ for (let x = minX; x <= maxX; x++) {
const tile = this.tiles[y][x];
if (!tile) {
throw new Error("Dafuqq is happing here?");
@@ -222,11 +306,68 @@ export class TileMap {
return new TileMap(w, h, tiles);
}
- getAreaAround(x, y, radius) {
- return this.getArea(x - radius, y - radius, x + radius, y + radius);
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {number} manhattanRadius
+ */
+ getAreaAround(x, y, manhattanRadius) {
+ return this.getArea(
+ x - manhattanRadius, // minX
+ y - manhattanRadius, // minY
+ x + manhattanRadius, // maxX
+ y + manhattanRadius, // maxY
+ );
+ }
+
+ /**
+ * @param {number} startX
+ * @param {number} startY
+ * @returns {TileCoordsHashTable}
+ */
+ getAllTraversableTilesConnectedTo(startX, startY) {
+ /** @type {TileCoordsHashTable} */
+ const result = new Map();
+
+ const allTilesFlat = new Array(this.width * this.height).fill();
+
+ this.forEach((tile, x, y) => {
+ const idx = x + y * this.width;
+ allTilesFlat[idx] = { tile, x, y };
+ });
+
+ const inspectionStack = [startX + startY * this.width];
+
+ while (inspectionStack.length > 0) {
+ const idx = inspectionStack.pop();
+
+ const { tile, x, y } = allTilesFlat[idx];
+
+ if (!tile.isTraversable) {
+ continue; // Can't walk there, move on
+ }
+
+ if (result.has(idx)) {
+ continue; // Already been here, move on
+ }
+
+ result.set(idx, allTilesFlat[idx]);
+
+ // Add neighbors
+ const [minX, minY] = [1, 1];
+ const maxX = this.width - 2;
+ const maxY = this.height - 2;
+
+ if (y >= minY) inspectionStack.push(idx - this.width); // up
+ if (y <= maxY) inspectionStack.push(idx + this.width); // down
+ if (x >= minX) inspectionStack.push(idx - 1); // left
+ if (x <= maxX) inspectionStack.push(idx + 1); // right
+ }
+
+ return result;
}
}
-if (Math.PI < 0 && ParsedCall) {
+if (Math.PI < 0 && TileOptions && WallTile) {
("STFU Linda");
}
diff --git a/frontend/ascii_tile_types.js b/frontend/ascii_tile_types.js
index ea6e115..0289297 100755
--- a/frontend/ascii_tile_types.js
+++ b/frontend/ascii_tile_types.js
@@ -1,105 +1,211 @@
-import { ParsedCall } from "../utils/callParser";
-import { Orientation, Vector2i } from "./ascii_types";
+import { mustBe, mustBeString } from "../utils/mustbe.js";
+import shallowCopy from "../utils/shallowCopy.js";
+import { TileOptions } from "../utils/tileOptionsParser.js";
+import { Orientation, Vector2i } from "./ascii_types.js";
+
+/**
+ * Array of __internal__ characters used to identify tile types.
+ * These are __not__ necessarily the characters used to display
+ * the tile on the minimap - but they are used when serializing
+ * the maps into a semi-human-readable text-format.
+ *
+ * @constant {Record} */
+export const TileTypes = {
+ [TileChars.FLOOR]: {
+ minimapChar: "·",
+ traversable: true,
+ },
+ [TileChars.WALL]: {
+ minimapChar: "█",
+ minimapColor: "#aaa",
+ textureId: "wall",
+ traversable: false,
+ looksLikeWall: true,
+ },
+ [TileChars.SECRET_PORTAL]: {
+ disguiseAs: TileChars.WALL,
+ revealedMinimapChar: "Ω",
+ revealedMinimapColor: "#EE82EE", //purple
+ revealedTextureId: "secret_portal_revealed",
+ portalTargetId: REQUIRED_ID,
+ looksLikeWall: true,
+ },
+ [TileChars.TELPORTATION_TARGET]: {
+ is: TileChars.FLOOR,
+ id: REQUIRED_ID,
+ orientation: REQUIRED_ORIENTATION,
+ disguiseAs: TileChars.FLOOR,
+ revealedMinimapChar: "𝑥",
+ revealedMinimapColor: "#EE82EE", // purple
+ },
+ [TileChars.ENCOUNTER_START_POINT]: {
+ is: TileChars.FLOOR, // this is actually just a floor tile that is occupied by an encounter when the map is loaded
+ encounterId: REQUIRED_ID,
+ textureId: REQUIRED_ID,
+ occupants: REQUIRED_OCCUPANTS,
+ },
+ [TileChars.PLAYER_START_POINT]: {
+ is: TileChars.FLOOR,
+ orientation: REQUIRED_ORIENTATION,
+ minimapChar: "▤", // stairs/ladder
+ minimapColor: "#FFF",
+ },
+};
export class Tile {
- /** @type {string|number} What is the id of this tile - only interactive tiles have IDs */
+ /** @readonly {string?|number?} Unique (but optional) instance if of this tile */
id;
-
/** @type {string} Icon char of tile */
minimapChar;
- /** @type {string} Icon char of tile after tile's secrets have been revealed */
- revealedMinimapChar;
- /** @type {string} Icon of tile */
+ /** @type {string} Color of the icon of tile */
minimapColor;
- /** @type {string} Icon char of tile after tile's secrets have been revealed */
- revealedMinimapColor;
-
+ /** @type {boolean} Can the player walk here? */
+ isTraversable;
+ /** @type {boolean} Should this be rendered as a wall? */
+ looksLikeWall;
+ /** @type {boolean} Is this where they player starts? */
+ isStartLocation;
/** @type {boolean} Is this a portal exit and/or entry */
isPortal;
/** @type {string|number} Where is the player transported if they enter the portal */
portalTargetId;
-
- /** @type {boolean} Should this be rendered as a wall? */
- looksLikeWall;
- /** @type {boolean} Can the player walk here? */
- isTraversable;
- /** @type {boolean} is this tile occupied by an encounter? */
- isEncounter;
- /** @type {boolean} Is this where they player starts? */
- isStartLocation;
- /** @type {boolean} Has the secret properties of this tile been revealed? */
- isRevealed;
- /** @type {string|number} */
- hasBumpEvent;
- /** @type {string|number} The portals "channel" - each tile in a portal pair must have the same channel */
- channel;
/** @type {number|string} id of texture to use */
textureId;
- /** @type {number|string} id of texture to use after the secrets of this tile has been revealed */
- revealedTextureId;
/** @type {number|string} type of encounter located on this tile. May or may not be unique*/
encounterType;
- /** @type {boolean} Can/does this tile wander around on empty tiles? */
- isRoaming;
+ /** @type {number|string} type of trap located on this tile. May or may not be unique*/
+ trapType;
/** @type {Orientation} */
orientation;
- /** @type {number} If this is a roaming tile, what is its current x-position on the map */
- currentPosX;
- /** @type {number} If this is a roaming tile, what is its current y-position on the map*/
- currentPosY;
+ /** @type {TileType} This tile disguises itself as another tile, and its true properties are revealed later if event is triggered */
+ disguiseAs;
+ /** @type {boolean} Has the secret properties of this tile been revealed? */
+ revealed;
+ /** @type {string} Icon char of tile after tile's secrets have been revealed */
+ revealedMinimapChar;
+ /** @type {string} Color of the icon char of tile after tile's secrets have been revealed */
+ revealedMinimapColor;
+ /** @type {number|string} id of texture to use after the secrets of this tile has been revealed */
+ revealedTextureId;
- static wallMinimapChar = "█";
+ /** @param {Tile} properties */
+ constructor(properties) {
+ mustBe(properties, "object");
- /** @param {Tile} options */
- constructor(options = {}) {
- for (let [k, v] of Object.entries(options)) {
- if (this[k] !== undefined) {
- this[k] = v;
+ //
+ // Copy props from properties.
+ //
+ for (const [key, val] of Object.entries(properties)) {
+ if (typeof val === "symbol" && val.description.startsWith("REQUIRED_")) {
+ console.error(
+ [
+ "REQUIRED_ symbol encountered in Tile constructor. ",
+ "REQUIRED_ is a placeholder, and cannot be used as a value directly",
+ ].join("\n"),
+ { key, val, options: properties },
+ );
+ throw new Error("Incomplete data in constructor. Args may not contain a data placeholder");
}
+
+ if (!Object.hasOwn(this, key) /* Object.prototype.hasOwnProperty.call(this, key) */) {
+ console.warn("Unknown tile property", { key, val, properties });
+ }
+ }
+
+ //
+ // If this tile is disguised, copy its attributes, but
+ // do not overwrite own attributes.
+ //
+ if (this.disguiseAs !== undefined) {
+ this.revealed = false;
+
+ const other = shallowCopy(TileTypes[this.is]);
+ for (const [pKey, pVal] of Object.entries(other)) {
+ if (this.key !== undefined) {
+ this[pKey] = pVal;
+ }
+ }
+ }
+
+ //
+ // If this tile "inherits" properties from another tile type,
+ // copy those properties, but do not overwrite own attributes.
+ //
+ if (this.is !== undefined) {
+ //
+ const other = shallowCopy(TileTypes[this.is]);
+ for (const [pKey, pVal] of Object.entries(other)) {
+ if (this.key !== undefined) {
+ this[pKey] = pVal;
+ }
+ }
+ }
+
+ //
+ // Normalize Orientation
+ //
+ if (this.orientation !== undefined && typeof this.orientation === "string") {
+ const valueMap = {
+ north: Orientation.NORTH,
+ south: Orientation.SOUTH,
+ east: Orientation.EAST,
+ west: Orientation.WEST,
+ };
+ this.orientation = mustBeString(valueMap[this.orientation.toLowerCase()]);
+ }
+
+ if (this.id !== undefined) {
+ mustBe(this.id, "number", "string");
+ }
+ if (this.textureId !== undefined) {
+ mustBe(this.textureId, "number", "string");
+ }
+ if (this.portalTargetId !== undefined) {
+ mustBe(this.portalTargetId, "number", "string");
}
}
/**
* @param {string} char
- * @param {ParsedCall} opt Options
+ * @param {TileOptions} options Options
* @param {number} x
* @param {number} y
*/
- static fromChar(char, opt, x, y) {
- opt = opt ?? new ParsedCall();
- if (!(opt instanceof ParsedCall)) {
- console.error("Invalid options", { char, opt: opt });
+ static fromChar(char, options) {
+ //
+ // Validate Options
+ options = options ?? new TileOptions();
+ if (!(options instanceof TileOptions)) {
+ console.error("Invalid options", { char, opt: options });
throw new Error("Invalid options");
}
- if (char === " ") return new FloorTile();
- if (char === "#") return new WallTile();
- if (char === "P") return new PlayerStartTile(opt.getValue("orientation", 0));
- if (char === "E")
- return new EncounterTile(x, y, opt.getValue("encounterType", 0), opt.getValue("textureId", 1));
- if (char === "Z")
- return new SecretPortalTile(
- opt.getValue("id", 0),
- opt.getValue("destinationid", 1),
- opt.getValue("orientation", 3),
- );
- console.warn("Unknown character", { char, options: opt });
- return new FloorTile();
- }
+ const typeInfo = TileTypes[char];
- hasTexture() {
- if (typeof this.textureId === "number") {
- return true;
+ let optionPos = 0;
+ const creationArgs = {};
+ const getOption = (name) => options.getValue(name, optionPos++);
+ for (let [key, val] of Object.entries(typeInfo)) {
+ //
+ const fetchFromOption = typeof val === "symbol" && val.descript.startsWith("REQUIRED_");
+
+ creationArgs[key] = fetchFromOption ? getOption(key) : shallowCopy(val);
}
- if (typeof this.textureId === "string" && this.textureId !== "") {
- return true;
- }
-
- return false;
- }
-
- getBumpEvent() {
- return null;
}
clone() {
@@ -107,77 +213,6 @@ export class Tile {
}
}
-export class FloorTile extends Tile {
- isTraversable = true;
- minimapChar = "·";
- minimapColor = "#555";
- internalMapChar = " ";
-}
-
-export class PlayerStartTile extends Tile {
- isTraversable = true;
- isStartLocation = true;
- minimapChar = "▤"; // stairs
- orientation = Orientation.NORTH;
-
- /** @param {Orientation} orientation */
- constructor(orientation) {
- super({ orientation });
- }
-}
-
-export class WallTile extends Tile {
- textureId = "wall";
- isTraversable = false;
- looksLikeWall = true;
- internalMapChar = "#";
- minimapChar = Tile.wallMinimapChar;
- minimapColor = "#aaa";
-}
-
-export class EncounterTile extends Tile {
- isEncounter = true;
- isRoaming = true;
- minimapChar = "†";
- minimapColor = "#f44";
- hasBumpEvent = true;
-
- /**
- * @param {number} x x-component of the encounter's initial position
- * @param {number} y y-component of the encounter's initial position
- * @param {string|number} encounterType name/id of the encounter that will be triggered when player bumps into this tile
- * @param {string|number} textureId id of the texture to use.
- */
- constructor(x, y, encounterType, textureId) {
- super();
- this.textureId = textureId ?? encounterType;
- this.encounterType = encounterType;
- this.currentPosX = x;
- this.currentPosY = y;
- this.id = `E_${encounterType}_${x}_${y}`;
- console.info("creating encounter", { encounter: this });
- }
-
- getBumpEvent() {
- return ["attack", { encounterType: this.encounterType }];
- }
-}
-
-export class SecretPortalTile extends WallTile {
- revealedTextureId = "secretTwoWayPortal";
- isPortal = true;
- internalMapChar = "Z";
- isRevealed = false;
- revealedMinimapChar = "Ω";
- revealedMinimapColor = "#4f4";
-
- // Change minimap char once the tile's secret has been uncovered.
-
- constructor(id, portalTargetId, orientation) {
- super({ id, portalTargetId, orientation });
- }
-}
-
-if (Math.PI < 0 && ParsedCall && Orientation && Vector2i) {
+if (Math.PI < 0 && TileOptions && Orientation && Vector2i) {
("STFU Linda");
}
diff --git a/frontend/ascii_types.js b/frontend/ascii_types.js
index 518c918..784bb5f 100755
--- a/frontend/ascii_types.js
+++ b/frontend/ascii_types.js
@@ -4,16 +4,47 @@ export const PI_OVER_TWO = Math.PI / 2;
* Enum Cardinal Direction (east north west south)
* @constant @readonly @enum {number}
*/
-export const Orientation = {
+export class Orientation {
/** @constant @readonly @type {number} */
- EAST: 0,
+ static EAST = 0;
/** @constant @readonly @type {number} */
- SOUTH: 1,
+ static SOUTH = 1;
/** @constant @readonly @type {number} */
- WEST: 2,
+ static WEST = 2;
/** @constant @readonly @type {number} */
- NORTH: 3,
-};
+ static NORTH = 3;
+
+ /**
+ * @param {string} str
+ * @returns {Orientation}
+ */
+ static fromString(str) {
+ if (typeof str !== "string") {
+ console.error(
+ "Invalid data type when converting string to orientation. >>str<< is not a string be string.",
+ { str },
+ );
+ return undefined;
+ }
+ str = str.toLowerCase();
+ if (str === "east") return Orientation.EAST;
+ if (str === "west") return Orientation.WEST;
+ if (str === "north") return Orientation.NORTH;
+ if (str === "south") return Orientation.SOUTH;
+ }
+
+ /**
+ * @param {string|number} val
+ * @returns {Orientation}
+ */
+ static normalize(val) {
+ if (typeof val === "string") {
+ return Orientation.fromString(val);
+ }
+
+ return val % 4;
+ }
+}
/**
* Enum Relative Direction (forward, left, right, backwards)
@@ -190,3 +221,7 @@ export class Vector2i {
return `[${this.x} , ${this.y}]`;
}
}
+
+const o = Orientation.fromString("south");
+
+console.log(o);
diff --git a/frontend/client.js b/frontend/client.js
index 266fd82..6a5a78f 100755
--- a/frontend/client.js
+++ b/frontend/client.js
@@ -76,8 +76,6 @@ class MUDClient {
// TODO Fix. Port should not be hardcoded
const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, ":3000");
- console.log(wsUrl);
-
this.updateStatus("Connecting...", "connecting");
try {
@@ -106,7 +104,7 @@ class MUDClient {
};
this.websocket.onerror = (error) => {
- console.log("Websocket error", error);
+ console.warn("Websocket error", error);
this.updateStatus("Connection Error", "error");
this.writeToOutput("Connection error occurred. Retrying...", { class: "error" });
};
@@ -137,7 +135,7 @@ class MUDClient {
* @param {...any} rest
*/
send(messageType, ...args) {
- console.log("sending", messageType, args);
+ console.debug("sending", messageType, args);
if (args.length === 0) {
this.websocket.send(JSON.stringify([messageType]));
@@ -202,7 +200,6 @@ class MUDClient {
// The quit command has its own message type
let help = helpRegex.exec(inputText);
if (help) {
- console.log("here");
help[1] ? this.send(MessageType.HELP, help[1].trim()) : this.send(MessageType.HELP);
this.echo(inputText);
return;
diff --git a/frontend/dungeon_generator.html b/frontend/dungeon_generator.html
deleted file mode 100644
index 7f2f602..0000000
--- a/frontend/dungeon_generator.html
+++ /dev/null
@@ -1,573 +0,0 @@
-
-
-
-
-
- ASCII Dungeon Generator
-
-
-
-