This commit is contained in:
Kim Ravn Hansen
2025-10-18 11:44:51 +02:00
parent 0265f78b6d
commit a9bb48babb
3 changed files with 107 additions and 99 deletions

View File

@@ -140,7 +140,7 @@ export class TileMap {
return this.tiles[y][x]; return this.tiles[y][x];
} }
behavesLikeWall(x, y) { isWallLike(x, y) {
x |= 0; x |= 0;
y |= 0; y |= 0;
@@ -161,7 +161,7 @@ export class TileMap {
* @param {number} y * @param {number} y
* @returns {boolean} * @returns {boolean}
*/ */
behavesLikeFloor(x, y) { isFloorLike(x, y) {
x |= 0; x |= 0;
y |= 0; y |= 0;

View File

@@ -4,12 +4,14 @@ import { TileOptions } from "../utils/tileOptionsParser.js";
import { Orientation } from "./ascii_types.js"; import { Orientation } from "./ascii_types.js";
/** @typedef {string} TileTypeId - a string with a length of 1 */ /** @typedef {string} TileTypeId - a string with a length of 1 */
/** @typedef {string} MinimapGrapheme - a string containing a single grapheme */
/** @typedef {string|number} ResourceId - id of a resource (such as a texture or another tile) used by a tile */
/** /**
* Array of __internal__ characters used to identify tile types. * Array of __internal__ characters used to identify tile types.
* These are __not__ necessarily the characters used to display * These are __not necessarily__ the characters used to display
* the tile on the minimap - but they are used when serializing * the tile on the minimap - but they are used when serializing
* the maps into a semi-human-readable text-format. * the maps into a human-readable text-format.
* *
* @enum {TileTypeId} * @enum {TileTypeId}
*/ */
@@ -27,36 +29,35 @@ export const TileChars = Object.freeze({
* must have their actual value supplied by the creator * must have their actual value supplied by the creator
* of the Tile object. * of the Tile object.
* *
* For instance, if a Tile has a textureId = PropertyPlaceholder.ID, then * For instance, if a Tile has a textureId = REQUIRED_ID, then
* the creature of that Tile MUST supply the textureId before the tile can * the creator of that Tile MUST supply the textureId before the tile can
* be used. Such values SHOULD be provided in the constructor, but CAN be * be used in the game Such values SHOULD be provided in the constructor,
* provided later, as long as they are provided before the tile is used * but CAN be provided later, as long as they are provided before the
* in the actual game. * tile is used in the actual game.
*
*/ */
/** Properties with this value must be valid ID values. */ /** Properties with this value must be valid ID values. */
const REQUIRED_ID = Symbol("REQUIRED_ID"); const REQUIRED_ID = Symbol("REQUIRED_ID");
const REQUIRED_ORIENTATION = Symbol("REQUIRED_ORIENTATION"); const REQUIRED_ORIENTATION = Symbol("REQUIRED_ORIENTATION");
function mustBeId(value) { function sanitizeId(value) {
if ((value | 0) === value) { if ((value | 0) === value) {
return value; return value;
} }
if (typeof value !== "string") { if (typeof value !== "string") {
throw new Error("Value id not a valid id", { value }); throw new Error("Value is not a valid id", { value });
} }
value = value.trim(); value = value.trim();
if (value === "") { if (value === "") {
throw new Error("Value id not a valid id", { value }); throw new Error("Value is not a valid id", { value });
} }
return value; return value;
} }
function mustBeOrientation(value) { function sanitizeOrientation(value) {
const result = Orientation.normalize(value); const result = Orientation.normalize(value);
if (result === undefined) { if (result === undefined) {
@@ -122,13 +123,13 @@ export const TileTypes = {
}; };
export class Tile { export class Tile {
/** @readonly {string?|number?} Unique (but optional) instance if of this tile */ /** @readonly {ResourceId?} Unique (but optional) instance id of this tile - only needed when this tile is referenced by other tiles */
id; id;
/** @type {TileTypeId} Char that defines this tile */ /** @type {TileTypeId} Char that defines this tile */
typeId; typeId;
/** @type {TileTypeId} Icon char of tile */ /** @type {MinimapGrapheme} Icon char of tile */
minimapChar; minimapChar;
/** @type {string} Color of the icon of tile */ /** @type {string} Color of the icon of tile */
@@ -146,18 +147,15 @@ export class Tile {
/** @type {boolean} Is this a portal exit and/or entry */ /** @type {boolean} Is this a portal exit and/or entry */
isPortal; isPortal;
/** @type {string|number} Where is the player transported if they enter the portal */ /** @type {ResourceId} id of the type where players will be transported - many portals may share same target */
portalTargetId; portalTargetId;
/** @type {number|string} id of texture to use */ /** @type {ResourceId} id of texture to use - does not need to be unique */
textureId; textureId;
/** @type {number|string} type of encounter located on this tile. May or may not be unique*/ /** @type {ResourceId} id of encounter located on this tile. May or may not be unique */
encounterId; encounterId;
/** @type {number|string} type of trap located on this tile. May or may not be unique*/
trapType;
/** @type {Orientation} */ /** @type {Orientation} */
orientation; orientation;
@@ -170,15 +168,71 @@ export class Tile {
/** @type {boolean} Has the secret properties of this tile been revealed? */ /** @type {boolean} Has the secret properties of this tile been revealed? */
revealed; revealed;
/** @type {TileTypeId} Icon char of tile after tile's secrets have been revealed */ /** @type {MinimapGrapheme} Icon char of tile after tile's secrets have been revealed */
revealedMinimapChar; revealedMinimapChar;
/** @type {string} Color of the icon char of tile after tile's secrets have been revealed */ /** @type {string} Color of the icon char of tile after tile's secrets have been revealed */
revealedMinimapColor; revealedMinimapColor;
/** @type {number|string} id of texture to use after the secrets of this tile has been revealed */ /** @type {ResourceId} id of texture to use after the secrets of this tile has been revealed */
revealedTextureId; revealedTextureId;
/** @returns {Tile} */
static createWall() {
return this.fromChar(TileChars.WALL);
}
/** @returns {Tile} */
static createEncounterStartPoint(encounterId) {
return this.fromChar(TileChars.ENCOUNTER_START_POINT, { encounterId });
}
/** @returns {Tile} */
static createFloor() {
return this.fromChar(TileChars.FLOOR);
}
/** @returns {Tile} */
static createPlayerStart(orientation) {
return this.fromChar(TileChars.PLAYER_START_POINT, { orientation });
}
/**
* Given a map symbol,
* @param {TileTypeId} typeId
* @param {TileOptions|Record<string,any>} options
* @returns {Tile}
*/
static fromChar(typeId, options) {
const prototype = TileTypes[typeId];
if (!prototype) {
console.log("unknown type id", { typeId });
throw new Error(`Unknown typeId >>>${typeId}<<<`);
}
//
// Normalize options into a TileOptions object,
//
if (!(options instanceof TileOptions)) {
options = TileOptions.fromObject(typeId, options ?? {});
}
let optionPos = 0;
const properties = {};
for (let [key, val] of Object.entries(prototype)) {
//
if (typeof val === "symbol" && val.description.startsWith("REQUIRED_")) {
properties[key] = options.getValue(name, optionPos++);
} else {
properties[key] = shallowCopy(val);
}
}
return new Tile(typeId, properties);
}
/** /**
* @param {TileTypeId} typeId * @param {TileTypeId} typeId
* @param {Tile?} properties * @param {Tile?} properties
@@ -250,80 +304,33 @@ export class Tile {
// //
// Sanitize and normalize // Sanitize and normalize
// //
this.id ??= mustBeId(this.id); if (this.id !== undefined) {
this.textureId ??= mustBeId(this.textureId); this.id = sanitizeId(this.id);
this.portalTargetId ??= mustBeId(this.portalTargetId); }
this.orientation ??= mustBeOrientation(this.orientation); if (this.textureId !== undefined) {
this.textureId = sanitizeId(this.textureId);
}
if (this.portalId !== undefined) {
this.portalTargetId = sanitizeId(this.portalTargetId);
}
if (this.orientation !== undefined) {
this.orientation = sanitizeOrientation(this.orientation);
}
mustBeSingleGrapheme(this.typeId); mustBeSingleGrapheme(this.typeId);
mustBeSingleGrapheme(this.minimapChar); mustBeSingleGrapheme(this.minimapChar);
mustBeSingleGrapheme(this.revealedMinimapChar); mustBeSingleGrapheme(this.revealedMinimapChar);
} }
/** @returns {Tile} */
static createWall() {
return this.fromChar(TileChars.WALL);
}
/** @returns {Tile} */
static createEncounterStartPoint(encounterId) {
return this.fromChar(TileChars.ENCOUNTER_START_POINT, { encounterId });
}
/** @returns {Tile} */
static createFloor() {
return this.fromChar(TileChars.FLOOR);
}
/** @returns {Tile} */
static createPlayerStart(orientation) {
return this.fromChar(TileChars.PLAYER_START_POINT, { orientation });
}
/**
* Given a map symbol,
* @param {TileTypeId} typeId
* @param {TileOptions|Record<string,string>} options
* @returns {Tile}
*/
static fromChar(typeId, options) {
const prototype = TileTypes[typeId];
if (!prototype) {
console.log("unknown type id", { typeId });
throw new Error(`Unknown typeId >>>${typeId}<<<`);
}
if (options === undefined) {
options = TileOptions.fromObject(typeId, TileTypes[typeId]);
}
//
// Normalize options into a TileOptions object,
//
if (!(options instanceof TileOptions)) {
options = TileOptions.fromObject(typeId, options);
}
let optionPos = 0;
const properties = {};
const getOption = (name) => options.getValue(name, optionPos++);
for (let [key, val] of Object.entries(prototype)) {
properties[key] = val;
//
const fetchOption = typeof val === "symbol" && val.description.startsWith("REQUIRED_");
properties[key] = fetchOption ? getOption(key) : shallowCopy(val);
}
return new Tile(typeId, properties);
}
clone() { clone() {
return new Tile(this.typeId, { ...this }); return new Tile(this.typeId, { ...this });
} }
isWallLike() { isWallLike() {
if (this.typeId === TileChars.WALL) {
return true;
}
if (this.is === TileChars.WALL) { if (this.is === TileChars.WALL) {
return true; return true;
} }
@@ -335,6 +342,10 @@ export class Tile {
return this.looksLikeWall && !this.isTraversable; return this.looksLikeWall && !this.isTraversable;
} }
isWall() {
return this.typeId === TileChars.WALL;
}
isFloorlike() { isFloorlike() {
if (this.typeId === TileChars.FLOOR) { if (this.typeId === TileChars.FLOOR) {
return true; return true;

View File

@@ -115,20 +115,17 @@ class DungeonGenerator {
let lastNonWallX = undefined; // x-index of the LAST (eastmost) non-wall tile that we encountered on this row 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++) { for (let x = 0; x < this.width; x++) {
const isWall = this.map.get(x, y).looksLikeWall; //
if (this.map.get(x, y).isWall()) {
if (isWall) {
continue; continue;
} }
if (firstNonWallX === undefined) { firstNonWallX ??= x;
firstNonWallX = x;
}
lastNonWallX = x; lastNonWallX = x;
} }
const onlyWalls = lastNonWallX === undefined; // Did this row contain only walls?
if (onlyWalls) { if (firstNonWallX === undefined) {
continue; continue;
} }
@@ -226,7 +223,7 @@ class DungeonGenerator {
continue; continue;
} }
if (this.map.behavesLikeFloor(x, y)) { if (this.map.get(x, y).isFloor()) {
walkabilityCache.push([x, y]); walkabilityCache.push([x, y]);
} }
} }
@@ -244,7 +241,7 @@ class DungeonGenerator {
for (let [x, y] of walkabilityCache) { for (let [x, y] of walkabilityCache) {
// //
const walkable = (offsetX, offsetY) => this.map.behavesLikeFloor(x + offsetX, y + offsetY); const walkable = (offsetX, offsetY) => this.map.isFloorLike(x + offsetX, y + offsetY);
const surroundingFloorCount = const surroundingFloorCount =
0 + 0 +
@@ -281,7 +278,7 @@ class DungeonGenerator {
continue; continue;
} }
if (this.map.behavesLikeFloor(x, y)) { if (this.map.isFloorLike(x, y)) {
walkabilityCache.push([x, y]); walkabilityCache.push([x, y]);
} }
} }
@@ -290,7 +287,7 @@ class DungeonGenerator {
const idx = this.random(0, walkabilityCache.length - 1); const idx = this.random(0, walkabilityCache.length - 1);
const [x, y] = walkabilityCache[idx]; const [x, y] = walkabilityCache[idx];
const walkable = (offsetX, offsetY) => this.map.behavesLikeFloor(x + offsetX, y + offsetY); const walkable = (offsetX, offsetY) => this.map.isFloorLike(x + offsetX, y + offsetY);
// //
// When spawning in, which direction should the player be oriented? // When spawning in, which direction should the player be oriented?