From 67874891860758115b928dfa6830aa541237f7e3 Mon Sep 17 00:00:00 2001 From: Kim Ravn Hansen Date: Wed, 1 Oct 2025 20:50:06 +0200 Subject: [PATCH] Wang --- frontend/ascii_dungeon_crawler.html | 27 ++- frontend/ascii_dungeon_crawler.js | 15 +- frontend/ascii_first_person_renderer.js | 260 +++++++++++++++++------- frontend/ascii_tile_map.js | 143 ++++--------- frontend/ascii_tile_types.js | 147 ++++++++++++++ frontend/ascii_types.js | 5 +- frontend/eob2.png | Bin 11880 -> 0 bytes frontend/{eob1.png => eobBlueWall.png} | Bin frontend/eobRedWall.png | Bin 0 -> 10496 bytes frontend/gnoll.png | Bin 8355 -> 16149 bytes node_modules/.vite/deps/_metadata.json | 26 ++- test.js | 0 utils/callParser.js | 97 +++++++++ 13 files changed, 509 insertions(+), 211 deletions(-) create mode 100755 frontend/ascii_tile_types.js delete mode 100644 frontend/eob2.png rename frontend/{eob1.png => eobBlueWall.png} (100%) mode change 100644 => 100755 create mode 100644 frontend/eobRedWall.png create mode 100755 test.js create mode 100755 utils/callParser.js 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 4e1603138ad6ec687c73b83d60f5091b357fa25a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11880 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

Y~@YgfyyPS#_zOxyf|g9Y_-1w|;}`|8-? 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 literal 0 HcmV?d00001 diff --git a/frontend/gnoll.png b/frontend/gnoll.png index e40fabc439e10019112cf641d30fc47ae48f5519..1b9c154bda7b74358f9c0fe5ee4c9f7909d761c0 100644 GIT binary patch literal 16149 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 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; + } +}