From 1720db9eb73a2bf681b9fef1a8e676088fcd7f4a Mon Sep 17 00:00:00 2001 From: Kim Ravn Hansen Date: Fri, 5 Sep 2025 17:57:09 +0200 Subject: [PATCH] stuff and junk and things --- server/.prettierignore | 1 + server/.prettierrc | 4 +- server/models/character.js | 24 +- server/models/game.js | 8 +- server/models/item.js | 31 +-- server/models/location.js | 16 +- server/models/player.js | 49 +++- server/models/portal.js | 4 +- server/public/index.html | 511 ++++++++++++++++++++----------------- server/server.js | 71 ++++-- server/utils/dice.js | 3 +- server/utils/id.js | 5 +- server/utils/messages.js | 30 ++- server/utils/password.js | 36 ++- 14 files changed, 460 insertions(+), 333 deletions(-) diff --git a/server/.prettierignore b/server/.prettierignore index 1b8ac88..f6455c6 100644 --- a/server/.prettierignore +++ b/server/.prettierignore @@ -1,3 +1,4 @@ # Ignore artifacts: build coverage +node_modules diff --git a/server/.prettierrc b/server/.prettierrc index 0967ef4..0a02bce 100644 --- a/server/.prettierrc +++ b/server/.prettierrc @@ -1 +1,3 @@ -{} +{ + "tabWidth": 4 +} diff --git a/server/models/character.js b/server/models/character.js index ddd2774..b5a639f 100755 --- a/server/models/character.js +++ b/server/models/character.js @@ -7,25 +7,32 @@ import * as id from "../utils/id.js"; * @class */ export class Character { - /** @type {string} character's name */ name; /** @protected @type {number} The number of XP the character has. */ _xp = 0; - get xp() { return this._xp; } + get xp() { + return this._xp; + } /** @protected @type {number} The character's level. */ _level = 1; - get level() { return this._level; } + get level() { + return this._level; + } /** @protected @type {string} unique name used for chats when there's a name clash and also other things that require a unique character id */ _id; - get id() { return this._id; } + get id() { + return this._id; + } /** @protected @type {string} username of the player that owns this character. */ _username; - get username() { return this._username; } + get username() { + return this._username; + } /** @type {string} Bloodline background */ ancestry; @@ -57,7 +64,6 @@ export class Character { * @param {boolean} initialize Should we initialize the character */ constructor(playerUname, name, initialize) { - this.name = name; // Initialize the unique name if this character. @@ -138,7 +144,9 @@ export class Character { this.meleeCombat = Math.max(this.skulduggery, 10); break; default: - throw new Error('Logic error, ancestry d8() roll was out of scope'); + throw new Error( + "Logic error, ancestry d8() roll was out of scope", + ); } // @@ -167,7 +175,7 @@ export class Character { this.equipment .set("sickle", 1) .set("poisoner's kit", 1) - .set("healer's kit", 1) + .set("healer's kit", 1); default: this.foundation = "debug"; this.proficiencies.add("heavy_armor"); diff --git a/server/models/game.js b/server/models/game.js index 82371f2..e5921c1 100755 --- a/server/models/game.js +++ b/server/models/game.js @@ -11,8 +11,7 @@ import WebSocket from "ws"; import { Character } from "./character.js"; import { ItemTemplate } from "./item.js"; -export class Game{ - +export class Game { /** @type {Map} List of all item templates in the game */ _itemTemplates = new Map(); @@ -33,5 +32,8 @@ export class Game{ * @protected * @type {Map} Map of users in the game username->Player */ - _players = new Map(); get players() { return this._players; } + _players = new Map(); + get players() { + return this._players; + } } diff --git a/server/models/item.js b/server/models/item.js index ef4f625..fde5f2b 100755 --- a/server/models/item.js +++ b/server/models/item.js @@ -7,7 +7,6 @@ import { cleanName } from "../utils/id.js"; * generate these CharacterItems. */ export class ItemTemplate { - _id; _name; _description; @@ -42,15 +41,18 @@ export class ItemTemplate { * @param {string=} id Item's machine-friendly name. */ constructor(name, itemSlots, description, id) { - if (typeof name !== "string") { - throw new Error("Name must be a string, but " + typeof name + " given."); + throw new Error( + "Name must be a string, but " + typeof name + " given.", + ); } if (typeof description === "undefined") { description = ""; } if (typeof description !== "string") { - throw new Error("Name must be a string, but " + typeof name + " given."); + throw new Error( + "Name must be a string, but " + typeof name + " given.", + ); } if (!Number.isFinite(itemSlots)) { throw new Error("itemSlots must be a finite number!"); @@ -66,11 +68,15 @@ export class ItemTemplate { this._id = id; this._itemSlots = Number(itemSlots); this._description = ""; - } createItem() { - return new ChracterItem(this._id, this._name, this._description, this._itemSlots); + return new ChracterItem( + this._id, + this._name, + this._description, + this._itemSlots, + ); } } @@ -82,10 +88,10 @@ export class ItemTemplate { * * If a character picks up a Pickaxe in the dungeon, a new CharacterItem is spawned and injected into * the character's Equipment Map. If the item is dropped/destroyed/sold, the CharacterItem is removed from - * the character's Equipment Map, and then deleted from memory. + * the character's Equipment Map, and then deleted from memory. * - * If a ChracterItem is traded away to another character, The other character inserts a clone of this item - * into their equipment map, and the item is then deleted from the previous owner's equipment list. + * If a ChracterItem is traded away to another character, The other character inserts a clone of this item + * into their equipment map, and the item is then deleted from the previous owner's equipment list. * This is done so we do not have mulltiple characters with pointers to the same item - we would rather risk * dupes than wonky references. * @@ -94,9 +100,8 @@ export class ItemTemplate { * Another bonus is, that the game can spawn custom items that arent even in the ItemTemplate Set. */ export class CharacterItem { - /** @type {string?} The unique name if the ItemTemplate this item is based on. May be null. */ - templateItemId; // We use the id instead of a pointer, could make garbage collection better. + templateItemId; // We use the id instead of a pointer, could make garbage collection better. /** @type {string} The player's name for this item. */ name; @@ -119,7 +124,3 @@ const i = new ItemTemplate("knife", 10000); const ci = new CharacterItem(); console.log(ci); - - - - diff --git a/server/models/location.js b/server/models/location.js index 777d380..0fb4cea 100755 --- a/server/models/location.js +++ b/server/models/location.js @@ -1,13 +1,13 @@ import { Portal } from "./portal"; /** -* Location in the world. -* -* Can contain characters, quests, monsters, loot, NPCs and more. -* -* Can contain mundane portals (such as doors or pathways) to adjacent rooms/locations, -* or magical portals to distant locations. -*/ + * Location in the world. + * + * Can contain characters, quests, monsters, loot, NPCs and more. + * + * Can contain mundane portals (such as doors or pathways) to adjacent rooms/locations, + * or magical portals to distant locations. + */ export class Location { /** @protected @type string */ _id; @@ -34,7 +34,7 @@ export class Location { } /** - */ + */ constructor(id, name, description) { this._id = id; this._name = name; diff --git a/server/models/player.js b/server/models/player.js index 50785d7..44328c2 100755 --- a/server/models/player.js +++ b/server/models/player.js @@ -1,4 +1,4 @@ -import WebSocket from 'ws'; +import WebSocket from "ws"; /** * Player Account. @@ -13,18 +13,24 @@ import WebSocket from 'ws'; * We regularly ping and pong to ensure that stale connections are closed. * */ -export class Player{ +export class Player { /** @protected @type {string} unique username */ _username; - get username() { return this._username; } + get username() { + return this._username; + } /** @protected @type {string} */ _passwordHash; - get passwordHash() { return this._passwordHash; } + get passwordHash() { + return this._passwordHash; + } /** @protected @type {WebSocket} Player's current and only websocket. If undefined, the player is not logged in. */ _websocket; - get websocket() { return this._websocket; } + get websocket() { + return this._websocket; + } /** @protected @type {Date} */ _latestSocketReceived; @@ -46,7 +52,11 @@ export class Player{ */ _send(data) { if (!this._websocket) { - console.error("Trying to send a message to an uninitialized websocket", this, data) + console.error( + "Trying to send a message to an uninitialized websocket", + this, + data, + ); return false; } if (this._websocket.readyState === WebSocket.OPEN) { @@ -54,19 +64,36 @@ export class Player{ return true; } if (this._websocket.readyState === WebSocket.CLOSED) { - console.error("Trying to send a message through a CLOSED websocket", this, data); + console.error( + "Trying to send a message through a CLOSED websocket", + this, + data, + ); return false; } if (this._websocket.readyState === WebSocket.CLOSING) { - console.error("Trying to send a message through a CLOSING websocket", this, data); + console.error( + "Trying to send a message through a CLOSING websocket", + this, + data, + ); return false; } if (this._websocket.readyState === WebSocket.CONNECTING) { - console.error("Trying to send a message through a CONNECTING (not yet open) websocket", this, data); + console.error( + "Trying to send a message through a CONNECTING (not yet open) websocket", + this, + data, + ); return false; } - console.error("Trying to send a message through a websocket with an UNKNOWN readyState (%d)", this.websocket.readyState, this, data); + console.error( + "Trying to send a message through a websocket with an UNKNOWN readyState (%d)", + this.websocket.readyState, + this, + data, + ); return false; } @@ -74,5 +101,3 @@ export class Player{ this.sendMessage(`\n[${this.currentRoom}] > `); } } - - diff --git a/server/models/portal.js b/server/models/portal.js index df02a09..9517bb1 100755 --- a/server/models/portal.js +++ b/server/models/portal.js @@ -8,9 +8,8 @@ * @todo Add encounters to portals */ export class Portal { - /** - * Target Location. + * Target Location. */ _targetLocationId; @@ -23,5 +22,4 @@ export class Portal { * Description shown to the player when they traverse the portal. */ _traversalDescription; - } diff --git a/server/public/index.html b/server/public/index.html index 5b207f5..bc93b13 100755 --- a/server/public/index.html +++ b/server/public/index.html @@ -1,269 +1,300 @@ - + - - - - WebSocket MUD - - - -
-
Connecting...
-
-
- - + .prompt { + color: #00ccff; + } + + + +
+
Connecting...
+
+
+ + +
-
- - + // Initialize the MUD client when the page loads + document.addEventListener("DOMContentLoaded", () => { + new MUDClient(); + }); + + diff --git a/server/server.js b/server/server.js index 3824ffe..7381e62 100755 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ import path from "path"; import fs from "fs"; import { Player } from "./models/player.js"; import { Game } from "./models/game.js"; -import { ClientMessage, MSG_ERROR, MSG_MESSAGE, MSG_PROMPT, MSG_CALAMITY } from "./utils/messages.js"; +import { ClientMessage, MSG_ERROR, MSG_MESSAGE, MSG_PROMPT, MSG_CALAMITY, } from "./utils/messages.js"; class Session { /** @type {boolean} */ @@ -49,8 +49,12 @@ class MudServer { console.log("New connection established"); this.sessions[websocket] = new Session(); - websocket.on("message", (data) => { this.onIncomingMessage(websocket, data) }); - websocket.on("close", () => { this.onConnectionClosed(websocket); }); + websocket.on("message", (data) => { + this.onIncomingMessage(websocket, data); + }); + websocket.on("close", () => { + this.onConnectionClosed(websocket); + }); this.send(websocket, MSG_MESSAGE, "Welcome to MUUUHD", "big"); this.send(websocket, MSG_PROMPT, "Please enter your username"); @@ -64,10 +68,18 @@ class MudServer { const session = this.sessions.get(websocket); if (!session) { - console.error("Incoming message from a client without a session!", data); - this.send(websocket, MSG_ERROR, "terminal", "You do not have an active session. Go away!") + console.error( + "Incoming message from a client without a session!", + data, + ); + this.send( + websocket, + MSG_ERROR, + "terminal", + "You do not have an active session. Go away!", + ); websocket.close(); - return + return; } let message; @@ -76,9 +88,14 @@ class MudServer { message = new ClientMessage(data); } catch (error) { console.error("Bad websocket message", data, error); - this.send(websocket, MSG_ERROR, "terminal", "You sent me a bad message! Goodbye...") + this.send( + websocket, + MSG_ERROR, + "terminal", + "You sent me a bad message! Goodbye...", + ); websocket.close(); - return + return; } if (!session.usernameProcessed) { @@ -87,8 +104,14 @@ class MudServer { // We haven"t gotten a username yet, so we expect one. //---------------------------------------------------- if (!message.hasUsername()) { - console.error("User should have sent a “username” message, but sent something else instead") - this.send(websocket, MSG_CALAMITY, "I expected you to send me a username, but you sent me something else instead. You bad! Goodbye...") + console.error( + "User should have sent a “username” message, but sent something else instead", + ); + this.send( + websocket, + MSG_CALAMITY, + "I expected you to send me a username, but you sent me something else instead. You bad! Goodbye...", + ); // for now, just close the socket. websocket.close(); @@ -97,10 +120,14 @@ class MudServer { const player = this.game.players.get(message.username); if (!player) { - // player not found - for now, just close the connection - make a better + // player not found - for now, just close the connection - make a better console.log("Invalid username sent during login: %s", username); this.send(websocket, MSG_ERROR, "Invalid username"); - this.send(websocket, MSG_PROMPT, "Please enter a valid username"); + this.send( + websocket, + MSG_PROMPT, + "Please enter a valid username", + ); } // correct username, tentatively assign player to session @@ -120,11 +147,13 @@ class MudServer { //---------------------------------------------------- if (!session.passwordProcessed) { if (!message.hasPassword) { - console.error("Youser should have sent a “password” message, but sent this instead: %s", message.type); + console.error( + "Youser should have sent a “password” message, but sent this instead: %s", + message.type, + ); } } - // //---------------------------------------------------- // Process the player's commands @@ -134,7 +163,10 @@ class MudServer { return; } - console.error("We have received a message we couldn't handle!!!", message); + console.error( + "We have received a message we couldn't handle!!!", + message, + ); } /** @@ -226,7 +258,9 @@ class MudServer { break; default: - player.sendMessage(`Unknown command: ${command}. Type "help" for available commands.`); + player.sendMessage( + `Unknown command: ${command}. Type "help" for available commands.`, + ); } player.sendPrompt(); @@ -257,7 +291,10 @@ class MudServer { // Create HTTP server for serving the client const server = http.createServer((req, res) => { // let filePath = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url); - let filePath = path.join("public", req.url === "/" ? "index.html" : req.url); + let filePath = path.join( + "public", + req.url === "/" ? "index.html" : req.url, + ); const ext = path.extname(filePath); const contentTypes = { diff --git a/server/utils/dice.js b/server/utils/dice.js index 33f16a9..8998d1c 100755 --- a/server/utils/dice.js +++ b/server/utils/dice.js @@ -1,5 +1,5 @@ export function withSides(sides) { - const r = Math.random() + const r = Math.random(); return Math.floor(r * sides) + 1; } @@ -10,4 +10,3 @@ export function d6() { export function d8() { return withSides(8); } - diff --git a/server/utils/id.js b/server/utils/id.js index 7ec1b28..2983c40 100755 --- a/server/utils/id.js +++ b/server/utils/id.js @@ -2,7 +2,10 @@ export function cleanName(s) { if (typeof s !== "string") { throw new Error("String expected, but got a ", typeof s); } - return s.toLowerCase().replace(" ", "_").replace(/[^a-zA-Z0-9_]/, "_"); + return s + .toLowerCase() + .replace(" ", "_") + .replace(/[^a-zA-Z0-9_]/, "_"); } /** diff --git a/server/utils/messages.js b/server/utils/messages.js index f3553ed..8ac3dbd 100755 --- a/server/utils/messages.js +++ b/server/utils/messages.js @@ -1,4 +1,4 @@ -/** +/** * Very bad logic error. Player must quit game, refresh page, and log in again. * * Client-->Server @@ -31,14 +31,14 @@ export const MSG_PASSWORD = "pass"; */ export const MSG_PROMPT = "ask"; -/** +/** * Client sends the player's username to the server * * Player-->Client-->Server */ export const MSG_USERNAME = "user"; -/** +/** * Player has entered a command, and wants to do something. * * Player-->Client-->Server @@ -49,8 +49,8 @@ export const MSG_COMMAND = "c"; * Represents a message sent from client to server. */ export class ClientMessage { - /** - * @protected + /** + * @protected * @type {any[]} _arr The array that contains the message data */ _arr; @@ -65,28 +65,36 @@ export class ClientMessage { return this._arr[0]; } - /** * @param {string} msgData the raw text data in the websocket message. */ constructor(msgData) { if (typeof msgData !== "string") { - throw new Error("Could not create client message. Attempting to parse json, but data was not even a string, it was a " + typeof msgData); - return + throw new Error( + "Could not create client message. Attempting to parse json, but data was not even a string, it was a " + + typeof msgData, + ); + return; } try { this._arr = JSON.parse(msgData); } catch (_) { - throw new Error(`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`); + throw new Error( + `Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`, + ); } if (typeof this._arr !== "array") { - throw new Error(`Could not create client message. Excpected an array, but got a ${typeof this._arr}`); + throw new Error( + `Could not create client message. Excpected an array, but got a ${typeof this._arr}`, + ); } if (this._arr.length < 1) { - throw new Error("Could not create client message. Excpected an array with at least 1 element, but got an empty one"); + throw new Error( + "Could not create client message. Excpected an array with at least 1 element, but got an empty one", + ); } this._arr = arr; diff --git a/server/utils/password.js b/server/utils/password.js index 2011349..218f7c0 100644 --- a/server/utils/password.js +++ b/server/utils/password.js @@ -1,30 +1,42 @@ -import { randomBytes, pbkdf2Sync, randomInt } from 'node:crypto'; +import { randomBytes, pbkdf2Sync, randomInt } from "node:crypto"; // Settings (tune as needed) const ITERATIONS = 100_000; // Slow enough to deter brute force const KEYLEN = 64; // 512-bit hash -const DIGEST = 'sha512'; +const DIGEST = "sha512"; /** * Generate a hash from a plaintext password. - * @param {String} password + * @param {String} password * @returns String */ export function hash(password) { - const salt = randomBytes(16).toString('hex'); // 128-bit salt - const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString('hex'); - return `${ITERATIONS}:${salt}:${hash}`; + const salt = randomBytes(16).toString("hex"); // 128-bit salt + const hash = pbkdf2Sync( + password, + salt, + ITERATIONS, + KEYLEN, + DIGEST, + ).toString("hex"); + return `${ITERATIONS}:${salt}:${hash}`; } /** * Verify that a password is correct against a given hash. - * - * @param {String} password - * @param {String} hashed_password + * + * @param {String} password + * @param {String} hashed_password * @returns Boolean */ export function verify(password, hashed_password) { - const [iterations, salt, hash] = hashed_password.split(':'); - const derived = pbkdf2Sync(password, salt, Number(iterations), KEYLEN, DIGEST).toString('hex'); - return hash === derived; + const [iterations, salt, hash] = hashed_password.split(":"); + const derived = pbkdf2Sync( + password, + salt, + Number(iterations), + KEYLEN, + DIGEST, + ).toString("hex"); + return hash === derived; }