thinstuff
This commit is contained in:
0
server/find-element-width-in-chars.html → find-element-width-in-chars.html
Normal file → Executable file
0
server/find-element-width-in-chars.html → find-element-width-in-chars.html
Normal file → Executable file
@@ -21,7 +21,7 @@ They never trigger quests or events where they go, but:
|
||||
- they can interact with adventurers (they are quite aggressive, and may attack unprovoked, maybe sneak past)
|
||||
- they can interact with each other, but mostly do so if there are PCs nearby.
|
||||
|
||||
# Attrition
|
||||
# ATTRITION
|
||||
|
||||
Even when a player is offline, their characters have to pay rent on their homes
|
||||
or room and board in an inn, or they can chance it in the wilderness.
|
||||
@@ -30,3 +30,20 @@ If they run out of money or rations, there is a small chance each day that the
|
||||
characters will be garbage collected.
|
||||
|
||||
The sum that needs paying while offline not very large though.
|
||||
|
||||
# CHAT SPELL
|
||||
|
||||
You can buy a spell that lets you initiate a secure and encrypted group chat
|
||||
with other players.
|
||||
|
||||
The person who casts the spell generates a private key and sends the public key
|
||||
to the others in the chat. Each recipient then generates a one-time symmetric
|
||||
key and sends it securely (via the caster's public key) to the caster. The
|
||||
caster then generates a "group chat key" and sends it to each recipient via
|
||||
their one-time key.
|
||||
|
||||
Any chats via the spell from then on is encrypted with the "group chat key".
|
||||
|
||||
All parties throw away the group chat key when the spell ends.
|
||||
|
||||
Each group chat has a name.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { isIdSane, miniUid } from "../utils/id.js";
|
||||
import { Xorshift32 } from "../utils/random.js";
|
||||
import { Character } from "./character.js";
|
||||
import { ItemAttributes, ItemBlueprint } from "./item.js";
|
||||
import { Player } from "./player.js";
|
||||
@@ -33,6 +34,24 @@ export class Game {
|
||||
*/
|
||||
_players = new Map();
|
||||
|
||||
|
||||
/** @protected @type {Xorshift32} */
|
||||
_rng;
|
||||
|
||||
/** @type {Xorshift32} */
|
||||
get rng() {
|
||||
return this._rng;
|
||||
}
|
||||
|
||||
/** @param {number} rngSeed Seed number used for randomization */
|
||||
constructor(rngSeed) {
|
||||
if (!Number.isInteger(rngSeed)) {
|
||||
throw new Error("rngSeed must be an integer");
|
||||
}
|
||||
|
||||
this._rng = new Xorshift32(rngSeed);
|
||||
}
|
||||
|
||||
getPlayer(username) {
|
||||
return this._players.get(username);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"quoteProps": "consistent",
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
|
||||
@@ -73,6 +73,11 @@ export class CharacterSeeder {
|
||||
|
||||
// Rolling skills
|
||||
|
||||
c.name =
|
||||
this.game.rng.oneOf("sir", "madam", "mister", "miss", "", "", "") +
|
||||
" random " +
|
||||
this.game.rng.get().toString();
|
||||
|
||||
c.awareness = roll.d6() + 2;
|
||||
c.grit = roll.d6() + 2;
|
||||
c.knowledge = roll.d6() + 2;
|
||||
@@ -126,6 +131,10 @@ export class CharacterSeeder {
|
||||
}
|
||||
|
||||
this.applyFoundation(c);
|
||||
|
||||
console.log(c);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +150,9 @@ export class CharacterSeeder {
|
||||
createParty(player, partySize) {
|
||||
//
|
||||
for (let i = 0; i < partySize; i++) {
|
||||
const character = this.createCharacter(player);
|
||||
player.addCharacter(
|
||||
this.createCharacter(player), //
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,9 +160,9 @@ export class CharacterSeeder {
|
||||
* @param {Character} c
|
||||
* @param {string|number} Foundation to add to character
|
||||
*/
|
||||
applyFoundation(c, foundation = "random") {
|
||||
applyFoundation(c, foundation = ":random") {
|
||||
switch (foundation) {
|
||||
case "random":
|
||||
case ":random":
|
||||
return this.applyFoundation(c, roll.dice(3));
|
||||
break;
|
||||
|
||||
@@ -193,12 +204,12 @@ export class CharacterSeeder {
|
||||
c, //
|
||||
":armor.light.leather",
|
||||
":weapon.light.sickle",
|
||||
":kits.poisoners_kit",
|
||||
":kits.healers_kit",
|
||||
":kit.poisoners_kit",
|
||||
":kit.healers_kit",
|
||||
);
|
||||
this.addSkillsToCharacter(
|
||||
c, //
|
||||
":armor.light.leather",
|
||||
":armor.light.sleather",
|
||||
":armor.light.hide",
|
||||
":weapon.light.sickle",
|
||||
);
|
||||
@@ -231,6 +242,7 @@ export class CharacterSeeder {
|
||||
":weapon.light.rapier",
|
||||
":weapon.light.dagger",
|
||||
);
|
||||
break;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import { PlayerSeeder } from "./playerSeeder.js";
|
||||
*/
|
||||
export class GameSeeder {
|
||||
/** @returns {Game} */
|
||||
createGame() {
|
||||
createGame(rngSeed) {
|
||||
/** @protected @constant @readonly @type {Game} */
|
||||
this.game = new Game();
|
||||
this.game = new Game(rngSeed);
|
||||
|
||||
this.work(); // Seeding may take a bit, so let's defer it so we can return early.
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export class ItemSeeder {
|
||||
itemSlots: 0.5,
|
||||
damage: 3,
|
||||
melee: true,
|
||||
skills: [":weapon.light"],
|
||||
ranged: true,
|
||||
specialEffect: ":effect.weapon.fast",
|
||||
});
|
||||
@@ -41,7 +40,6 @@ export class ItemSeeder {
|
||||
description: "For cutting nuts, and branches",
|
||||
itemSlots: 1,
|
||||
damage: 4,
|
||||
skills: [":weapon.light"],
|
||||
specialEffect: ":effect.weapon.sickle",
|
||||
});
|
||||
|
||||
@@ -50,11 +48,14 @@ export class ItemSeeder {
|
||||
description: "Spikes with gauntlets on them!",
|
||||
itemSlots: 1,
|
||||
damage: 5,
|
||||
skills: [
|
||||
// Spiked gauntlets are :Weird so you must be specially trained to use them.
|
||||
// This is done by having a skill that exactly matches the weapon's blueprintId
|
||||
":weapon.weird.spiked_gauntlets",
|
||||
],
|
||||
specialEffect: "TBD",
|
||||
});
|
||||
|
||||
this.game.addItemBlueprint(":weapon.light.rapier", {
|
||||
name: "Rapier",
|
||||
description: "Fancy musketeer sword",
|
||||
itemSlots: 1,
|
||||
damage: 5,
|
||||
specialEffect: "TBD",
|
||||
});
|
||||
|
||||
@@ -70,7 +71,6 @@ export class ItemSeeder {
|
||||
description: "Padded and hardened leather with metal stud reinforcement",
|
||||
itemSlots: 3,
|
||||
specialEffect: "TBD",
|
||||
skills: [":armor.light"],
|
||||
armorHitPoints: 10,
|
||||
});
|
||||
this.game.addItemBlueprint(":armor.light.leather", {
|
||||
@@ -78,7 +78,6 @@ export class ItemSeeder {
|
||||
description: "Padded and hardened leather",
|
||||
itemSlots: 2,
|
||||
specialEffect: "TBD",
|
||||
skills: [":armor.light"],
|
||||
armorHitPoints: 6,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,10 +9,20 @@ import { AuthState } from "./states/authState.js";
|
||||
import { GameSeeder } from "./seeders/gameSeeder.js";
|
||||
import { Config } from "./config.js";
|
||||
|
||||
// __ __ _ _ ____ ____
|
||||
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
|
||||
// | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__|
|
||||
// | | | | |_| | |_| | ___) | __/ | \ V / __/ |
|
||||
// |_| |_|\___/|____/ |____/ \___|_| \_/ \___|_|
|
||||
// -----------------------------------------------------
|
||||
class MudServer {
|
||||
constructor() {
|
||||
/** @type {Xorshift32} */
|
||||
rng;
|
||||
|
||||
/** @param {number?} rngSeed seed for the pseudo-random number generator. */
|
||||
constructor(rngSeed = undefined) {
|
||||
/** @type {Game} */
|
||||
this.game = new GameSeeder().createGame();
|
||||
this.game = new GameSeeder().createGame(rngSeed || Date.now());
|
||||
}
|
||||
|
||||
// ____ ___ _ _ _ _ _____ ____ _____ _____ ____
|
||||
@@ -66,12 +76,8 @@ class MudServer {
|
||||
console.debug("incoming websocket message %s", data);
|
||||
|
||||
if (!session.state) {
|
||||
console.error(
|
||||
"we received a message, but don't even have a state. Zark!",
|
||||
);
|
||||
websocket.send(
|
||||
msg.prepare(msg.ERROR, "Oh no! I don't know what to do!?"),
|
||||
);
|
||||
console.error("we received a message, but don't even have a state. Zark!");
|
||||
websocket.send(msg.prepare(msg.ERROR, "Oh no! I don't know what to do!?"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,36 +89,19 @@ class MudServer {
|
||||
//---------------------
|
||||
// Set state = QuitState
|
||||
//
|
||||
websocket.send(
|
||||
msg.prepare(
|
||||
msg.MESSAGE,
|
||||
"The quitting quitter quits... Typical. Cya!",
|
||||
),
|
||||
);
|
||||
websocket.send(msg.prepare(msg.MESSAGE, "The quitting quitter quits... Typical. Cya!"));
|
||||
websocket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof session.state.onMessage !== "function") {
|
||||
console.error(
|
||||
"we received a message, but we're not i a State to receive it",
|
||||
);
|
||||
websocket.send(
|
||||
msg.prepare(
|
||||
msg.ERROR,
|
||||
"Oh no! I don't know what to do with that message.",
|
||||
),
|
||||
);
|
||||
console.error("we received a message, but we're not i a State to receive it");
|
||||
websocket.send(msg.prepare(msg.ERROR, "Oh no! I don't know what to do with that message."));
|
||||
return;
|
||||
}
|
||||
session.state.onMessage(msgObj);
|
||||
} catch (error) {
|
||||
console.trace(
|
||||
"received an invalid message (error: %s)",
|
||||
error,
|
||||
data.toString(),
|
||||
data,
|
||||
);
|
||||
console.trace("received an invalid message (error: %s)", error, data.toString(), data);
|
||||
websocket.send(msg.prepare(msg.CALAMITY, error));
|
||||
}
|
||||
});
|
||||
@@ -140,10 +129,7 @@ class MudServer {
|
||||
//
|
||||
// Create HTTP server for serving the client - Consider moving to own file
|
||||
const httpServer = http.createServer((req, res) => {
|
||||
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 contentType = contentTypes[ext];
|
||||
|
||||
@@ -191,9 +177,9 @@ class MudServer {
|
||||
// | \/ | / \ |_ _| \ | |
|
||||
// | |\/| | / _ \ | || \| |
|
||||
// | | | |/ ___ \ | || |\ |
|
||||
// |_| |_/_/ \_\___|_| \_|
|
||||
// |_| |_/_/ \_\___|_| \_| A
|
||||
//---------------------------
|
||||
// Code entry point
|
||||
//-----------------
|
||||
const mudserver = new MudServer();
|
||||
const mudserver = new MudServer(/* location of crypto key for saving games */);
|
||||
mudserver.start();
|
||||
|
||||
@@ -12,24 +12,24 @@ export class JustLoggedInState {
|
||||
|
||||
// Show welcome screen
|
||||
onAttach() {
|
||||
this.session.sendMessage([
|
||||
"",
|
||||
"Welcome",
|
||||
"",
|
||||
"You can type “:quit” at any time to quit the game",
|
||||
"",
|
||||
]);
|
||||
this.session.sendMessage(["", "Welcome", "", "You can type “:quit” at any time to quit the game", ""]);
|
||||
|
||||
//
|
||||
// Check if we need to create characters for the player
|
||||
if (this.session.player.characters.size === 0) {
|
||||
this.session.sendMessage(
|
||||
"You haven't got any characters, so let's make some\n\n",
|
||||
);
|
||||
this.session.sendMessage("You haven't got any characters, so let's make some\n\n");
|
||||
this.session.setState(new PartyCreationState(this.session));
|
||||
return;
|
||||
}
|
||||
|
||||
const replacer = (key, value) => {
|
||||
if (value instanceof Set) {
|
||||
return [...value]; // turn Set into array
|
||||
}
|
||||
return value;
|
||||
}
|
||||
this.session.sendMessage(JSON.stringify(this.session.player.characters.entries(), replacer, "\t"));
|
||||
|
||||
this.session.setState(new AwaitCommandsState(this.session));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
const UID_DIGITS = 12;
|
||||
const MINI_UID_REGEX = /\.uid\.[a-z0-9]{6,}$/;
|
||||
const ID_SANITY_REGEX = /^:([a-z0-9]+\.)*[a-z0-9_]+$/;
|
||||
import * as regex from "./regex.js";
|
||||
|
||||
const MINI_UID_REGEX = regex.pretty(
|
||||
"\.uid\.", // Mini-uids always begin with ".uid."
|
||||
"[a-z0-9]{6,}$", // Terminated by 6 or more random numbers and lowercase letters.
|
||||
);
|
||||
const ID_SANITY_REGEX = regex.pretty(
|
||||
"^:", // All ids start with a colon
|
||||
"([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.
|
||||
);
|
||||
|
||||
/**
|
||||
* Sanity check a string to see if it is a potential id.
|
||||
@@ -17,17 +25,22 @@ export function isIdSane(id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ID_SANITY_REGEX.test(id);
|
||||
if (!ID_SANITY_REGEX.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} crypto-unsafe pseudo random number.
|
||||
* @returns {string} crypto-unsafe pseudo random numbe"r.
|
||||
*
|
||||
* Generate a random number, convert it to base36, and return it as a string with 7-8 characters.
|
||||
*/
|
||||
export function miniUid() {
|
||||
// we use 12 digits, but we could go up to 16
|
||||
return Number(Math.random().toFixed(UID_DIGITS).substring(2)).toString(36);
|
||||
// we use 12 digits, but we could go all the way to 16
|
||||
const digits = 12;
|
||||
return Number(Math.random().toFixed(digits).substring(2)).toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
80
server/utils/random.js
Executable file
80
server/utils/random.js
Executable file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Pseudo random number generator
|
||||
* using the xorshift32 method.
|
||||
*/
|
||||
export class Xorshift32 {
|
||||
/* @type {number} */
|
||||
state;
|
||||
|
||||
/** @param {number} seed */
|
||||
constructor(seed) {
|
||||
this.state = seed | 0;
|
||||
}
|
||||
|
||||
/** @protected Shuffle the internal state. */
|
||||
shuffle() {
|
||||
//
|
||||
// Run the actual xorshift32 algorithm
|
||||
let x = this.state;
|
||||
x ^= x << 13;
|
||||
x ^= x >>> 17;
|
||||
x ^= x << 5;
|
||||
x = (x >>> 0) / 4294967296;
|
||||
|
||||
this.state = x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random number and shuffle the internal state.
|
||||
* @returns {number} a pseudo-random positive integer.
|
||||
*/
|
||||
get() {
|
||||
this.shuffle();
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/** @param {number} x @returns {number} a positive integer lower than x */
|
||||
lowerThan(x) {
|
||||
return this.get() % x;
|
||||
}
|
||||
|
||||
/** @param {number} x @reurns {number} a positive integer lower than or equal to x */
|
||||
lowerThanOrEqual(x) {
|
||||
return this.get() % (x + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {<T>[]} arr
|
||||
*
|
||||
* @return {<T>}
|
||||
*/
|
||||
randomElement(arr) {
|
||||
const idx = this.lowerThan(arr.length);
|
||||
|
||||
return arr[idx];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...<T>} args
|
||||
* @returns {<T>}
|
||||
*/
|
||||
oneOf(...args) {
|
||||
const idx = this.lowerThan(args.length);
|
||||
|
||||
return args[idx];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} lowerThanOrEqual a positive integer
|
||||
* @param {number} greaterThanOrEqual a positive integer greater than lowerThanOrEqual
|
||||
* @returns {number} a pseudo-random integer
|
||||
*/
|
||||
within(greaterThanOrEqual, lowerThanOrEqual) {
|
||||
const range = lowerThanOrEqual - greaterThanOrEqual;
|
||||
|
||||
const num = this.lowerThanOrEqual(range);
|
||||
|
||||
return num + greaterThanOrEqual;
|
||||
}
|
||||
}
|
||||
|
||||
10
server/utils/regex.js
Normal file
10
server/utils/regex.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Makes it easier to document regexes because you can break them up
|
||||
*
|
||||
* @param {...string} args
|
||||
* @returns {Regexp}
|
||||
*/
|
||||
export function pretty(...args) {
|
||||
const regexprStr = args.join("");
|
||||
return new RegExp(regexprStr);
|
||||
}
|
||||
0
server/tui.md → tui.md
Normal file → Executable file
0
server/tui.md → tui.md
Normal file → Executable file
Reference in New Issue
Block a user