diff --git a/frontend/ascii_first_person_renderer.js b/frontend/ascii_first_person_renderer.js index f474003..db5959a 100755 --- a/frontend/ascii_first_person_renderer.js +++ b/frontend/ascii_first_person_renderer.js @@ -176,16 +176,16 @@ export class FirstPersonRenderer { } if (tile.looksLikeWall) { - if (!this.map.looksLikeWall(x, y + 1)) { + if (!this.map.behavesLikeWall(x, y + 1)) { wallPlanes.push([x, y + 0.5, Math.PI * 0.0]); } - if (!this.map.looksLikeWall(x + 1, y)) { + if (!this.map.behavesLikeWall(x + 1, y)) { wallPlanes.push([x + 0.5, y, Math.PI * 0.5]); } - if (!this.map.looksLikeWall(x, y - 1)) { + if (!this.map.behavesLikeWall(x, y - 1)) { wallPlanes.push([x, y - 0.5, Math.PI * 1.0]); } - if (!this.map.looksLikeWall(x - 1, y)) { + if (!this.map.behavesLikeWall(x - 1, y)) { wallPlanes.push([x - 0.5, y, Math.PI * 1.5]); } return; diff --git a/frontend/ascii_tile_map.js b/frontend/ascii_tile_map.js index 82f7a6a..26710ee 100755 --- a/frontend/ascii_tile_map.js +++ b/frontend/ascii_tile_map.js @@ -132,7 +132,7 @@ export class TileMap { return this.tiles[y][x]; } - looksLikeWall(x, y) { + behavesLikeWall(x, y) { x |= 0; y |= 0; @@ -145,10 +145,10 @@ export class TileMap { return true; } - return this.tiles[y][x].looksLikeWall; + return this.tiles[y][x].isWallLike(); } - isTraversable(x, y) { + behavesLikeFloor(x, y) { x |= 0; y |= 0; @@ -156,7 +156,7 @@ export class TileMap { return true; } - return this.tiles[y][x].isTraversable; + return this.tiles[y][x].isFloorlike(); } /** @@ -220,11 +220,11 @@ export class TileMap { /** * @returns {number} */ - getTraversableTileCount() { + getFloorlikeTileCount() { let sum = 0; this.forEach((tile) => { - if (tile.isTraversable) { + if (tile.isFloorlike()) { sum++; } }); @@ -368,6 +368,6 @@ export class TileMap { } } -if (Math.PI < 0 && TileOptions ) { +if (Math.PI < 0 && TileOptions) { ("STFU Linda"); } diff --git a/frontend/ascii_tile_types.js b/frontend/ascii_tile_types.js index 87f52d4..6a26030 100755 --- a/frontend/ascii_tile_types.js +++ b/frontend/ascii_tile_types.js @@ -3,13 +3,15 @@ import shallowCopy from "../utils/shallowCopy.js"; import { TileOptions } from "../utils/tileOptionsParser.js"; import { Orientation, Vector2i } from "./ascii_types.js"; +/** @typedef {string} TileTypeId - a string with a length of 1 */ + /** * 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} */ +/** @type {Record} */ export const TileTypes = { [TileChars.FLOOR]: { minimapChar: "ยท", @@ -69,43 +71,70 @@ export const TileTypes = { export class Tile { /** @readonly {string?|number?} Unique (but optional) instance if of this tile */ id; - /** @type {string} Icon char of tile */ + + /** @type {TileTypeId} Char that defines this tile */ + typeId; + + /** @type {TileTypeId} Icon char of tile */ minimapChar; + /** @type {string} Color of the icon of tile */ minimapColor; + /** @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 {number|string} id of texture to use */ textureId; + /** @type {number|string} type of encounter located on this tile. May or may not be unique*/ encounterType; + /** @type {number|string} type of trap located on this tile. May or may not be unique*/ trapType; + /** @type {Orientation} */ orientation; - /** @type {TileType} This tile disguises itself as another tile, and its true properties are revealed later if event is triggered */ + + /** @type {TileTypeId} This tile disguises itself as another tile, and its true properties are revealed later if event is triggered */ disguiseAs; + + /** @type {TileTypeId} This tile "inherits" the properties of another tile type */ + is; + /** @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 */ + + /** @type {TileTypeId} 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; - /** @param {Tile} properties */ - constructor(properties) { + /** + * @param {TileTypeId} typeId + * @param {Tile?} properties + */ + constructor(typeId, properties) { mustBe(properties, "object"); + this.typeId = typeId; + // // Copy props from properties. // @@ -199,26 +228,50 @@ export class Tile { } } - static CreateWalLTile() { - return this.fromChar(); + /** @returns {Tile} */ + static createWall() { + return this.fromChar(TileChars.WALL); + } + + /** @returns {Tile} */ + static createEncounterStartPoint() { + return this.fromChar(TileChars.ENCOUNTER_START_POINT); + } + + /** @returns {Tile} */ + static createFloor() { + return this.fromChar(TileChars.FLOOR); + } + + /** @returns {Tile} */ + static createPlayerStart(orientation) { + return this.fromChar(TileChars.PLAYER_START_POINT, { orientation }); } /** - * @param {string} char - * @param {TileOptions} options Options - * + * Given a map symbol, + * @param {TileTypeId} typeId + * @param {TileOptions|Record} options * @returns {Tile} */ - 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"); + static fromChar(typeId, options) { + const typeInfo = TileTypes[typeId]; + + if (!typeInfo) { + console.log("unknown type id", { typeId }); + throw new Error(`Unknown typeId >>>${typeId}<<<`); } - const typeInfo = TileTypes[char]; + if (options === undefined) { + options = TileOptions.fromObject(typeId, TileTypes[typeId]); + } + + // + // Normalize options into a TileOptions object, + // + if (!(options instanceof TileOptions)) { + options = TileOptions.fromObject(options); + } let optionPos = 0; const creationArgs = {}; @@ -230,12 +283,40 @@ export class Tile { creationArgs[key] = fetchFromOption ? getOption(key) : shallowCopy(val); } - return new Tile(creationArgs); + return new Tile(typeId, creationArgs); } clone() { return new this.constructor(this); } + + isWallLike() { + if (this.is === TileChars.WALL) { + return true; + } + + if (this.disguiseAs === TileChars.WALL) { + return true; + } + + return this.looksLikeWall && !this.isTraversable; + } + + isFloorlike() { + if (this.is === TileChars.FLOOR) { + return true; + } + + if (this.disguiseAs === TileChars.FLOOR) { + return true; + } + + return this.isTraversable; + } + + isFloor() { + return this.typeId === TileChars.FLOOR; + } } if (Math.PI < 0 && TileOptions && Orientation && Vector2i) { diff --git a/frontend/dungeon_studio.js b/frontend/dungeon_studio.js index b006dd1..8826501 100755 --- a/frontend/dungeon_studio.js +++ b/frontend/dungeon_studio.js @@ -9,7 +9,7 @@ class DungeonGenerator { this.corridors = []; // 2d array of pure wall tiles - const tiles = new Array(height).fill().map(() => Array(width).fill(new WallTile())); + const tiles = new Array(height).fill().map(() => Array(width).fill(Tile.createWall())); this.map = new TileMap(tiles); } @@ -73,7 +73,7 @@ class DungeonGenerator { carveRoom(room) { for (let y = room.y; y < room.y + room.height; y++) { for (let x = room.x; x < room.x + room.width; x++) { - this.map.tiles[y][x] = new FloorTile(); + this.map.tiles[y][x] = Tile.createFloor(); } } } @@ -115,7 +115,7 @@ class DungeonGenerator { let lastNonWallX = undefined; // x-index of the LAST (eastmost) non-wall tile that we encountered on this row for (let x = 0; x < this.width; x++) { - const isWall = this.map.get(x, y) instanceof WallTile; + const isWall = this.map.get(x, y).looksLikeWall; if (isWall) { continue; @@ -152,24 +152,24 @@ class DungeonGenerator { const newTiles = []; // First row is all walls - newTiles.push(new Array(newWidth).fill(new WallTile())); + newTiles.push(new Array(newWidth).fill(Tile.createWall())); // Populate the new grid for (let y = dungeonStartY; y <= dungeonEndY; y++) { const row = []; - row.push(new WallTile()); // Initial wall tile on this row + row.push(Tile.createWall()); // Initial wall tile on this row for (let x = dungeonStartX; x <= dungeonEndX; x++) { /**/ const tile = this.map.get(x, y); row.push(tile); } - row.push(new WallTile()); // Final wall tile on this row + row.push(Tile.createWall()); // Final wall tile on this row newTiles.push(row); } // Final row is all walls - newTiles.push(new Array(newWidth).fill(new WallTile())); + newTiles.push(new Array(newWidth).fill(Tile.createWall())); this.map = new TileMap(newTiles); } @@ -201,7 +201,7 @@ class DungeonGenerator { while (x !== x2 || y !== y2) { if (x >= 0 && x < this.width && y >= 0 && y < this.height) { - this.map.tiles[y][x] = new FloorTile(); + this.map.tiles[y][x] = Tile.createFloor(); } if (x !== x2) x += dx; @@ -210,7 +210,7 @@ class DungeonGenerator { // Ensure endpoint is carved if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) { - this.map.tiles[y2][x2] = new FloorTile(); + this.map.tiles[y2][x2] = Tile.createFloor(); } } @@ -226,7 +226,7 @@ class DungeonGenerator { continue; } - if (this.map.isTraversable(x, y)) { + if (this.map.behavesLikeFloor(x, y)) { walkabilityCache.push([x, y]); } } @@ -244,7 +244,7 @@ class DungeonGenerator { for (let [x, y] of walkabilityCache) { // - const walkable = (offsetX, offsetY) => this.map.isTraversable(x + offsetX, y + offsetY); + const walkable = (offsetX, offsetY) => this.map.behavesLikeFloor(x + offsetX, y + offsetY); const surroundingFloorCount = 0 + @@ -264,7 +264,7 @@ class DungeonGenerator { if (surroundingFloorCount >= 7) { // MAGIC NUMBER 7 - this.map.tiles[y][x] = new WallTile(); + this.map.tiles[y][x] = Tile.createWall(); } } } @@ -281,7 +281,7 @@ class DungeonGenerator { continue; } - if (this.map.isTraversable(x, y)) { + if (this.map.behavesLikeFloor(x, y)) { walkabilityCache.push([x, y]); } } @@ -290,7 +290,7 @@ class DungeonGenerator { const idx = this.random(0, walkabilityCache.length - 1); const [x, y] = walkabilityCache[idx]; - const walkable = (offsetX, offsetY) => this.map.isTraversable(x + offsetX, y + offsetY); + const walkable = (offsetX, offsetY) => this.map.behavesLikeFloor(x + offsetX, y + offsetY); // // When spawning in, which direction should the player be oriented? @@ -301,14 +301,16 @@ class DungeonGenerator { if (walkable(-1, +0)) directions.push(Orientation.WEST); if (walkable(+0, -1)) directions.push(Orientation.SOUTH); + // Player's initial orientation is randomized in such a way that + // they don't face a wall upon spawning. const dirIdx = this.random(0, directions.length - 1); - this.map.tiles[y][x] = new PlayerStartTile(directions[dirIdx]); + this.map.tiles[y][x] = Tile.createPlayerStart(directions[dirIdx]); } // Add portals to isolated areas addPortals() { - let traversableTileCount = this.map.getTraversableTileCount(); + let traversableTileCount = this.map.getFloorlikeTileCount(); const result = this.map.getAllTraversableTilesConnectedTo(/** TODO PlayerPos */); @@ -322,29 +324,24 @@ class DungeonGenerator { // | || | | | | | | | | | // | || |_| | |_| | |_| | // |_| \___/|____/ \___/ - //------------------------------------- - // Connect isolated rooms via portals - //------------------------------------- + //---------------------------------------------- + // Connect isolated rooms via a chain of portals + //---------------------------------------------- // // LET Area0 = getAllTilesConnectedTo(playerStartTile) // LET Areas = Array containing one item so far: Area0 // FOR EACH tile in this.map - // IF tile not painted + // IF tile NOT in any Area // LET newArea = getAllTilesConnectedTo(tile) // PUSH newArea ONTO Areas // - // FOR EACH area IN Areas - // LET index = IndexOf(Areas, area) + // FOR EACH (index, area) IN Areas // LET next = index + 1 mod LENGTH(Areas) - // entryPos = findValidPortalEntryPositionInArea(area) - // exitPos = findValidPortalExitPositionInArea(area) + // entryPos = findValidPortalEntryPositionInArea(area) // entry is a pure wall tile that is exactly one adjacent floor tile - and that floor tile must be pure + // exitPos = findValidPortalExitPositionInArea(area) // must be a valid pure floor tile with one or more adjacent floor tiles, at least on of which are pure // - // this.map[entryPos.y, entryPos.x] = new PortalEntryTile(index) - // this.map[exitPos.y, exitPos.x] = new PortalExitTile(next) - // - // - // - // Start pointing it (another color) + // this.map[entryPos.y, entryPos.x] = new PortalEntryTile(index) // Create a portal in the current area + // this.map[exitPos.y, exitPos.x] = new PortalExitTile(next) // let the exit to the portal reside in the next area // console.warn( @@ -360,40 +357,43 @@ class DungeonGenerator { const floorTiles = []; for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { - if (this.map.get(x, y) instanceof FloorTile) { + if (this.map.get(x, y).isFloor()) { floorTiles.push({ x, y }); } } } - if (floorTiles.length === 0) return; - - // Add loot - const lootCount = Math.min(3, Math.floor(this.rooms.length / 2)); - for (let i = 0; i < lootCount; i++) { - const pos = floorTiles[this.random(0, floorTiles.length - 1)]; - if (this.map.tiles[pos.y][pos.x] instanceof FloorTile) { - this.map.tiles[pos.y][pos.x] = new LootTile(undefined, undefined); - } + if (floorTiles.length === 0) { + return; } + // Add loot + // const lootCount = Math.min(3, Math.floor(this.rooms.length / 2)); + // for (let i = 0; i < lootCount; i++) { + // const pos = floorTiles[this.random(0, floorTiles.length - 1)]; + // if (this.map.tiles[pos.y][pos.x].isFloor()) { + // this.map.tiles[pos.y][pos.x] = new LootTile(undefined, undefined); + // } + // } + // Add monsters - const monsterCount = Math.min(5, this.rooms.length); - for (let i = 0; i < monsterCount; i++) { + const encouterCount = Math.min(5, this.rooms.length); + for (let i = 0; i < encouterCount; i++) { const pos = floorTiles[this.random(0, floorTiles.length - 1)]; - if (this.map.tiles[pos.y][pos.x] instanceof FloorTile) { - this.map.tiles[pos.y][pos.x] = new EncounterTile(pos.x, pos.y, undefined, undefined); + if (this.map.tiles[pos.y][pos.x].isFloor()) { + this.map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint(); + // TODO: Add encounter to the dungeon's "roaming entities" array. } } // Add traps - const trapCount = Math.floor(floorTiles.length / 30); - for (let i = 0; i < trapCount; i++) { - const pos = floorTiles[this.random(0, floorTiles.length - 1)]; - if (this.map.tiles[pos.y][pos.x] instanceof FloorTile) { - this.map.tiles[pos.y][pos.x] = new TrapTile(); - } - } + // const trapCount = Math.floor(floorTiles.length / 30); + // for (let i = 0; i < trapCount; i++) { + // const pos = floorTiles[this.random(0, floorTiles.length - 1)]; + // if (this.map.tiles[pos.y][pos.x].isFloor()) { + // this.map.tiles[pos.y][pos.x] = new TrapTile(); + // } + // } } random(min, max) { diff --git a/frontend/gnoll.png b/frontend/gnoll.png old mode 100644 new mode 100755 diff --git a/frontend/skelebones.png b/frontend/skelebones.png old mode 100644 new mode 100755