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 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.
|
- 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
|
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.
|
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.
|
characters will be garbage collected.
|
||||||
|
|
||||||
The sum that needs paying while offline not very large though.
|
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 { isIdSane, miniUid } from "../utils/id.js";
|
||||||
|
import { Xorshift32 } from "../utils/random.js";
|
||||||
import { Character } from "./character.js";
|
import { Character } from "./character.js";
|
||||||
import { ItemAttributes, ItemBlueprint } from "./item.js";
|
import { ItemAttributes, ItemBlueprint } from "./item.js";
|
||||||
import { Player } from "./player.js";
|
import { Player } from "./player.js";
|
||||||
@@ -33,6 +34,24 @@ export class Game {
|
|||||||
*/
|
*/
|
||||||
_players = new Map();
|
_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) {
|
getPlayer(username) {
|
||||||
return this._players.get(username);
|
return this._players.get(username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"prettier": {
|
"prettier": {
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
|
"quoteProps": "consistent",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ export class CharacterSeeder {
|
|||||||
|
|
||||||
// Rolling skills
|
// Rolling skills
|
||||||
|
|
||||||
|
c.name =
|
||||||
|
this.game.rng.oneOf("sir", "madam", "mister", "miss", "", "", "") +
|
||||||
|
" random " +
|
||||||
|
this.game.rng.get().toString();
|
||||||
|
|
||||||
c.awareness = roll.d6() + 2;
|
c.awareness = roll.d6() + 2;
|
||||||
c.grit = roll.d6() + 2;
|
c.grit = roll.d6() + 2;
|
||||||
c.knowledge = roll.d6() + 2;
|
c.knowledge = roll.d6() + 2;
|
||||||
@@ -126,6 +131,10 @@ export class CharacterSeeder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.applyFoundation(c);
|
this.applyFoundation(c);
|
||||||
|
|
||||||
|
console.log(c);
|
||||||
|
|
||||||
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,7 +150,9 @@ export class CharacterSeeder {
|
|||||||
createParty(player, partySize) {
|
createParty(player, partySize) {
|
||||||
//
|
//
|
||||||
for (let i = 0; i < partySize; i++) {
|
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 {Character} c
|
||||||
* @param {string|number} Foundation to add to character
|
* @param {string|number} Foundation to add to character
|
||||||
*/
|
*/
|
||||||
applyFoundation(c, foundation = "random") {
|
applyFoundation(c, foundation = ":random") {
|
||||||
switch (foundation) {
|
switch (foundation) {
|
||||||
case "random":
|
case ":random":
|
||||||
return this.applyFoundation(c, roll.dice(3));
|
return this.applyFoundation(c, roll.dice(3));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -193,12 +204,12 @@ export class CharacterSeeder {
|
|||||||
c, //
|
c, //
|
||||||
":armor.light.leather",
|
":armor.light.leather",
|
||||||
":weapon.light.sickle",
|
":weapon.light.sickle",
|
||||||
":kits.poisoners_kit",
|
":kit.poisoners_kit",
|
||||||
":kits.healers_kit",
|
":kit.healers_kit",
|
||||||
);
|
);
|
||||||
this.addSkillsToCharacter(
|
this.addSkillsToCharacter(
|
||||||
c, //
|
c, //
|
||||||
":armor.light.leather",
|
":armor.light.sleather",
|
||||||
":armor.light.hide",
|
":armor.light.hide",
|
||||||
":weapon.light.sickle",
|
":weapon.light.sickle",
|
||||||
);
|
);
|
||||||
@@ -231,6 +242,7 @@ export class CharacterSeeder {
|
|||||||
":weapon.light.rapier",
|
":weapon.light.rapier",
|
||||||
":weapon.light.dagger",
|
":weapon.light.dagger",
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import { PlayerSeeder } from "./playerSeeder.js";
|
|||||||
*/
|
*/
|
||||||
export class GameSeeder {
|
export class GameSeeder {
|
||||||
/** @returns {Game} */
|
/** @returns {Game} */
|
||||||
createGame() {
|
createGame(rngSeed) {
|
||||||
/** @protected @constant @readonly @type {Game} */
|
/** @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.
|
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,
|
itemSlots: 0.5,
|
||||||
damage: 3,
|
damage: 3,
|
||||||
melee: true,
|
melee: true,
|
||||||
skills: [":weapon.light"],
|
|
||||||
ranged: true,
|
ranged: true,
|
||||||
specialEffect: ":effect.weapon.fast",
|
specialEffect: ":effect.weapon.fast",
|
||||||
});
|
});
|
||||||
@@ -41,7 +40,6 @@ export class ItemSeeder {
|
|||||||
description: "For cutting nuts, and branches",
|
description: "For cutting nuts, and branches",
|
||||||
itemSlots: 1,
|
itemSlots: 1,
|
||||||
damage: 4,
|
damage: 4,
|
||||||
skills: [":weapon.light"],
|
|
||||||
specialEffect: ":effect.weapon.sickle",
|
specialEffect: ":effect.weapon.sickle",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,11 +48,14 @@ export class ItemSeeder {
|
|||||||
description: "Spikes with gauntlets on them!",
|
description: "Spikes with gauntlets on them!",
|
||||||
itemSlots: 1,
|
itemSlots: 1,
|
||||||
damage: 5,
|
damage: 5,
|
||||||
skills: [
|
specialEffect: "TBD",
|
||||||
// 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",
|
this.game.addItemBlueprint(":weapon.light.rapier", {
|
||||||
],
|
name: "Rapier",
|
||||||
|
description: "Fancy musketeer sword",
|
||||||
|
itemSlots: 1,
|
||||||
|
damage: 5,
|
||||||
specialEffect: "TBD",
|
specialEffect: "TBD",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +71,6 @@ export class ItemSeeder {
|
|||||||
description: "Padded and hardened leather with metal stud reinforcement",
|
description: "Padded and hardened leather with metal stud reinforcement",
|
||||||
itemSlots: 3,
|
itemSlots: 3,
|
||||||
specialEffect: "TBD",
|
specialEffect: "TBD",
|
||||||
skills: [":armor.light"],
|
|
||||||
armorHitPoints: 10,
|
armorHitPoints: 10,
|
||||||
});
|
});
|
||||||
this.game.addItemBlueprint(":armor.light.leather", {
|
this.game.addItemBlueprint(":armor.light.leather", {
|
||||||
@@ -78,7 +78,6 @@ export class ItemSeeder {
|
|||||||
description: "Padded and hardened leather",
|
description: "Padded and hardened leather",
|
||||||
itemSlots: 2,
|
itemSlots: 2,
|
||||||
specialEffect: "TBD",
|
specialEffect: "TBD",
|
||||||
skills: [":armor.light"],
|
|
||||||
armorHitPoints: 6,
|
armorHitPoints: 6,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,20 @@ import { AuthState } from "./states/authState.js";
|
|||||||
import { GameSeeder } from "./seeders/gameSeeder.js";
|
import { GameSeeder } from "./seeders/gameSeeder.js";
|
||||||
import { Config } from "./config.js";
|
import { Config } from "./config.js";
|
||||||
|
|
||||||
|
// __ __ _ _ ____ ____
|
||||||
|
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
|
||||||
|
// | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__|
|
||||||
|
// | | | | |_| | |_| | ___) | __/ | \ V / __/ |
|
||||||
|
// |_| |_|\___/|____/ |____/ \___|_| \_/ \___|_|
|
||||||
|
// -----------------------------------------------------
|
||||||
class MudServer {
|
class MudServer {
|
||||||
constructor() {
|
/** @type {Xorshift32} */
|
||||||
|
rng;
|
||||||
|
|
||||||
|
/** @param {number?} rngSeed seed for the pseudo-random number generator. */
|
||||||
|
constructor(rngSeed = undefined) {
|
||||||
/** @type {Game} */
|
/** @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);
|
console.debug("incoming websocket message %s", data);
|
||||||
|
|
||||||
if (!session.state) {
|
if (!session.state) {
|
||||||
console.error(
|
console.error("we received a message, but don't even have a state. Zark!");
|
||||||
"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!?"));
|
||||||
);
|
|
||||||
websocket.send(
|
|
||||||
msg.prepare(msg.ERROR, "Oh no! I don't know what to do!?"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,36 +89,19 @@ class MudServer {
|
|||||||
//---------------------
|
//---------------------
|
||||||
// Set state = QuitState
|
// Set state = QuitState
|
||||||
//
|
//
|
||||||
websocket.send(
|
websocket.send(msg.prepare(msg.MESSAGE, "The quitting quitter quits... Typical. Cya!"));
|
||||||
msg.prepare(
|
|
||||||
msg.MESSAGE,
|
|
||||||
"The quitting quitter quits... Typical. Cya!",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
websocket.close();
|
websocket.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof session.state.onMessage !== "function") {
|
if (typeof session.state.onMessage !== "function") {
|
||||||
console.error(
|
console.error("we received a message, but we're not i a State to receive it");
|
||||||
"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."));
|
||||||
);
|
|
||||||
websocket.send(
|
|
||||||
msg.prepare(
|
|
||||||
msg.ERROR,
|
|
||||||
"Oh no! I don't know what to do with that message.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.state.onMessage(msgObj);
|
session.state.onMessage(msgObj);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.trace(
|
console.trace("received an invalid message (error: %s)", error, data.toString(), data);
|
||||||
"received an invalid message (error: %s)",
|
|
||||||
error,
|
|
||||||
data.toString(),
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
websocket.send(msg.prepare(msg.CALAMITY, error));
|
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
|
// Create HTTP server for serving the client - Consider moving to own file
|
||||||
const httpServer = http.createServer((req, res) => {
|
const httpServer = http.createServer((req, res) => {
|
||||||
let filePath = path.join(
|
let filePath = path.join("public", req.url === "/" ? "index.html" : req.url);
|
||||||
"public",
|
|
||||||
req.url === "/" ? "index.html" : req.url,
|
|
||||||
);
|
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
const contentType = contentTypes[ext];
|
const contentType = contentTypes[ext];
|
||||||
|
|
||||||
@@ -191,9 +177,9 @@ class MudServer {
|
|||||||
// | \/ | / \ |_ _| \ | |
|
// | \/ | / \ |_ _| \ | |
|
||||||
// | |\/| | / _ \ | || \| |
|
// | |\/| | / _ \ | || \| |
|
||||||
// | | | |/ ___ \ | || |\ |
|
// | | | |/ ___ \ | || |\ |
|
||||||
// |_| |_/_/ \_\___|_| \_|
|
// |_| |_/_/ \_\___|_| \_| A
|
||||||
//---------------------------
|
//---------------------------
|
||||||
// Code entry point
|
// Code entry point
|
||||||
//-----------------
|
//-----------------
|
||||||
const mudserver = new MudServer();
|
const mudserver = new MudServer(/* location of crypto key for saving games */);
|
||||||
mudserver.start();
|
mudserver.start();
|
||||||
|
|||||||
@@ -12,24 +12,24 @@ export class JustLoggedInState {
|
|||||||
|
|
||||||
// Show welcome screen
|
// Show welcome screen
|
||||||
onAttach() {
|
onAttach() {
|
||||||
this.session.sendMessage([
|
this.session.sendMessage(["", "Welcome", "", "You can type “:quit” at any time to quit the game", ""]);
|
||||||
"",
|
|
||||||
"Welcome",
|
|
||||||
"",
|
|
||||||
"You can type “:quit” at any time to quit the game",
|
|
||||||
"",
|
|
||||||
]);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Check if we need to create characters for the player
|
// Check if we need to create characters for the player
|
||||||
if (this.session.player.characters.size === 0) {
|
if (this.session.player.characters.size === 0) {
|
||||||
this.session.sendMessage(
|
this.session.sendMessage("You haven't got any characters, so let's make some\n\n");
|
||||||
"You haven't got any characters, so let's make some\n\n",
|
|
||||||
);
|
|
||||||
this.session.setState(new PartyCreationState(this.session));
|
this.session.setState(new PartyCreationState(this.session));
|
||||||
return;
|
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));
|
this.session.setState(new AwaitCommandsState(this.session));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
const UID_DIGITS = 12;
|
import * as regex from "./regex.js";
|
||||||
const MINI_UID_REGEX = /\.uid\.[a-z0-9]{6,}$/;
|
|
||||||
const ID_SANITY_REGEX = /^:([a-z0-9]+\.)*[a-z0-9_]+$/;
|
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.
|
* Sanity check a string to see if it is a potential id.
|
||||||
@@ -17,17 +25,22 @@ export function isIdSane(id) {
|
|||||||
return false;
|
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.
|
* Generate a random number, convert it to base36, and return it as a string with 7-8 characters.
|
||||||
*/
|
*/
|
||||||
export function miniUid() {
|
export function miniUid() {
|
||||||
// we use 12 digits, but we could go up to 16
|
// we use 12 digits, but we could go all the way to 16
|
||||||
return Number(Math.random().toFixed(UID_DIGITS).substring(2)).toString(36);
|
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