This commit is contained in:
Kim Ravn Hansen
2025-11-04 08:57:59 +01:00
parent 4c2b2dcdfe
commit 87f8add864
9 changed files with 78 additions and 59 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
resources
# Logs # Logs
logs logs
*.log *.log

View File

@@ -13,35 +13,47 @@ import { Orientation } from "./ascii_types";
/** Dungeon Generator - generates TileMaps populated with rooms, traps, encounters, etc. */ /** Dungeon Generator - generates TileMaps populated with rooms, traps, encounters, etc. */
class DungeonFactory { class DungeonFactory {
/** @type {number} */ /** @type {number} */
roomCount; #roomCount;
/** @type {RoomConfig[]} */ /** @type {RoomConfig[]} */
rooms; #rooms;
/** @type {TileMap} */ /** @type {TileMap} */
map; #map;
get roomCount() {
return this.#roomCount;
}
get rooms() {
return this.#rooms;
}
get map() {
return this.#map;
}
get width() { get width() {
return this.map.width; return this.#map.width;
} }
get height() { get height() {
return this.map.height; return this.#map.height;
} }
/** /**
* @param {number} width * @param {number} width
* @param {number} height * @param {number} height
* @param {number} roomCount * @param {number} #roomCount
*/ */
constructor(width, height, roomCount) { constructor(width, height, roomCount) {
this.roomCount = roomCount | 0; this.#roomCount = roomCount | 0;
this.rooms = []; this.#rooms = [];
// 2d array of pure wall tiles // 2d array of pure wall tiles
const tiles = new Array(height | 0).fill().map(() => Array(width | 0).fill(Tile.createWall())); const tiles = new Array(height | 0).fill().map(() => Array(width | 0).fill(Tile.createWall()));
this.map = new TileMap(tiles); this.#map = new TileMap(tiles);
} }
generate() { generate() {
@@ -52,18 +64,18 @@ class DungeonFactory {
this.addFeatures(); this.addFeatures();
this.addPlayerStart(); this.addPlayerStart();
this.addPortals(); this.addPortals();
return this.map.toString(CharType.TYPE_ID); return this.#map.toString(CharType.TYPE_ID);
} }
generateRooms() { generateRooms() {
this.rooms = []; this.#rooms = [];
const maxAttempts = this.roomCount * 10; const maxAttempts = this.#roomCount * 10;
let attempts = 0; let attempts = 0;
while (this.rooms.length < this.roomCount && attempts < maxAttempts) { while (this.#rooms.length < this.#roomCount && attempts < maxAttempts) {
const room = this.generateRoom(); const room = this.generateRoom();
if (room && !this.roomOverlaps(room)) { if (room && !this.roomOverlaps(room)) {
this.rooms.push(room); this.#rooms.push(room);
this.carveRoom(room); this.carveRoom(room);
} }
attempts++; attempts++;
@@ -83,7 +95,7 @@ class DungeonFactory {
} }
roomOverlaps(newRoom) { roomOverlaps(newRoom) {
return this.rooms.some( return this.#rooms.some(
(room) => (room) =>
newRoom.x < room.x + room.width + 2 && newRoom.x < room.x + room.width + 2 &&
newRoom.x + newRoom.width + 2 > room.x && newRoom.x + newRoom.width + 2 > room.x &&
@@ -95,26 +107,26 @@ class DungeonFactory {
carveRoom(room) { carveRoom(room) {
for (let y = room.y; y < room.y + room.height; y++) { for (let y = room.y; y < room.y + room.height; y++) {
for (let x = room.x; x < room.x + room.width; x++) { for (let x = room.x; x < room.x + room.width; x++) {
this.map.tiles[y][x] = Tile.createFloor(); this.#map.tiles[y][x] = Tile.createFloor();
} }
} }
} }
connectRooms() { connectRooms() {
if (this.rooms.length < 2) return; if (this.#rooms.length < 2) return;
// Connect each room to at least one other room // Connect each room to at least one other room
for (let i = 1; i < this.rooms.length >> 1; i++) { for (let i = 1; i < this.#rooms.length >> 1; i++) {
const roomA = this.rooms[i - 1]; const roomA = this.#rooms[i - 1];
const roomB = this.rooms[i]; const roomB = this.#rooms[i];
this.createCorridor(roomA, roomB); this.createCorridor(roomA, roomB);
} }
// Add some extra connections for more interesting layouts // Add some extra connections for more interesting layouts
const extraConnections = Math.floor(this.rooms.length / 3); const extraConnections = Math.floor(this.#rooms.length / 3);
for (let i = 0; i < extraConnections; i++) { for (let i = 0; i < extraConnections; i++) {
const roomA = this.rooms[this.random(0, this.rooms.length - 1)]; const roomA = this.#rooms[this.random(0, this.#rooms.length - 1)];
const roomB = this.rooms[this.random(0, this.rooms.length - 1)]; const roomB = this.#rooms[this.random(0, this.#rooms.length - 1)];
if (roomA !== roomB) { if (roomA !== roomB) {
this.createCorridor(roomA, roomB); this.createCorridor(roomA, roomB);
} }
@@ -138,7 +150,7 @@ class DungeonFactory {
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
// //
if (this.map.get(x, y).isWall()) { if (this.#map.get(x, y).isWall()) {
continue; continue;
} }
@@ -180,7 +192,7 @@ class DungeonFactory {
row.push(Tile.createWall()); // Initial wall tile on this row row.push(Tile.createWall()); // Initial wall tile on this row
for (let x = dungeonStartX; x <= dungeonEndX; x++) { for (let x = dungeonStartX; x <= dungeonEndX; x++) {
/**/ /**/
const tile = this.map.get(x, y); const tile = this.#map.get(x, y);
row.push(tile); row.push(tile);
} }
row.push(Tile.createWall()); // Final wall tile on this row row.push(Tile.createWall()); // Final wall tile on this row
@@ -190,7 +202,7 @@ class DungeonFactory {
// Final row is all walls // Final row is all walls
newTiles.push(new Array(newWidth).fill(Tile.createWall())); newTiles.push(new Array(newWidth).fill(Tile.createWall()));
this.map = new TileMap(newTiles); this.#map = new TileMap(newTiles);
} }
createCorridor(roomA, roomB) { createCorridor(roomA, roomB) {
@@ -220,7 +232,7 @@ class DungeonFactory {
while (x !== x2 || y !== y2) { while (x !== x2 || y !== y2) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) { if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this.map.tiles[y][x] = Tile.createFloor(); this.#map.tiles[y][x] = Tile.createFloor();
} }
if (x !== x2) x += dx; if (x !== x2) x += dx;
@@ -229,7 +241,7 @@ class DungeonFactory {
// Ensure endpoint is carved // Ensure endpoint is carved
if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) { if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) {
this.map.tiles[y2][x2] = Tile.createFloor(); this.#map.tiles[y2][x2] = Tile.createFloor();
} }
} }
@@ -238,14 +250,14 @@ class DungeonFactory {
for (let y = 1; y < this.height - 1; y++) { for (let y = 1; y < this.height - 1; y++) {
// //
for (let x = 1; x < this.width - 1; x++) { for (let x = 1; x < this.width - 1; x++) {
const cell = this.map.get(x, y); const cell = this.#map.get(x, y);
if (!cell) { if (!cell) {
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell); console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
continue; continue;
} }
if (this.map.get(x, y).isFloor()) { if (this.#map.get(x, y).isFloor()) {
walkabilityCache.push([x, y]); walkabilityCache.push([x, y]);
} }
} }
@@ -263,7 +275,7 @@ class DungeonFactory {
for (let [x, y] of walkabilityCache) { for (let [x, y] of walkabilityCache) {
// //
const walkable = (offsetX, offsetY) => this.map.isFloorLike(x + offsetX, y + offsetY); const walkable = (offsetX, offsetY) => this.#map.isFloorLike(x + offsetX, y + offsetY);
const surroundingFloorCount = const surroundingFloorCount =
0 + 0 +
@@ -283,7 +295,7 @@ class DungeonFactory {
if (surroundingFloorCount >= 7) { if (surroundingFloorCount >= 7) {
// MAGIC NUMBER 7 // MAGIC NUMBER 7
this.map.tiles[y][x] = Tile.createWall(); this.#map.tiles[y][x] = Tile.createWall();
} }
} }
} }
@@ -293,14 +305,14 @@ class DungeonFactory {
for (let y = 1; y < this.height - 1; y++) { for (let y = 1; y < this.height - 1; y++) {
// //
for (let x = 1; x < this.width - 1; x++) { for (let x = 1; x < this.width - 1; x++) {
const cell = this.map.get(x, y); const cell = this.#map.get(x, y);
if (!cell) { if (!cell) {
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell); console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
continue; continue;
} }
if (this.map.isFloorLike(x, y)) { if (this.#map.isFloorLike(x, y)) {
walkabilityCache.push([x, y]); walkabilityCache.push([x, y]);
} }
} }
@@ -309,7 +321,7 @@ class DungeonFactory {
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.isFloorLike(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?
@@ -324,23 +336,23 @@ class DungeonFactory {
// they don't face a wall upon spawning. // they don't face a wall upon spawning.
const dirIdx = this.random(0, directions.length - 1); const dirIdx = this.random(0, directions.length - 1);
this.map.tiles[y][x] = Tile.createPlayerStart(directions[dirIdx]); this.#map.tiles[y][x] = Tile.createPlayerStart(directions[dirIdx]);
} }
// Add portals to isolated areas // Add portals to isolated areas
addPortals() { addPortals() {
let traversableTileCount = this.map.getFloorlikeTileCount(); let traversableTileCount = this.#map.getFloorlikeTileCount();
// //
// Find the player's start point, and let this be the // Find the player's start point, and let this be the
// bases of area 0 // bases of area 0
const [x, y] = this.map.forEach((tile, x, y) => { const [x, y] = this.#map.forEach((tile, x, y) => {
if (tile.typeId === TileChars.PLAYER_START_POINT) { if (tile.typeId === TileChars.PLAYER_START_POINT) {
return [x, y]; return [x, y];
} }
}); });
const result = this.map.getAllTraversableTilesConnectedTo(x, y); const result = this.#map.getAllTraversableTilesConnectedTo(x, y);
if (result.size === traversableTileCount) { if (result.size === traversableTileCount) {
// There are no isolated areas, return // There are no isolated areas, return
@@ -385,7 +397,7 @@ class DungeonFactory {
const floorTiles = []; const floorTiles = [];
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
if (this.map.get(x, y).isFloor()) { if (this.#map.get(x, y).isFloor()) {
floorTiles.push({ x, y }); floorTiles.push({ x, y });
} }
} }
@@ -405,11 +417,11 @@ class DungeonFactory {
// } // }
// Add monsters // Add monsters
const encouterCount = Math.min(5, this.rooms.length); const encouterCount = Math.min(5, this.#rooms.length);
for (let i = 0; i < encouterCount; i++) { for (let i = 0; i < encouterCount; i++) {
const pos = floorTiles[this.random(0, floorTiles.length - 1)]; const pos = floorTiles[this.random(0, floorTiles.length - 1)];
if (this.map.tiles[pos.y][pos.x].isFloor()) { if (this.#map.tiles[pos.y][pos.x].isFloor()) {
this.map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint("PLACEHOLDER_ENCOUNTER_ID"); this.#map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint("PLACEHOLDER_ENCOUNTER_ID");
// TODO: Add encounter to the dungeon's "roaming entities" array. // TODO: Add encounter to the dungeon's "roaming entities" array.
} }
} }
@@ -468,15 +480,15 @@ export const downloadDungeon = () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
widthEl.addEventListener("input", function () { widthEl.addEventListener("input", function() {
document.getElementById("widthValue").textContent = this.value; document.getElementById("widthValue").textContent = this.value;
}); });
heightEl.addEventListener("input", function () { heightEl.addEventListener("input", function() {
document.getElementById("heightValue").textContent = this.value; document.getElementById("heightValue").textContent = this.value;
}); });
roomCountEl.addEventListener("input", function () { roomCountEl.addEventListener("input", function() {
document.getElementById("roomCountValue").textContent = this.value; document.getElementById("roomCountValue").textContent = this.value;
}); });

View File

@@ -1,7 +1,8 @@
import WebSocket from "ws";
import { Character } from "./character.js";
import { Config } from "./../config.js"; import { Config } from "./../config.js";
import { Scene } from "../scenes/scene.js";
/** @typedef {import("../scenes/scene.js").Scene} Scene */
/** @typedef {import("./characer.js").Character} Character */
/** @typedef {import("ws").Websocket} Websocket */
/** /**
* Player Account. * Player Account.

View File

@@ -42,6 +42,7 @@ export class Session {
* @param {Scene} scene * @param {Scene} scene
*/ */
setScene(scene) { setScene(scene) {
this.frankofil = stil;
console.debug("Changing scene", { scene: scene.constructor.name }); console.debug("Changing scene", { scene: scene.constructor.name });
if (!(scene instanceof Scene)) { if (!(scene instanceof Scene)) {
throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`); throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`);
@@ -99,10 +100,10 @@ export class Session {
* @param {string|string[]} text The prompt message (the request to get the user to enter some info). * @param {string|string[]} text The prompt message (the request to get the user to enter some info).
* @param {string?} context * @param {string?} context
*/ /** */ /**
* @overload * @overload
* @param {string|string[]} text The prompt message (the request to get the user to enter some info). * @param {string|string[]} text The prompt message (the request to get the user to enter some info).
* @param {object?} options Any options for the text (client side text formatting, color-, font-, or style info, etc.). * @param {object?} options Any options for the text (client side text formatting, color-, font-, or style info, etc.).
*/ */
sendPrompt(text, options) { sendPrompt(text, options) {
options = options || {}; options = options || {};

View File

@@ -256,8 +256,10 @@ export class CharacterSeeder {
// //
// Stats // Stats
c.maxHitPoints = c.currentHitPoints = 15; c.maxHitPoints = c.currentHitPoints = 15;
c.meleeCombat = Math.max(c.meleeCombat, 10); c.meleeCombat = Math.max(c.meleeCombat, 10);
c.magic = Math.min(c.magic, 10); c.awareness = Math.max(c.awareness, 10)
c.skulduggery = Math.min(c.skulduggery, 10);
// //
// Skills // Skills

View File

@@ -9,6 +9,7 @@ import { gGame } from "./models/globals.js";
import { AuthenticationScene } from "./scenes/authentication/authenticationScene.js"; import { AuthenticationScene } from "./scenes/authentication/authenticationScene.js";
import { MessageType, WebsocketMessage, formatMessage } from "./utils/messages.js"; import { MessageType, WebsocketMessage, formatMessage } from "./utils/messages.js";
// __ __ _ _ ____ ____ // __ __ _ _ ____ ____
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __ // | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
// | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__| // | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__|

View File

@@ -1,10 +1,10 @@
import * as regex from "./regex.js"; import * as regex from "./regex.js";
const MINI_UID_REGEX = regex.pretty( const MINI_UID_REGEX = regex.compileMultilineRegex(
"\.uid\.", // Mini-uids always begin with ".uid." "\.uid\.", // Mini-uids always begin with ".uid."
"[a-z0-9]{6,}$", // Terminated by 6 or more random numbers and lowercase letters. "[a-z0-9]{6,}$", // Terminated by 6 or more random numbers and lowercase letters.
); );
const ID_SANITY_REGEX = regex.pretty( const ID_SANITY_REGEX = regex.compileMultilineRegex(
"^:", // All ids start with a colon "^:", // All ids start with a colon
"([a-z0-9]+\.)*?", // Middle -optional- part :myid.gogle.thing.thang.thong "([a-z0-9]+\.)*?", // Middle -optional- part :myid.gogle.thing.thang.thong
"[a-z0-9_]+$", // The terminating part of the id is numbers, lowercase letters, and -notably- underscores. "[a-z0-9_]+$", // The terminating part of the id is numbers, lowercase letters, and -notably- underscores.
@@ -33,7 +33,7 @@ export function isIdSane(id) {
} }
/** /**
* @returns {string} crypto-unsafe pseudo random numbe"r. * @returns {string} crypto-unsafe pseudo random number.
* *
* Generate a random number, convert it to base36, and return it as a string with 7-8 characters. * Generate a random number, convert it to base36, and return it as a string with 7-8 characters.
*/ */

View File

@@ -4,7 +4,7 @@
* @param {...string} args * @param {...string} args
* @returns {Regexp} * @returns {Regexp}
*/ */
export function pretty(...args) { export function compileMultilineRegex(...args) {
const regexprStr = args.join(""); const regexprStr = args.join("");
return new RegExp(regexprStr); return new RegExp(regexprStr);
} }

View File

@@ -97,7 +97,7 @@ export class FramingOptions {
frameChars = FrameType.values.Double; frameChars = FrameType.values.Double;
/** /**
* @param {object} o * @param {FramingOptions} o
* @returns {FramingOptions} * @returns {FramingOptions}
*/ */
static fromObject(o) { static fromObject(o) {