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; + } +}