diff --git a/frontend/ascii_dungeon_crawler.html b/frontend/ascii_dungeon_crawler.html
index b65e595..d85b4ae 100755
--- a/frontend/ascii_dungeon_crawler.html
+++ b/frontend/ascii_dungeon_crawler.html
@@ -72,12 +72,11 @@
-
-
-
orientation
-
+
orientation
+
+
←→↑↓
@@ -87,12 +86,12 @@
############################################################
## ################# ########################
## # ### ################# # ## ########################
-## #S# ################# # ## ################
+## #P# Z###############Z # ## ################ ::: P(north) Z(1) Z(1) ;; Comments
## # # # ################# # ## #### ####
-## M # # ## # #### # # ####
+## E # # ## # #### # # #### ::: E(Gnolls)
###### #################### ## #### # ####
###### #################### # ## # # #### ####
-######M#################### # ####
+######E#################### # #### ::: E(Goblins) These are comments
###### #################### ########## #### ####
###### #################### ########## # # #### # # ####
###### #################### ########## #### # # ####
@@ -100,23 +99,23 @@
###### #################### ############################
###### #################### # ############################
###### #################### # ############################
-######M#################### # ############################
-###### ## ########### ############################
-###### ## ########### # # ############################
-###### ## ########### ######## ############
+######E#################### # ############################ ::: E(Minotaur)
+###### ## ##### ## ############################
+###### ## Z#### ## # # ############################ ::: Z(2) // Channel 2
+###### ## ####Z ## ######## ############ ::: Z(2) // Channel 2
###### ## ## # ########### ## ######## ############
-###### ## # # ############
+######E## # #E ############ ::: E(Dwarf) ; E(Gelatinous_Cube)
###### # # # ############
######### # ## ########### # ######### # ############
######### # # ########### # ######### # # ############
######### ########### # ######### ############
-########################### # ######### #### ###############
+###########O############### # ######### #### ############### ::: O(1)
########################### # ######### #### ###############
########################### # ######### #### ###############
########################### # ######### #### ###############
########################### # #### ###############
######################### # #### # # # ######## ###
-######################### # # ######## # ###
+########################o # # ######## # ### ::: o:2
######################### # ##### # # # # ######## ###
######################### # # ###
######################### ####################### # ###
diff --git a/frontend/ascii_dungeon_crawler.js b/frontend/ascii_dungeon_crawler.js
index a410568..e4827c6 100755
--- a/frontend/ascii_dungeon_crawler.js
+++ b/frontend/ascii_dungeon_crawler.js
@@ -3,8 +3,6 @@ import { DefaultRendererOptions, FirstPersonRenderer } from "./ascii_first_perso
import { MiniMapRenderer } from "../ascii_minimap_renderer.js";
import { AsciiWindow } from "./ascii_window.js";
import { TileMap } from "./ascii_tile_map.js";
-import eobWallUrl1 from "./eob1.png";
-import gnollSpriteUrl from "./gnoll.png";
import { sprintf } from "sprintf-js";
class Player {
@@ -109,7 +107,7 @@ class DungeonCrawler {
/** @type {FirstPersonRenderer} */ firstPersonRenderer: null,
/** @type {MiniMapRenderer} */ miniMapRenderer: null,
- firstPersonWindow: new AsciiWindow(document.getElementById("viewport"), 100, 45), // MAGIC CONSTANTS
+ firstPersonWindow: new AsciiWindow(document.getElementById("viewport"), 80, 45), // MAGIC CONSTANTS
minimapWindow: new AsciiWindow(document.getElementById("minimap"), 9, 9), // MAGIC CONSTANT
options: DefaultRendererOptions,
@@ -163,11 +161,10 @@ class DungeonCrawler {
this.rendering.miniMapRenderer = new MiniMapRenderer(this.rendering.minimapWindow, this.map);
- const textureFilenames = [eobWallUrl1, gnollSpriteUrl];
this.rendering.firstPersonRenderer = new FirstPersonRenderer(
this.rendering.firstPersonWindow,
this.map,
- textureFilenames,
+ ["./eobBlueWall.png", "gnoll.png"], // textures
this.rendering.options,
);
this.rendering.firstPersonRenderer.onReady = () => {
@@ -216,15 +213,15 @@ class DungeonCrawler {
//
// We cant move into walls
- if (this.map.isWall(targetV.x, targetV.y)) {
+ if (!this.map.isTraversable(targetV.x, targetV.y)) {
console.info(
- "bumped into wall at %s (mypos: %s), direction=%d",
+ "bumped into an obstacle at %s (mypos: %s), direction=%d",
targetV,
this.player._posV,
this.player.angle,
);
- // this.delay += 250; // MAGIC NUMBER: Pause for a tenth of a second after hitting a wall
- // return false;
+ this.delay += 250; // MAGIC NUMBER: Pause for a bit after hitting an obstacle
+ return false;
}
this.animation = {
diff --git a/frontend/ascii_first_person_renderer.js b/frontend/ascii_first_person_renderer.js
index f34422d..97c3530 100755
--- a/frontend/ascii_first_person_renderer.js
+++ b/frontend/ascii_first_person_renderer.js
@@ -1,20 +1,15 @@
-import { TileMap, Tile } from "./ascii_tile_map.js";
+import { TileMap } from "./ascii_tile_map.js";
+import { Tile } from "./ascii_tile_types.js";
import { AsciiWindow } from "./ascii_window.js";
import * as THREE from "three";
-import eobWallUrl1 from "./eob1.png";
-import gnollSpriteUrl from "./gnoll.png";
+import { Vector3 } from "three";
export const DefaultRendererOptions = {
viewDistance: 5,
- fov: Math.PI / 3, // 60 degrees - good for spooky
-
- wallChar: "#",
+ fov: 60, // degrees
floorColor: 0x654321,
- floorChar: "f",
ceilingColor: 0x555555,
- ceilingChar: "c",
- fadeOutColor: 0x555555,
};
export class FirstPersonRenderer {
@@ -24,37 +19,64 @@ export class FirstPersonRenderer {
* @param {string[]} textureFilenames
*/
constructor(aWindow, map, textureFilenames, options) {
- const w = 600;
- const h = 400;
+ this.map = map;
+ this.window = aWindow;
+
+ this.widthPx = aWindow.htmlElement.clientWidth;
+ this.heightPx = aWindow.htmlElement.clientHeight;
+ this.asciiWidth = aWindow.width;
+ this.asciiHeight = aWindow.height;
+ this.aaspect = this.widthPx / this.heightPx;
this.fov = options.fov ?? DefaultRendererOptions.fov;
this.viewDistance = options.viewDistance ?? DefaultRendererOptions.viewDistance;
-
- this.window = aWindow;
- this.map = map;
+ this.floorColor = options.floorColor ?? DefaultRendererOptions.floorColor;
+ this.ceilingColor = options.ceilingColor ?? DefaultRendererOptions.ceilingColor;
this.scene = new THREE.Scene();
- this.camera = new THREE.PerspectiveCamera((this.fov * 180) / Math.PI, w / h);
- this.renderer = new THREE.WebGLRenderer({ antialias: false }); // Do not anti-alias, it could interfere with the conversion to ascii
+ this.mainCamera = new THREE.PerspectiveCamera(this.fov, this.aspect, 0.1, this.viewDistance);
+ this.renderer = new THREE.WebGLRenderer({
+ antialias: false,
+ preserveDrawingBuffer: true,
+ }); // Do not anti-alias, it could interfere with the conversion to ascii
+
+ //
+ // Render buffer
+ //
+ this.bufferCanvas = document.createElement("canvas");
+ this.bufferCanvas.width = this.asciiWidth;
+ this.bufferCanvas.height = this.asciiHeight;
+ this.bufferContext = this.bufferCanvas.getContext("2d");
//
// Fog, Fadeout & Background
//
this.scene.background = new THREE.Color(0);
- this.scene.fog = new THREE.Fog(0, 0, this.viewDistance - 1);
+ this.scene.fog = new THREE.Fog(0, 0, this.viewDistance);
//
// Camera
//
- this.camera.up.set(0, 0, 1); // Z-up instead of Y-up
+ this.mainCamera.up.set(0, 0, 1); // Z-up instead of Y-up
//
// Torch
//
- this.torch = new THREE.PointLight(0xffffff, 0.9, this.viewDistance, 2); // https://threejs.org/docs/#api/en/lights/PointLight
- this.torch.position.copy(this.camera.position);
+ this.torch = new THREE.PointLight(0xffffff, 2, this.viewDistance * 2, 1); // https://threejs.org/docs/#api/en/lights/PointLight
+ this.torch.position.copy(this.mainCamera.position);
this.scene.add(this.torch);
+ this.textures = [];
+
+ for (const textureFile of textureFilenames) {
+ const tex = new THREE.TextureLoader().load(textureFile, (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.textures.push(tex);
+ }
+
//
// Sprites
//
@@ -65,8 +87,7 @@ export class FirstPersonRenderer {
this.initMap();
//
- this.renderer.setSize(w, h);
- document.getElementById("threejs").appendChild(this.renderer.domElement);
+ this.renderer.setSize(this.asciiWidth * 1, this.asciiHeight * 1);
this.renderFrame();
}
@@ -74,15 +95,19 @@ export class FirstPersonRenderer {
const wallPlanes = [];
const sprites = [];
+ //
+ // -------------
+ // PARSE THE MAP
+ // -------------
/** @type {Map {
//
if (tile.isStartLocation) {
- this.camera.position.set(x, y, 0);
- this.camera.lookAt(x, y - 1, 0);
- this.torch.position.copy(this.camera.position);
+ this.mainCamera.position.set(x, y, 0);
+ this.mainCamera.lookAt(x, y - 1, 0);
+ this.torch.position.copy(this.mainCamera.position);
- console.log("Initial Camera Position:", this.camera.position);
+ console.log("Initial Camera Position:", this.mainCamera.position);
return;
}
@@ -102,7 +127,7 @@ export class FirstPersonRenderer {
return;
}
- if (tile.isSprite) {
+ if (tile.isEncounter) {
console.log("Sprite", tile);
sprites.push([x, y, tile.textureId]);
return;
@@ -112,41 +137,48 @@ export class FirstPersonRenderer {
});
//
- // Floor (XY plane at Z = -.5)
- //
+ // ---------------------------
+ // FLOOR (XY PLANE AT Z = -.5)
+ // ---------------------------
const floorGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
- const floorMat = new THREE.MeshStandardMaterial({ color: 0x964b00 /* side: THREE.DoubleSide */ });
+ const floorMat = new THREE.MeshStandardMaterial({
+ 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);
this.scene.add(floor);
//
- // Ceiling (XY plane at Z = .5)
- //
+ // -----------------------------
+ // CEILING (XY PLANE AT Z = .5)
+ // -----------------------------
const ceilingGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
- const ceilingMat = new THREE.MeshStandardMaterial({ color: 0x333333, side: THREE.BackSide });
+ const ceilingMat = new THREE.MeshStandardMaterial({
+ color: this.ceilingColor,
+ side: THREE.BackSide,
+ });
const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat);
ceiling.position.set(this.map.width / 2, this.map.height / 2, 0.5);
this.scene.add(ceiling);
//
- // Walls
- //
- const wallTex = new THREE.TextureLoader().load(eobWallUrl1, (texture) => {
- texture.magFilter = THREE.NearestFilter; // no smoothing when scaling up
- texture.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
- texture.generateMipmaps = false; // don’t build mipmaps
- });
-
+ // ------
+ // WALLS
+ // ------
const wallGeo = new THREE.PlaneGeometry();
wallGeo.rotateX(Math.PI / 2); // Get the geometry-plane the right way up (z-up)
- // wallGeo.rotateY(Math.PI); // rotate textures to be the right way up
+ wallGeo.rotateY(Math.PI); // rotate textures to be the right way up
const instancedMesh = new THREE.InstancedMesh(
wallGeo,
- new THREE.MeshStandardMaterial({ map: wallTex }),
+ new THREE.MeshStandardMaterial({ map: this.textures[0] }),
wallPlanes.length,
);
+ instancedMesh.userData.pastelMaterial = new THREE.MeshBasicMaterial({
+ color: 0xffffff,
+ });
+
+ instancedMesh.userData.parimaryMaterial = instancedMesh.material;
this.scene.add(instancedMesh);
// Temp objects for generating matrices
@@ -162,29 +194,19 @@ export class FirstPersonRenderer {
instancedMesh.instanceMatrix.needsUpdate = true;
//
- // Sprites
+ // -------
+ // SPRITES
+ // -------
//
- // Load a sprite texture
-
- const tex = new THREE.TextureLoader().load(gnollSpriteUrl, (t) => {
- t.magFilter = THREE.NearestFilter; // pixel-art crisp
- t.minFilter = THREE.NearestFilter;
- t.generateMipmaps = false;
- t.wrapS = THREE.RepeatWrapping;
- t.wrapT = THREE.RepeatWrapping;
- t.repeat.set(1, 1);
- });
-
- const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
-
for (const [x, y, textureId] of sprites) {
+ // TODO: only one material per sprite type
+ const spriteMat = new THREE.SpriteMaterial({
+ map: this.textures[textureId],
+ transparent: true,
+ });
const sprite = new THREE.Sprite(spriteMat);
- sprite.position.set(
- x,
- y,
- 0, // z (stand on floor)
- );
sprite.position.set(x, y, 0);
+ sprite.userData.mapLocation = new Vector3(x, y, 0); // The location (in tilemap coordinates) of this sprite
this.sprites.push(sprite);
this.scene.add(sprite);
console.log({ x, y, textureId });
@@ -192,23 +214,115 @@ export class FirstPersonRenderer {
}
renderFrame(posX, posY, dirAngle, commit = true) {
- this.renderer.render(this.scene, this.camera);
- const lookAtV = new THREE.Vector3(1, 0, 0);
- lookAtV
- .applyAxisAngle(new THREE.Vector3(0, 0, 1), dirAngle)
- .normalize()
- .add(this.camera.position);
+ //
+ const posV = new Vector3(posX, posY, 0);
- this.camera.position.x = posX;
- this.camera.position.y = posY;
+ //
+ // -------------------------------
+ // Camera Position and Orientation
+ // -------------------------------
+ //
+ // Direction we're looking
+ const lookDirV = new Vector3(1, 0, 0)
+ .applyAxisAngle(new Vector3(0, 0, 1), dirAngle)
+ .setZ(0)
+ .normalize();
- this.torch.position.copy(this.camera.position);
- this.torch.position.z += 0.25;
- this.camera.lookAt(lookAtV);
+ //
+ // The Point we're looking at.
+ //
+ const lookAtV = lookDirV.clone().add(posV);
+ lookAtV.z = 0;
+
+ this.mainCamera.position.copy(posV); // Move the camera
+ this.mainCamera.lookAt(lookAtV); // Rotate the camera
+
+ // -----
+ // TORCH
+ // -----
+ //
+ // The torch should hover right above the camera
+ this.torch.position.set(posV.x, posV.y, posV.z + 0.25);
+
+ // -------
+ // SPRITES
+ // -------
+ //
+ this.sprites.forEach((sprite) => {
+ //
+ // The tilemap position (vector) of the sprite
+ /** @type {Vector3} */
+ const spriteCenterV = sprite.userData.mapLocation;
+
+ //
+ // Direction from sprite to camera
+ const dir = new Vector3().subVectors(spriteCenterV, posV);
+ const len = dir.length();
+
+ //
+ if (len > this.viewDistance) {
+ // Sprite is out of range, do nothing
+ return;
+ }
+
+ if (Math.abs(dir.x) > 1e-6 && Math.abs(dir.y) > 1e-6) {
+ // Sprite is not in a direct cardinal line to us, do nothing
+ return;
+ }
+
+ sprite.position.copy(spriteCenterV).addScaledVector(lookDirV, -0.5);
+ });
+
+ performance.mark("scene_render_start");
+ this.renderer.render(this.scene, this.mainCamera);
+ performance.mark("scene_render_end");
+ performance.measure("3D Scene Rendering", "scene_render_start", "scene_render_end");
+
+ //
+ //
+ // ----------------
+ // ASCII Conversion
+ // ----------------
+ //
+ performance.mark("asciification_start");
+ const gl = this.renderer.getContext();
+ const width = this.renderer.domElement.width;
+ const height = this.renderer.domElement.height;
+
+ const pixels = new Uint8Array(width * height * 4);
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+
+ let idx = 0;
+ for (let y = height - 1; y >= 0; y--) {
+ for (let x = 0; x < width; x++) {
+ const r = pixels[idx];
+ const g = pixels[idx + 1];
+ const b = pixels[idx + 2];
+
+ const cssColor =
+ "#" + //
+ r.toString(16).padStart(2, "0") +
+ g.toString(16).padStart(2, "0") +
+ b.toString(16).padStart(2, "0");
+
+ this.window.put(x, y, "#", cssColor);
+
+ idx += 4;
+ }
+ }
+ performance.mark("asciification_end");
+ performance.measure(
+ "Asciification", // The name for our measurement
+ "asciification_start", // The starting mark
+ "asciification_end", // The ending mark
+ );
//
if (commit) {
+ performance.mark("dom_commit_start");
this.window.commitToDOM();
+ performance.mark("dom_commit_end");
+ performance.measure("DOM Commit", "dom_commit_start", "dom_commit_end");
}
}
}
diff --git a/frontend/ascii_tile_map.js b/frontend/ascii_tile_map.js
index cfa79d6..f78db9c 100755
--- a/frontend/ascii_tile_map.js
+++ b/frontend/ascii_tile_map.js
@@ -1,129 +1,40 @@
+import { FunctionCallParser } from "../utils/callParser.js";
import { Vector2i, Orientation } from "./ascii_types.js";
import { AsciiWindow } from "./ascii_window.js";
-export class Tile {
- /** @type {string} How should this tile be rendered on the minimap.*/
- minimapChar = " ";
-
- /** @type {string} How should this tile be rendered on the minimap.*/
- minimapColor = "#fff";
-
- /** @type {boolean} Should this be rendered as a wall? */
- isWall = false;
-
- /** @type {boolean} is this tile occupied by a sprite? */
- isSprite = false;
-
- /** @type {boolean} Can the player walk here? */
- traversable = true;
-
- /** @type {boolean} Is this where they player starts? */
- isStartLocation = 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) {
- this[k] = v;
- }
- }
- }
-
- get collision() {
- return this.isWall || this.isSprite;
- }
-}
-
-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,
- isWall: false,
- }),
-
- //
- // default floor
- " ": new Tile({
- minimapChar: " ",
- traversable: true,
- isWall: false,
- }),
- //
- // Default wall
- "#": new Tile({
- minimapChar: "#",
- traversable: false,
- isWall: true,
- textureId: 0,
- }),
-
- "M": new Tile({
- textureId: 1,
- minimapChar: "M",
- minimapColor: "#f00",
- traversable: false,
- isWall: false,
- isSprite: true,
- }),
-
- //
- //secret door (looks like wall, but is traversable)
- "Ω": new Tile({
- minimapChar: "#",
- traversable: true,
- isWall: true,
- }),
- //
- // where the player starts
- "S": new Tile({
- minimapChar: "S", // "Š",
- traversable: true,
- isWall: false,
- isStartLocation: true,
- }),
-});
-
export class TileMap {
/**
* @param {string} str
* @param {Record Math.max(acc, line.length), 0);
-
- const tiles = new Array(lines.length).fill().map(() => Array(longestLine));
+ let mapWidth;
lines.forEach((line, y) => {
- line = line.padEnd(longestLine, "#");
+ tiles[y] = [];
+ options[y] = [];
- line.split("").forEach((char, x) => {
- let tile = legend[char];
+ // Everything before ":::" is map tiles, and everything after is options for the tiles on that line
+ let [tileStr, optionStr] = line.split(/\s*:::\s*/);
- // unknown char?
- // check fallback tile.
- if (tile === undefined) {
- tile = legend[""];
- }
+ // Infer the width of the map from the first line
+ if (!mapWidth) {
+ mapWidth = tileStr.length;
+ }
- // still no tile - i.e. no back fallback tile?
- if (tile === undefined) {
- throw new Error("Dont know how to handle this character: " + char);
- }
+ optionStr = optionStr.split(/\s*\/\//)[0];
+ options[y] = optionStr ? optionsParser.parse(optionStr) : [];
- // insert tile into map.
- tiles[y][x] = tile;
- });
+ // STFU Linda
+ console.log(tileStr, optionStr, y);
});
- return new TileMap(longestLine, lines.length, tiles);
+ // return new TileMap(longestLine, lines.length, tiles, options);
}
tileIdx(x, y) {
@@ -184,9 +95,25 @@ export class TileMap {
return true;
}
+ if (!this.tiles[y][x]) {
+ x++;
+ return true;
+ }
+
return this.tiles[y][x].isWall;
}
+ isTraversable(x, y) {
+ x |= 0;
+ y |= 0;
+
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
+ return true;
+ }
+
+ return this.tiles[y][x].isTraversable;
+ }
+
findFirst(criteria) {
return this.forEach((tile, x, y) => {
for (let k in criteria) {
diff --git a/frontend/ascii_tile_types.js b/frontend/ascii_tile_types.js
new file mode 100755
index 0000000..056932c
--- /dev/null
+++ b/frontend/ascii_tile_types.js
@@ -0,0 +1,147 @@
+import { Orientation } from "./ascii_types";
+
+export class Tile {
+ /** @type {string} How should this tile be rendered on the minimap.*/
+ minimapChar;
+ /** @type {string} How should this tile be rendered on the minimap.*/
+ minimapColor;
+ /** @type {boolean} Should this be rendered as a wall? */
+ isWall;
+ /** @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} Is this a two-way portal entry/exit */
+ isTwoWayPortal;
+ /** @type {boolean} Is this a one-way portal entry */
+ isOneWayPortalEntry;
+ /** @type {boolean} Is this a one-way portal exit */
+ isOneWayPortalExit;
+ /** @type {boolean} Has the secret properties of this tile been uncovered? */
+ isUncovered;
+ /** @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 the encounter located on this tile */
+ encounterId;
+ /** @type {boolean} Can/does this tile wander around on empty tiles? */
+ isWandering;
+ /** @type {Orientation} */
+ orientation;
+
+ /** @param {Tile} options */
+ constructor(options) {
+ for (let [k, v] of Object.entries(options)) {
+ if (this[k] !== undefined) {
+ this[k] = v;
+ }
+ }
+ }
+
+ /** @param {Tile} options */
+ static fromChar(char, options = {}) {
+ switch (char) {
+ case " ":
+ return new FloorTile();
+ case "#":
+ return new WallTile();
+ case "P":
+ return new PlayerStartTile(options.orientation);
+ case "E":
+ return new EncounterTile(options.textureId, options.encounterId);
+ case "O":
+ return new SecretOneWayPortalEntryTile(options.channel);
+ case "o":
+ return new SecretOneWayPortalExitTile(options.channel);
+ case "Z":
+ return new SecretTwoWayPortalTile(options.channel);
+ default:
+ throw new Error("Unknown character: " + char);
+ }
+ }
+
+ clone() {
+ return new this.constructor(this);
+ }
+}
+
+export class FloorTile extends Tile {
+ isTraversable = true;
+ minimapChar = " ";
+ 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 = 0;
+ isTraversable = false;
+ isWall = true;
+ minimapChar = "#";
+ internalMapChar = "#";
+}
+
+export class EncounterTile extends Tile {
+ isEncounter = true;
+ constructor(textureId, encounterId) {
+ super({ textureId, encounterId });
+ }
+}
+
+/**
+ * One-way portal entries look exactly like walls. You need to
+ * probe for them, or otherwise unlock their location.
+ * You can walk into them, and then the magic happens
+ */
+export class SecretOneWayPortalEntryTile extends Tile {
+ textureId = 0;
+ isWall = true;
+ isTraversable = true; // we can walk in to it?
+ isOneWayPortalEntry = true;
+ internalMapChar = "O";
+ minimapChar = "#"; // Change char when the portal has been uncovered
+ isUncovered = false;
+
+ constructor(channel) {
+ super({ channel });
+ }
+}
+
+export class SecretOneWayPortalExitTile extends Tile {
+ isTraversable = true;
+ isOneWayPortalExit = true;
+ internalMapChar = "o";
+ minimapChar = " "; // Change char when the portal has been uncovered
+ isUncovered = false;
+
+ constructor(channel) {
+ super({ channel });
+ }
+}
+
+export class SecretTwoWayPortalTile extends Tile {
+ textureId = 0;
+ isWall = true;
+ isTraversable = true;
+ isTwoWayPortalEntry = true;
+ internalMapChar = "0";
+ minimapChar = "#"; // Change char when the portal has been uncovered
+ isUncovered = false;
+
+ constructor(channel) {
+ super({ channel });
+ }
+}
diff --git a/frontend/ascii_types.js b/frontend/ascii_types.js
index 2419321..dd73106 100755
--- a/frontend/ascii_types.js
+++ b/frontend/ascii_types.js
@@ -2,8 +2,7 @@ export const PI_OVER_TWO = Math.PI / 2;
/**
* Enum Cardinal Direction (east north west south)
- * @constant
- * @readonly
+ * @constant @readonly @enum {number}
*/
export const Orientation = {
/** @constant @readonly @type {number} Going east increases X */
@@ -18,7 +17,7 @@ export const Orientation = {
/**
* Enum Relative Direction (forward, left, right, backwards)
- * @readonly
+ * @constant @readonly @enum {number}
*/
export const RelativeMovement = {
FORWARD: 0,
diff --git a/frontend/eob2.png b/frontend/eob2.png
deleted file mode 100644
index 4e16031..0000000
Binary files a/frontend/eob2.png and /dev/null differ
diff --git a/frontend/eob1.png b/frontend/eobBlueWall.png
old mode 100644
new mode 100755
similarity index 100%
rename from frontend/eob1.png
rename to frontend/eobBlueWall.png
diff --git a/frontend/eobRedWall.png b/frontend/eobRedWall.png
new file mode 100644
index 0000000..4bcde32
Binary files /dev/null and b/frontend/eobRedWall.png differ
diff --git a/frontend/gnoll.png b/frontend/gnoll.png
index e40fabc..1b9c154 100644
Binary files a/frontend/gnoll.png and b/frontend/gnoll.png differ
diff --git a/node_modules/.vite/deps/_metadata.json b/node_modules/.vite/deps/_metadata.json
index 2d12353..2ae75ed 100644
--- a/node_modules/.vite/deps/_metadata.json
+++ b/node_modules/.vite/deps/_metadata.json
@@ -2,28 +2,46 @@
"hash": "5eac6a41",
"configHash": "86a557ed",
"lockfileHash": "3ceab950",
- "browserHash": "20105502",
+ "browserHash": "1d3df51c",
"optimized": {
"sprintf-js": {
"src": "../../sprintf-js/src/sprintf.js",
"file": "sprintf-js.js",
- "fileHash": "089e5f0c",
+ "fileHash": "cfe4c24f",
"needsInterop": true
},
"three": {
"src": "../../three/build/three.module.js",
"file": "three.js",
- "fileHash": "22510eaa",
+ "fileHash": "7e792144",
"needsInterop": false
},
"three/src/math/MathUtils.js": {
"src": "../../three/src/math/MathUtils.js",
"file": "three_src_math_MathUtils__js.js",
- "fileHash": "f611651c",
+ "fileHash": "6afcef1b",
+ "needsInterop": false
+ },
+ "three/tsl": {
+ "src": "../../three/build/three.tsl.js",
+ "file": "three_tsl.js",
+ "fileHash": "40fd901e",
+ "needsInterop": false
+ },
+ "three/webgpu": {
+ "src": "../../three/build/three.webgpu.js",
+ "file": "three_webgpu.js",
+ "fileHash": "0d6a1d7c",
"needsInterop": false
}
},
"chunks": {
+ "chunk-5FFPRNLG": {
+ "file": "chunk-5FFPRNLG.js"
+ },
+ "chunk-GHUIN7QU": {
+ "file": "chunk-GHUIN7QU.js"
+ },
"chunk-BUSYA2B4": {
"file": "chunk-BUSYA2B4.js"
}
diff --git a/test.js b/test.js
new file mode 100755
index 0000000..e69de29
diff --git a/utils/callParser.js b/utils/callParser.js
new file mode 100755
index 0000000..d0f5dcc
--- /dev/null
+++ b/utils/callParser.js
@@ -0,0 +1,97 @@
+export class FunctionCallParser {
+ /**
+ * @typedef {{name: string, args: Array}} CallType
+ */
+
+ /**
+ *
+ * @param {string} input
+ *
+ * @returns {CallType[]}
+ */
+ parse(input) {
+ const calls = [];
+ const pattern = /(\w+)\s*\(([^)]*)\)/g;
+ let match;
+
+ while ((match = pattern.exec(input)) !== null) {
+ const name = match[1];
+ const argsStr = match[2].trim();
+ const args = this.parseArguments(argsStr);
+
+ calls.push({ name, args });
+ }
+
+ return calls;
+ }
+
+ /** @protected */
+ parseArguments(argsStr) {
+ if (!argsStr) return [];
+
+ const args = [];
+ const tokens = this.tokenize(argsStr);
+
+ for (const token of tokens) {
+ args.push(this.parseValue(token));
+ }
+
+ return args;
+ }
+
+ /** @protected */
+ tokenize(argsStr) {
+ const tokens = [];
+ let current = "";
+ let depth = 0;
+
+ for (let i = 0; i < argsStr.length; i++) {
+ const char = argsStr[i];
+
+ if (char === "(" || char === "[" || char === "{") {
+ depth++;
+ current += char;
+ } else if (char === ")" || char === "]" || char === "}") {
+ depth--;
+ current += char;
+ } else if (char === "," && depth === 0) {
+ if (current.trim()) {
+ tokens.push(current.trim());
+ }
+ current = "";
+ } else {
+ current += char;
+ }
+ }
+
+ if (current.trim()) {
+ tokens.push(current.trim());
+ }
+
+ return tokens;
+ }
+
+ /** @protected */
+ parseValue(str) {
+ str = str.trim();
+
+ // Try to parse as number
+ if (/^-?\d+(\.\d+)?$/.test(str)) {
+ return parseFloat(str);
+ }
+
+ // Boolean
+ if (str === "true") return true;
+ if (str === "false") return false;
+
+ // Null/undefined
+ if (str === "null") return null;
+
+ // Otherwise treat as string (remove quotes if present)
+ if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
+ return str.slice(1, -1);
+ }
+
+ return str;
+ }
+}