diff --git a/frontend/ascii_tile_types.js b/frontend/ascii_tile_types.js index ffa7f3d..5c0b5ea 100755 --- a/frontend/ascii_tile_types.js +++ b/frontend/ascii_tile_types.js @@ -1,4 +1,4 @@ -import { mustBe, mustBeString } from "../utils/mustbe.js"; +import { mustBe } from "../utils/mustbe.js"; import shallowCopy from "../utils/shallowCopy.js"; import { TileOptions } from "../utils/tileOptionsParser.js"; import { Orientation } from "./ascii_types.js"; @@ -22,9 +22,64 @@ export const TileChars = Object.freeze({ PLAYER_START_POINT: "P", }); +/** + * Properties whose value matches one of these constants + * must have their actual value supplied by the creator + * of the Tile object. + * + * For instance, if a Tile has a textureId = PropertyPlaceholder.ID, then + * the creature of that Tile MUST supply the textureId before the tile can + * be used. Such values SHOULD be provided in the constructor, but CAN be + * provided later, as long as they are provided before the tile is used + * in the actual game. + * + */ + +/** Properties with this value must be valid ID values. */ const REQUIRED_ID = Symbol("REQUIRED_ID"); const REQUIRED_ORIENTATION = Symbol("REQUIRED_ORIENTATION"); +function mustBeId(value) { + if ((value | 0) === value) { + return value; + } + + if (typeof value !== "string") { + throw new Error("Value id not a valid id", { value }); + } + + value = value.trim(); + + if (value === "") { + throw new Error("Value id not a valid id", { value }); + } + + return value; +} +function mustBeOrientation(value) { + const result = Orientation.normalize(value); + + if (result === undefined) { + throw new Error("Value is not a valid orientation", { value }); + } + + return result; +} + +function mustBeSingleGrapheme(value) { + if (typeof value !== "string") { + throw new Error("Value is not a one-grapheme string", { value }); + } + + const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + + if ([...seg.segment(value)].length !== 1) { + throw new Error("Value is not a one-grapheme string", { value }); + } + + return value; +} + /** @type {Record} */ export const TileTypes = { [TileChars.FLOOR]: { @@ -138,26 +193,15 @@ export class Tile { // for (const [key, val] of Object.entries(properties)) { // - // Ensure that we do not have placeholder symbols in the incoming properties - // Placeolder symbols indicate that the data must be supplied externally by - // the creator of the tile - // - if (typeof val === "symbol" && val.description.startsWith("REQUIRED_")) { - console.error( - [ - "REQUIRED_ symbol encountered in Tile constructor. ", - "REQUIRED_ is a placeholder, and cannot be used as a value directly", - ].join("\n"), - { key, val, properties }, - ); - throw new Error("Incomplete data in constructor. Args may not contain a data placeholder"); + // Skip empty properties. + if (val === undefined) { + continue; } if (!Object.hasOwn(this, key) /* Object.prototype.hasOwnProperty.call(this, key) */) { console.warn("Unknown tile property", { key, val, properties }); + continue; } - - this[key] = val; } // @@ -169,7 +213,7 @@ export class Tile { // bump event, spell, or other method for discovering secrets // if (this.disguiseAs !== undefined) { - this.revealed = false; + this.revealed ??= false; const other = shallowCopy(TileTypes[this.disguiseAs]); for (const [pKey, pVal] of Object.entries(other)) { @@ -198,43 +242,22 @@ export class Tile { } // - // Normalize Orientation. - if (this.orientation !== undefined && typeof this.orientation === "string") { - const valueMap = { - north: Orientation.NORTH, - south: Orientation.SOUTH, - east: Orientation.EAST, - west: Orientation.WEST, - }; - this.orientation = mustBeString(valueMap[this.orientation.toLowerCase()]); - } + // Populate derivable values that have not yet been set. + this.minimapChar ??= this.typeId; + this.revealedMinimapChar ??= this.minimapChar; + this.revealedMinimapColo ??= this.minimapColor; // - // Tiles are not required to have IDs, but IDs must be numbers or strings - if (this.id !== undefined) { - mustBe(this.id, "number", "string"); - } - + // Sanitize and normalize // - // If a tile has a texture, the texture id must be string or number - if (this.textureId !== undefined) { - mustBe(this.textureId, "number", "string"); - } + this.id ??= mustBeId(this.id); + this.textureId ??= mustBeId(this.textureId); + this.portalTargetId ??= mustBeId(this.portalTargetId); + this.orientation ??= mustBeOrientation(this.orientation); - // - // If a tile is a portal with a portal target, that target id must be a number or string. - if (this.portalTargetId !== undefined) { - mustBe(this.portalTargetId, "number", "string"); - } - - if (this.minimapChar === undefined) { - console.debug("minimap = typeid", { ...this }); - this.minimapChar = this.typeId; - } - if (this.revealedMinimapChar === undefined) { - console.debug("reveaked = minimap", { ...this }); - this.revealedMinimapChar = this.minimapChar; - } + mustBeSingleGrapheme(this.typeId); + mustBeSingleGrapheme(this.minimapChar); + mustBeSingleGrapheme(this.revealedMinimapChar); } /** @returns {Tile} */ diff --git a/frontend/ascii_types.js b/frontend/ascii_types.js index 784bb5f..4406e18 100755 --- a/frontend/ascii_types.js +++ b/frontend/ascii_types.js @@ -16,7 +16,7 @@ export class Orientation { /** * @param {string} str - * @returns {Orientation} + * @returns {Orientation|undefined} */ static fromString(str) { if (typeof str !== "string") { @@ -35,14 +35,16 @@ export class Orientation { /** * @param {string|number} val - * @returns {Orientation} + * @returns {Orientation|undefined} */ static normalize(val) { if (typeof val === "string") { return Orientation.fromString(val); } - return val % 4; + if ((val | 0) === val) { + return (val | 0) % 4; + } } } diff --git a/frontend/dungeon_studio.js b/frontend/dungeon_studio.js index 38755f7..873601a 100755 --- a/frontend/dungeon_studio.js +++ b/frontend/dungeon_studio.js @@ -29,8 +29,8 @@ class DungeonGenerator { this.addPillarsToBigRooms(); this.addFeatures(); this.addPlayerStart(); - // this.addPortals(); - return this.map.toString(CharType.MINIMAP_REVEALED); + this.addPortals(); + return this.map.toString(CharType.TYPE_ID); } generateRooms() {