stuff and junk and things
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
# Ignore artifacts:
|
# Ignore artifacts:
|
||||||
build
|
build
|
||||||
coverage
|
coverage
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,25 +7,32 @@ import * as id from "../utils/id.js";
|
|||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
export class Character {
|
export class Character {
|
||||||
|
|
||||||
/** @type {string} character's name */
|
/** @type {string} character's name */
|
||||||
name;
|
name;
|
||||||
|
|
||||||
/** @protected @type {number} The number of XP the character has. */
|
/** @protected @type {number} The number of XP the character has. */
|
||||||
_xp = 0;
|
_xp = 0;
|
||||||
get xp() { return this._xp; }
|
get xp() {
|
||||||
|
return this._xp;
|
||||||
|
}
|
||||||
|
|
||||||
/** @protected @type {number} The character's level. */
|
/** @protected @type {number} The character's level. */
|
||||||
_level = 1;
|
_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 */
|
/** @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;
|
_id;
|
||||||
get id() { return this._id; }
|
get id() {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
/** @protected @type {string} username of the player that owns this character. */
|
/** @protected @type {string} username of the player that owns this character. */
|
||||||
_username;
|
_username;
|
||||||
get username() { return this._username; }
|
get username() {
|
||||||
|
return this._username;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {string} Bloodline background */
|
/** @type {string} Bloodline background */
|
||||||
ancestry;
|
ancestry;
|
||||||
@@ -57,7 +64,6 @@ export class Character {
|
|||||||
* @param {boolean} initialize Should we initialize the character
|
* @param {boolean} initialize Should we initialize the character
|
||||||
*/
|
*/
|
||||||
constructor(playerUname, name, initialize) {
|
constructor(playerUname, name, initialize) {
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|
||||||
// Initialize the unique name if this character.
|
// Initialize the unique name if this character.
|
||||||
@@ -138,7 +144,9 @@ export class Character {
|
|||||||
this.meleeCombat = Math.max(this.skulduggery, 10);
|
this.meleeCombat = Math.max(this.skulduggery, 10);
|
||||||
break;
|
break;
|
||||||
default:
|
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
|
this.equipment
|
||||||
.set("sickle", 1)
|
.set("sickle", 1)
|
||||||
.set("poisoner's kit", 1)
|
.set("poisoner's kit", 1)
|
||||||
.set("healer's kit", 1)
|
.set("healer's kit", 1);
|
||||||
default:
|
default:
|
||||||
this.foundation = "debug";
|
this.foundation = "debug";
|
||||||
this.proficiencies.add("heavy_armor");
|
this.proficiencies.add("heavy_armor");
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import WebSocket from "ws";
|
|||||||
import { Character } from "./character.js";
|
import { Character } from "./character.js";
|
||||||
import { ItemTemplate } from "./item.js";
|
import { ItemTemplate } from "./item.js";
|
||||||
|
|
||||||
export class Game{
|
export class Game {
|
||||||
|
|
||||||
/** @type {Map<string,ItemTemplate>} List of all item templates in the game */
|
/** @type {Map<string,ItemTemplate>} List of all item templates in the game */
|
||||||
_itemTemplates = new Map();
|
_itemTemplates = new Map();
|
||||||
|
|
||||||
@@ -33,5 +32,8 @@ export class Game{
|
|||||||
* @protected
|
* @protected
|
||||||
* @type {Map<string,Player>} Map of users in the game username->Player
|
* @type {Map<string,Player>} Map of users in the game username->Player
|
||||||
*/
|
*/
|
||||||
_players = new Map(); get players() { return this._players; }
|
_players = new Map();
|
||||||
|
get players() {
|
||||||
|
return this._players;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { cleanName } from "../utils/id.js";
|
|||||||
* generate these CharacterItems.
|
* generate these CharacterItems.
|
||||||
*/
|
*/
|
||||||
export class ItemTemplate {
|
export class ItemTemplate {
|
||||||
|
|
||||||
_id;
|
_id;
|
||||||
_name;
|
_name;
|
||||||
_description;
|
_description;
|
||||||
@@ -42,15 +41,18 @@ export class ItemTemplate {
|
|||||||
* @param {string=} id Item's machine-friendly name.
|
* @param {string=} id Item's machine-friendly name.
|
||||||
*/
|
*/
|
||||||
constructor(name, itemSlots, description, id) {
|
constructor(name, itemSlots, description, id) {
|
||||||
|
|
||||||
if (typeof name !== "string") {
|
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") {
|
if (typeof description === "undefined") {
|
||||||
description = "";
|
description = "";
|
||||||
}
|
}
|
||||||
if (typeof description !== "string") {
|
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)) {
|
if (!Number.isFinite(itemSlots)) {
|
||||||
throw new Error("itemSlots must be a finite number!");
|
throw new Error("itemSlots must be a finite number!");
|
||||||
@@ -66,11 +68,15 @@ export class ItemTemplate {
|
|||||||
this._id = id;
|
this._id = id;
|
||||||
this._itemSlots = Number(itemSlots);
|
this._itemSlots = Number(itemSlots);
|
||||||
this._description = "";
|
this._description = "";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createItem() {
|
createItem() {
|
||||||
return new ChracterItem(this._id, this._name, this._description, this._itemSlots);
|
return new ChracterItem(
|
||||||
|
this._id,
|
||||||
|
this._name,
|
||||||
|
this._description,
|
||||||
|
this._itemSlots,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +100,6 @@ export class ItemTemplate {
|
|||||||
* Another bonus is, that the game can spawn custom items that arent even in the ItemTemplate Set.
|
* Another bonus is, that the game can spawn custom items that arent even in the ItemTemplate Set.
|
||||||
*/
|
*/
|
||||||
export class CharacterItem {
|
export class CharacterItem {
|
||||||
|
|
||||||
/** @type {string?} The unique name if the ItemTemplate this item is based on. May be null. */
|
/** @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.
|
||||||
|
|
||||||
@@ -119,7 +124,3 @@ const i = new ItemTemplate("knife", 10000);
|
|||||||
|
|
||||||
const ci = new CharacterItem();
|
const ci = new CharacterItem();
|
||||||
console.log(ci);
|
console.log(ci);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Portal } from "./portal";
|
import { Portal } from "./portal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Location in the world.
|
* Location in the world.
|
||||||
*
|
*
|
||||||
* Can contain characters, quests, monsters, loot, NPCs and more.
|
* Can contain characters, quests, monsters, loot, NPCs and more.
|
||||||
*
|
*
|
||||||
* Can contain mundane portals (such as doors or pathways) to adjacent rooms/locations,
|
* Can contain mundane portals (such as doors or pathways) to adjacent rooms/locations,
|
||||||
* or magical portals to distant locations.
|
* or magical portals to distant locations.
|
||||||
*/
|
*/
|
||||||
export class Location {
|
export class Location {
|
||||||
/** @protected @type string */
|
/** @protected @type string */
|
||||||
_id;
|
_id;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import WebSocket from 'ws';
|
import WebSocket from "ws";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player Account.
|
* Player Account.
|
||||||
@@ -13,18 +13,24 @@ import WebSocket from 'ws';
|
|||||||
* We regularly ping and pong to ensure that stale connections are closed.
|
* We regularly ping and pong to ensure that stale connections are closed.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class Player{
|
export class Player {
|
||||||
/** @protected @type {string} unique username */
|
/** @protected @type {string} unique username */
|
||||||
_username;
|
_username;
|
||||||
get username() { return this._username; }
|
get username() {
|
||||||
|
return this._username;
|
||||||
|
}
|
||||||
|
|
||||||
/** @protected @type {string} */
|
/** @protected @type {string} */
|
||||||
_passwordHash;
|
_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. */
|
/** @protected @type {WebSocket} Player's current and only websocket. If undefined, the player is not logged in. */
|
||||||
_websocket;
|
_websocket;
|
||||||
get websocket() { return this._websocket; }
|
get websocket() {
|
||||||
|
return this._websocket;
|
||||||
|
}
|
||||||
|
|
||||||
/** @protected @type {Date} */
|
/** @protected @type {Date} */
|
||||||
_latestSocketReceived;
|
_latestSocketReceived;
|
||||||
@@ -46,7 +52,11 @@ export class Player{
|
|||||||
*/
|
*/
|
||||||
_send(data) {
|
_send(data) {
|
||||||
if (!this._websocket) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (this._websocket.readyState === WebSocket.OPEN) {
|
if (this._websocket.readyState === WebSocket.OPEN) {
|
||||||
@@ -54,19 +64,36 @@ export class Player{
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this._websocket.readyState === WebSocket.CLOSED) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (this._websocket.readyState === WebSocket.CLOSING) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (this._websocket.readyState === WebSocket.CONNECTING) {
|
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;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,5 +101,3 @@ export class Player{
|
|||||||
this.sendMessage(`\n[${this.currentRoom}] > `);
|
this.sendMessage(`\n[${this.currentRoom}] > `);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
* @todo Add encounters to portals
|
* @todo Add encounters to portals
|
||||||
*/
|
*/
|
||||||
export class Portal {
|
export class Portal {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Target Location.
|
* Target Location.
|
||||||
*/
|
*/
|
||||||
@@ -23,5 +22,4 @@ export class Portal {
|
|||||||
* Description shown to the player when they traverse the portal.
|
* Description shown to the player when they traverse the portal.
|
||||||
*/
|
*/
|
||||||
_traversalDescription;
|
_traversalDescription;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>WebSocket MUD</title>
|
<title>WebSocket MUD</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
border: 2px solid #333;
|
border: 2px solid #333;
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
border: 2px solid #555;
|
border: 2px solid #555;
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +101,18 @@
|
|||||||
color: #00ccff;
|
color: #00ccff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="status" class="connecting">Connecting...</div>
|
<div id="status" class="connecting">Connecting...</div>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<div id="input-container">
|
<div id="input-container">
|
||||||
<input type="text" id="input" placeholder="Enter command..." disabled>
|
<input
|
||||||
|
type="text"
|
||||||
|
id="input"
|
||||||
|
placeholder="Enter command..."
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
<button id="send" disabled>Send</button>
|
<button id="send" disabled>Send</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,26 +121,27 @@
|
|||||||
class MUDClient {
|
class MUDClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.output = document.getElementById('output');
|
this.output = document.getElementById("output");
|
||||||
this.input = document.getElementById('input');
|
this.input = document.getElementById("input");
|
||||||
this.sendButton = document.getElementById('send');
|
this.sendButton = document.getElementById("send");
|
||||||
this.status = document.getElementById('status');
|
this.status = document.getElementById("status");
|
||||||
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol =
|
||||||
|
window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const wsUrl = `${protocol}//${window.location.host}`;
|
const wsUrl = `${protocol}//${window.location.host}`;
|
||||||
|
|
||||||
this.updateStatus('Connecting...', 'connecting');
|
this.updateStatus("Connecting...", "connecting");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this. s = new WebSocket(wsUrl);
|
this.s = new WebSocket(wsUrl);
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
this.updateStatus('Connected', 'connected');
|
this.updateStatus("Connected", "connected");
|
||||||
this.input.disabled = false;
|
this.input.disabled = false;
|
||||||
this.sendButton.disabled = false;
|
this.sendButton.disabled = false;
|
||||||
this.input.focus();
|
this.input.focus();
|
||||||
@@ -147,7 +153,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
this.updateStatus('Disconnected', 'disconnected');
|
this.updateStatus("Disconnected", "disconnected");
|
||||||
this.input.disabled = true;
|
this.input.disabled = true;
|
||||||
this.sendButton.disabled = true;
|
this.sendButton.disabled = true;
|
||||||
|
|
||||||
@@ -156,24 +162,26 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onerror = (error) => {
|
this.ws.onerror = (error) => {
|
||||||
this.updateStatus('Connection Error', 'error');
|
this.updateStatus("Connection Error", "error");
|
||||||
this.appendOutput('Connection error occurred. Retrying...', 'error');
|
this.appendOutput(
|
||||||
|
"Connection error occurred. Retrying...",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.updateStatus('Connection Failed', 'error');
|
this.updateStatus("Connection Failed", "error");
|
||||||
setTimeout(() => this.connect(), 3000);
|
setTimeout(() => this.connect(), 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
this.input.addEventListener('keypress', (e) => {
|
this.input.addEventListener("keypress", (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
this.sendMessage();
|
this.sendMessage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sendButton.addEventListener('click', () => {
|
this.sendButton.addEventListener("click", () => {
|
||||||
this.sendMessage();
|
this.sendMessage();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,21 +189,34 @@
|
|||||||
this.commandHistory = [];
|
this.commandHistory = [];
|
||||||
this.historyIndex = -1;
|
this.historyIndex = -1;
|
||||||
|
|
||||||
this.input.addEventListener('keydown', (e) => {
|
this.input.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'ArrowUp') {
|
if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.historyIndex < this.commandHistory.length - 1) {
|
if (
|
||||||
|
this.historyIndex <
|
||||||
|
this.commandHistory.length - 1
|
||||||
|
) {
|
||||||
this.historyIndex++;
|
this.historyIndex++;
|
||||||
this.input.value = this.commandHistory[this.commandHistory.length - 1 - this.historyIndex];
|
this.input.value =
|
||||||
|
this.commandHistory[
|
||||||
|
this.commandHistory.length -
|
||||||
|
1 -
|
||||||
|
this.historyIndex
|
||||||
|
];
|
||||||
}
|
}
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.historyIndex > 0) {
|
if (this.historyIndex > 0) {
|
||||||
this.historyIndex--;
|
this.historyIndex--;
|
||||||
this.input.value = this.commandHistory[this.commandHistory.length - 1 - this.historyIndex];
|
this.input.value =
|
||||||
|
this.commandHistory[
|
||||||
|
this.commandHistory.length -
|
||||||
|
1 -
|
||||||
|
this.historyIndex
|
||||||
|
];
|
||||||
} else if (this.historyIndex === 0) {
|
} else if (this.historyIndex === 0) {
|
||||||
this.historyIndex = -1;
|
this.historyIndex = -1;
|
||||||
this.input.value = '';
|
this.input.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -203,9 +224,17 @@
|
|||||||
|
|
||||||
sendMessage() {
|
sendMessage() {
|
||||||
const message = this.input.value.trim();
|
const message = this.input.value.trim();
|
||||||
if (message && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (
|
||||||
|
message &&
|
||||||
|
this.ws &&
|
||||||
|
this.ws.readyState === WebSocket.OPEN
|
||||||
|
) {
|
||||||
// Add to command history
|
// Add to command history
|
||||||
if (this.commandHistory[this.commandHistory.length - 1] !== message) {
|
if (
|
||||||
|
this.commandHistory[
|
||||||
|
this.commandHistory.length - 1
|
||||||
|
] !== message
|
||||||
|
) {
|
||||||
this.commandHistory.push(message);
|
this.commandHistory.push(message);
|
||||||
if (this.commandHistory.length > 50) {
|
if (this.commandHistory.length > 50) {
|
||||||
this.commandHistory.shift();
|
this.commandHistory.shift();
|
||||||
@@ -213,40 +242,42 @@
|
|||||||
}
|
}
|
||||||
this.historyIndex = -1;
|
this.historyIndex = -1;
|
||||||
|
|
||||||
this.ws.send(JSON.stringify({
|
this.ws.send(
|
||||||
type: 'command',
|
JSON.stringify({
|
||||||
content: message
|
type: "command",
|
||||||
}));
|
content: message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
this.input.value = '';
|
this.input.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMessage(data) {
|
handleMessage(data) {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'message':
|
case "message":
|
||||||
this.appendOutput(data.content);
|
this.appendOutput(data.content);
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case "error":
|
||||||
this.appendOutput(data.content, 'error');
|
this.appendOutput(data.content, "error");
|
||||||
break;
|
break;
|
||||||
case 'system':
|
case "system":
|
||||||
this.appendOutput(data.content, 'system');
|
this.appendOutput(data.content, "system");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.appendOutput(data.content);
|
this.appendOutput(data.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendOutput(text, className = '') {
|
appendOutput(text, className = "") {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement("div");
|
||||||
if (className) {
|
if (className) {
|
||||||
div.className = className;
|
div.className = className;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this looks like a prompt
|
// Check if this looks like a prompt
|
||||||
if (text.includes('] > ')) {
|
if (text.includes("] > ")) {
|
||||||
div.className = 'prompt';
|
div.className = "prompt";
|
||||||
}
|
}
|
||||||
|
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
@@ -261,9 +292,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the MUD client when the page loads
|
// Initialize the MUD client when the page loads
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
new MUDClient();
|
new MUDClient();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { Player } from "./models/player.js";
|
import { Player } from "./models/player.js";
|
||||||
import { Game } from "./models/game.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 {
|
class Session {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
@@ -49,8 +49,12 @@ class MudServer {
|
|||||||
console.log("New connection established");
|
console.log("New connection established");
|
||||||
this.sessions[websocket] = new Session();
|
this.sessions[websocket] = new Session();
|
||||||
|
|
||||||
websocket.on("message", (data) => { this.onIncomingMessage(websocket, data) });
|
websocket.on("message", (data) => {
|
||||||
websocket.on("close", () => { this.onConnectionClosed(websocket); });
|
this.onIncomingMessage(websocket, data);
|
||||||
|
});
|
||||||
|
websocket.on("close", () => {
|
||||||
|
this.onConnectionClosed(websocket);
|
||||||
|
});
|
||||||
|
|
||||||
this.send(websocket, MSG_MESSAGE, "Welcome to MUUUHD", "big");
|
this.send(websocket, MSG_MESSAGE, "Welcome to MUUUHD", "big");
|
||||||
this.send(websocket, MSG_PROMPT, "Please enter your username");
|
this.send(websocket, MSG_PROMPT, "Please enter your username");
|
||||||
@@ -64,10 +68,18 @@ class MudServer {
|
|||||||
const session = this.sessions.get(websocket);
|
const session = this.sessions.get(websocket);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
console.error("Incoming message from a client without a session!", data);
|
console.error(
|
||||||
this.send(websocket, MSG_ERROR, "terminal", "You do not have an active session. Go away!")
|
"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();
|
websocket.close();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message;
|
let message;
|
||||||
@@ -76,9 +88,14 @@ class MudServer {
|
|||||||
message = new ClientMessage(data);
|
message = new ClientMessage(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Bad websocket message", data, 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();
|
websocket.close();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.usernameProcessed) {
|
if (!session.usernameProcessed) {
|
||||||
@@ -87,8 +104,14 @@ class MudServer {
|
|||||||
// We haven"t gotten a username yet, so we expect one.
|
// We haven"t gotten a username yet, so we expect one.
|
||||||
//----------------------------------------------------
|
//----------------------------------------------------
|
||||||
if (!message.hasUsername()) {
|
if (!message.hasUsername()) {
|
||||||
console.error("User should have sent a “username” message, but sent something else instead")
|
console.error(
|
||||||
this.send(websocket, MSG_CALAMITY, "I expected you to send me a username, but you sent me something else instead. You bad! Goodbye...")
|
"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.
|
// for now, just close the socket.
|
||||||
websocket.close();
|
websocket.close();
|
||||||
@@ -100,7 +123,11 @@ class MudServer {
|
|||||||
// 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);
|
console.log("Invalid username sent during login: %s", username);
|
||||||
this.send(websocket, MSG_ERROR, "Invalid 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
|
// correct username, tentatively assign player to session
|
||||||
@@ -120,11 +147,13 @@ class MudServer {
|
|||||||
//----------------------------------------------------
|
//----------------------------------------------------
|
||||||
if (!session.passwordProcessed) {
|
if (!session.passwordProcessed) {
|
||||||
if (!message.hasPassword) {
|
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
|
// Process the player's commands
|
||||||
@@ -134,7 +163,10 @@ class MudServer {
|
|||||||
return;
|
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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
player.sendMessage(`Unknown command: ${command}. Type "help" for available commands.`);
|
player.sendMessage(
|
||||||
|
`Unknown command: ${command}. Type "help" for available commands.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.sendPrompt();
|
player.sendPrompt();
|
||||||
@@ -257,7 +291,10 @@ class MudServer {
|
|||||||
// Create HTTP server for serving the client
|
// Create HTTP server for serving the client
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
// let filePath = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url);
|
// 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 ext = path.extname(filePath);
|
||||||
|
|
||||||
const contentTypes = {
|
const contentTypes = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export function withSides(sides) {
|
export function withSides(sides) {
|
||||||
const r = Math.random()
|
const r = Math.random();
|
||||||
return Math.floor(r * sides) + 1;
|
return Math.floor(r * sides) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,4 +10,3 @@ export function d6() {
|
|||||||
export function d8() {
|
export function d8() {
|
||||||
return withSides(8);
|
return withSides(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ export function cleanName(s) {
|
|||||||
if (typeof s !== "string") {
|
if (typeof s !== "string") {
|
||||||
throw new Error("String expected, but got a ", typeof s);
|
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_]/, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,28 +65,36 @@ export class ClientMessage {
|
|||||||
return this._arr[0];
|
return this._arr[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} msgData the raw text data in the websocket message.
|
* @param {string} msgData the raw text data in the websocket message.
|
||||||
*/
|
*/
|
||||||
constructor(msgData) {
|
constructor(msgData) {
|
||||||
if (typeof msgData !== "string") {
|
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);
|
throw new Error(
|
||||||
return
|
"Could not create client message. Attempting to parse json, but data was not even a string, it was a " +
|
||||||
|
typeof msgData,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._arr = JSON.parse(msgData);
|
this._arr = JSON.parse(msgData);
|
||||||
} catch (_) {
|
} 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") {
|
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) {
|
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;
|
this._arr = arr;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { randomBytes, pbkdf2Sync, randomInt } from 'node:crypto';
|
import { randomBytes, pbkdf2Sync, randomInt } from "node:crypto";
|
||||||
|
|
||||||
// Settings (tune as needed)
|
// Settings (tune as needed)
|
||||||
const ITERATIONS = 100_000; // Slow enough to deter brute force
|
const ITERATIONS = 100_000; // Slow enough to deter brute force
|
||||||
const KEYLEN = 64; // 512-bit hash
|
const KEYLEN = 64; // 512-bit hash
|
||||||
const DIGEST = 'sha512';
|
const DIGEST = "sha512";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a hash from a plaintext password.
|
* Generate a hash from a plaintext password.
|
||||||
@@ -11,8 +11,14 @@ const DIGEST = 'sha512';
|
|||||||
* @returns String
|
* @returns String
|
||||||
*/
|
*/
|
||||||
export function hash(password) {
|
export function hash(password) {
|
||||||
const salt = randomBytes(16).toString('hex'); // 128-bit salt
|
const salt = randomBytes(16).toString("hex"); // 128-bit salt
|
||||||
const hash = pbkdf2Sync(password, salt, ITERATIONS, KEYLEN, DIGEST).toString('hex');
|
const hash = pbkdf2Sync(
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
ITERATIONS,
|
||||||
|
KEYLEN,
|
||||||
|
DIGEST,
|
||||||
|
).toString("hex");
|
||||||
return `${ITERATIONS}:${salt}:${hash}`;
|
return `${ITERATIONS}:${salt}:${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +30,13 @@ export function hash(password) {
|
|||||||
* @returns Boolean
|
* @returns Boolean
|
||||||
*/
|
*/
|
||||||
export function verify(password, hashed_password) {
|
export function verify(password, hashed_password) {
|
||||||
const [iterations, salt, hash] = hashed_password.split(':');
|
const [iterations, salt, hash] = hashed_password.split(":");
|
||||||
const derived = pbkdf2Sync(password, salt, Number(iterations), KEYLEN, DIGEST).toString('hex');
|
const derived = pbkdf2Sync(
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
Number(iterations),
|
||||||
|
KEYLEN,
|
||||||
|
DIGEST,
|
||||||
|
).toString("hex");
|
||||||
return hash === derived;
|
return hash === derived;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user