thinstuff
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
<!-- CHATGPT EXAMPLE -->
|
||||
<!-- 1. Get the width of the container (in pixels). -->
|
||||
<!-- 2. Get the width of a single character in the monospaced font (this can be done by creating a temporary element with the same font and measuring its width).-->
|
||||
<!-- 3. Divide the container's width by the character's width to get the number of characters. -->
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Measure Div Width in Characters</title>
|
||||
<style>
|
||||
.monospaced-div {
|
||||
font-family: "Courier New", Courier, monospace; /* Monospaced font */
|
||||
width: 360px; /* Example width in pixels */
|
||||
border: 1px solid #333;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="monospaced-div">This is a div with monospaced text.</div>
|
||||
|
||||
<script>
|
||||
function getMonospacedCharCount(div) {
|
||||
// Create a temporary span to get the width of one character
|
||||
const testChar = document.createElement("span");
|
||||
testChar.textContent = "0"; // Monospaced fonts use "0" for width
|
||||
testChar.style.fontFamily = window.getComputedStyle(div).fontFamily;
|
||||
testChar.style.visibility = "hidden"; // Hide the element
|
||||
document.body.appendChild(testChar);
|
||||
|
||||
const charWidth = testChar.offsetWidth; // Get width of a single character
|
||||
document.body.removeChild(testChar); // Remove the test element
|
||||
|
||||
// Get the width of the div and calculate how many characters fit
|
||||
const divWidth = div.offsetWidth;
|
||||
|
||||
// Return the number of characters that fit in the div width
|
||||
return Math.floor(divWidth / charWidth);
|
||||
}
|
||||
|
||||
const div = document.querySelector(".monospaced-div");
|
||||
const charCount = getMonospacedCharCount(div);
|
||||
console.log("Number of characters the div can hold:", charCount);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
310
server/server.js
310
server/server.js
@@ -9,191 +9,177 @@ import { AuthState } from "./states/authState.js";
|
||||
import { GameSeeder } from "./seeders/gameSeeder.js";
|
||||
import { Config } from "./config.js";
|
||||
|
||||
// __ __ _ _ ____ ____
|
||||
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
|
||||
// | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__|
|
||||
// | | | | |_| | |_| | ___) | __/ | \ V / __/ |
|
||||
// |_| |_|\___/|____/ |____/ \___|_| \_/ \___|_|
|
||||
// -----------------------------------------------------
|
||||
class MudServer {
|
||||
constructor() {
|
||||
/** @type {Game} */
|
||||
this.game = new GameSeeder().createGame();
|
||||
}
|
||||
/** @type {Xorshift32} */
|
||||
rng;
|
||||
|
||||
// ____ ___ _ _ _ _ _____ ____ _____ _____ ____
|
||||
// / ___/ _ \| \ | | \ | | ____/ ___|_ _| ____| _ \
|
||||
// | | | | | | \| | \| | _|| | | | | _| | | | |
|
||||
// | |__| |_| | |\ | |\ | |__| |___ | | | |___| |_| |
|
||||
// \____\___/|_| \_|_| \_|_____\____| |_| |_____|____/
|
||||
//------------------------------------------------------
|
||||
// Handle New Socket Connections
|
||||
//------------------------------
|
||||
/** @param {WebSocket} websocket */
|
||||
onConnectionEstabished(websocket) {
|
||||
console.log("New connection established");
|
||||
const session = new Session(websocket, this.game);
|
||||
session.sendSystemMessage("dev", true);
|
||||
/** @param {number?} rngSeed seed for the pseudo-random number generator. */
|
||||
constructor(rngSeed = undefined) {
|
||||
/** @type {Game} */
|
||||
this.game = new GameSeeder().createGame(rngSeed || Date.now());
|
||||
}
|
||||
|
||||
// ____ _ ___ ____ _____
|
||||
// / ___| | / _ \/ ___|| ____|
|
||||
// | | | | | | | \___ \| _|
|
||||
// | |___| |__| |_| |___) | |___
|
||||
// \____|_____\___/|____/|_____|
|
||||
//-------------------------------
|
||||
// Handle Socket Closing
|
||||
//----------------------
|
||||
websocket.on("close", () => {
|
||||
if (!session.player) {
|
||||
console.info("A player without a session disconnected");
|
||||
return;
|
||||
}
|
||||
//-------------
|
||||
// TODO
|
||||
//-------------
|
||||
// Handle player logout (move the or hide their characters)
|
||||
//
|
||||
// Maybe session.onConnectionClosed() that calls session._state.onConnectionClosed()
|
||||
// Maybe this.setState(new ConnectionClosedState());
|
||||
// Maybe both ??
|
||||
console.log(`Player ${session.player.username} disconnected`);
|
||||
});
|
||||
// ____ ___ _ _ _ _ _____ ____ _____ _____ ____
|
||||
// / ___/ _ \| \ | | \ | | ____/ ___|_ _| ____| _ \
|
||||
// | | | | | | \| | \| | _|| | | | | _| | | | |
|
||||
// | |__| |_| | |\ | |\ | |__| |___ | | | |___| |_| |
|
||||
// \____\___/|_| \_|_| \_|_____\____| |_| |_____|____/
|
||||
//------------------------------------------------------
|
||||
// Handle New Socket Connections
|
||||
//------------------------------
|
||||
/** @param {WebSocket} websocket */
|
||||
onConnectionEstabished(websocket) {
|
||||
console.log("New connection established");
|
||||
const session = new Session(websocket, this.game);
|
||||
session.sendSystemMessage("dev", true);
|
||||
|
||||
// __ __ _____ ____ ____ _ ____ _____
|
||||
// | \/ | ____/ ___/ ___| / \ / ___| ____|
|
||||
// | |\/| | _| \___ \___ \ / _ \| | _| _|
|
||||
// | | | | |___ ___) |__) / ___ \ |_| | |___
|
||||
// |_| |_|_____|____/____/_/ \_\____|_____|
|
||||
//--------------------------------------------
|
||||
// HANDLE INCOMING MESSAGES
|
||||
//-------------------------
|
||||
websocket.on("message", (data) => {
|
||||
try {
|
||||
console.debug("incoming websocket message %s", data);
|
||||
// ____ _ ___ ____ _____
|
||||
// / ___| | / _ \/ ___|| ____|
|
||||
// | | | | | | | \___ \| _|
|
||||
// | |___| |__| |_| |___) | |___
|
||||
// \____|_____\___/|____/|_____|
|
||||
//-------------------------------
|
||||
// Handle Socket Closing
|
||||
//----------------------
|
||||
websocket.on("close", () => {
|
||||
if (!session.player) {
|
||||
console.info("A player without a session disconnected");
|
||||
return;
|
||||
}
|
||||
//-------------
|
||||
// TODO
|
||||
//-------------
|
||||
// Handle player logout (move the or hide their characters)
|
||||
//
|
||||
// Maybe session.onConnectionClosed() that calls session._state.onConnectionClosed()
|
||||
// Maybe this.setState(new ConnectionClosedState());
|
||||
// Maybe both ??
|
||||
console.log(`Player ${session.player.username} disconnected`);
|
||||
});
|
||||
|
||||
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!?"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// __ __ _____ ____ ____ _ ____ _____
|
||||
// | \/ | ____/ ___/ ___| / \ / ___| ____|
|
||||
// | |\/| | _| \___ \___ \ / _ \| | _| _|
|
||||
// | | | | |___ ___) |__) / ___ \ |_| | |___
|
||||
// |_| |_|_____|____/____/_/ \_\____|_____|
|
||||
//--------------------------------------------
|
||||
// HANDLE INCOMING MESSAGES
|
||||
//-------------------------
|
||||
websocket.on("message", (data) => {
|
||||
try {
|
||||
console.debug("incoming websocket message %s", data);
|
||||
|
||||
const msgObj = new msg.ClientMessage(data.toString());
|
||||
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!?"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (msgObj.isQuitCommand()) {
|
||||
//---------------------
|
||||
// TODO TODO TODO TODO
|
||||
//---------------------
|
||||
// Set state = QuitState
|
||||
//
|
||||
websocket.send(
|
||||
msg.prepare(
|
||||
msg.MESSAGE,
|
||||
"The quitting quitter quits... Typical. Cya!",
|
||||
),
|
||||
);
|
||||
websocket.close();
|
||||
return;
|
||||
}
|
||||
const msgObj = new msg.ClientMessage(data.toString());
|
||||
|
||||
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.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
session.state.onMessage(msgObj);
|
||||
} catch (error) {
|
||||
console.trace(
|
||||
"received an invalid message (error: %s)",
|
||||
error,
|
||||
data.toString(),
|
||||
data,
|
||||
);
|
||||
websocket.send(msg.prepare(msg.CALAMITY, error));
|
||||
}
|
||||
});
|
||||
if (msgObj.isQuitCommand()) {
|
||||
//---------------------
|
||||
// TODO TODO TODO TODO
|
||||
//---------------------
|
||||
// Set state = QuitState
|
||||
//
|
||||
websocket.send(msg.prepare(msg.MESSAGE, "The quitting quitter quits... Typical. Cya!"));
|
||||
websocket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
session.setState(new AuthState(session));
|
||||
}
|
||||
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."));
|
||||
return;
|
||||
}
|
||||
session.state.onMessage(msgObj);
|
||||
} catch (error) {
|
||||
console.trace("received an invalid message (error: %s)", error, data.toString(), data);
|
||||
websocket.send(msg.prepare(msg.CALAMITY, error));
|
||||
}
|
||||
});
|
||||
|
||||
// ____ _____ _ ____ _____
|
||||
// / ___|_ _|/ \ | _ \_ _|
|
||||
// \___ \ | | / _ \ | |_) || |
|
||||
// ___) || |/ ___ \| _ < | |
|
||||
// |____/ |_/_/ \_\_| \_\|_|
|
||||
//-----------------------------
|
||||
// Start the server
|
||||
//-----------------
|
||||
start() {
|
||||
//
|
||||
// The file types we allow to be served.
|
||||
const contentTypes = {
|
||||
".js": "application/javascript",
|
||||
".css": "text/css",
|
||||
".html": "text/html",
|
||||
};
|
||||
session.setState(new AuthState(session));
|
||||
}
|
||||
|
||||
//
|
||||
// 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,
|
||||
);
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = contentTypes[ext];
|
||||
// ____ _____ _ ____ _____
|
||||
// / ___|_ _|/ \ | _ \_ _|
|
||||
// \___ \ | | / _ \ | |_) || |
|
||||
// ___) || |/ ___ \| _ < | |
|
||||
// |____/ |_/_/ \_\_| \_\|_|
|
||||
//-----------------------------
|
||||
// Start the server
|
||||
//-----------------
|
||||
start() {
|
||||
//
|
||||
// The file types we allow to be served.
|
||||
const contentTypes = {
|
||||
".js": "application/javascript",
|
||||
".css": "text/css",
|
||||
".html": "text/html",
|
||||
};
|
||||
|
||||
//
|
||||
// Check if the requested file has a legal file type.
|
||||
if (!contentType) {
|
||||
// Invalid file, pretend it did not exist!
|
||||
res.writeHead(404);
|
||||
res.end(`File not found`);
|
||||
console.log("Bad http request", req.url);
|
||||
return;
|
||||
}
|
||||
//
|
||||
// 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);
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = contentTypes[ext];
|
||||
|
||||
//
|
||||
// Check if the file exists.
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end(`File not found`);
|
||||
console.log("Bad http request", req.url);
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "Content-Type": contentType });
|
||||
res.end(data);
|
||||
});
|
||||
});
|
||||
//
|
||||
// Check if the requested file has a legal file type.
|
||||
if (!contentType) {
|
||||
// Invalid file, pretend it did not exist!
|
||||
res.writeHead(404);
|
||||
res.end(`File not found`);
|
||||
console.log("Bad http request", req.url);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Create WebSocket server
|
||||
const websocketServer = new WebSocketServer({ server: httpServer });
|
||||
//
|
||||
// Check if the file exists.
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end(`File not found`);
|
||||
console.log("Bad http request", req.url);
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "Content-Type": contentType });
|
||||
res.end(data);
|
||||
});
|
||||
});
|
||||
|
||||
websocketServer.on("connection", (ws) => {
|
||||
this.onConnectionEstabished(ws);
|
||||
});
|
||||
//
|
||||
// Create WebSocket server
|
||||
const websocketServer = new WebSocketServer({ server: httpServer });
|
||||
|
||||
console.info(`running environment: ${Config.env}`);
|
||||
httpServer.listen(Config.port, () => {
|
||||
console.log(`NUUHD server running on port ${Config.port}`);
|
||||
console.log(`WebSocket server ready for connections`);
|
||||
});
|
||||
}
|
||||
websocketServer.on("connection", (ws) => {
|
||||
this.onConnectionEstabished(ws);
|
||||
});
|
||||
|
||||
console.info(`running environment: ${Config.env}`);
|
||||
httpServer.listen(Config.port, () => {
|
||||
console.log(`NUUHD server running on port ${Config.port}`);
|
||||
console.log(`WebSocket server ready for connections`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// __ __ _ ___ _ _
|
||||
// | \/ | / \ |_ _| \ | |
|
||||
// | |\/| | / _ \ | || \| |
|
||||
// | | | |/ ___ \ | || |\ |
|
||||
// |_| |_/_/ \_\___|_| \_|
|
||||
// |_| |_/_/ \_\___|_| \_| A
|
||||
//---------------------------
|
||||
// Code entry point
|
||||
//-----------------
|
||||
const mudserver = new MudServer();
|
||||
const mudserver = new MudServer(/* location of crypto key for saving games */);
|
||||
mudserver.start();
|
||||
|
||||
@@ -4,32 +4,32 @@ import { AwaitCommandsState } from "./awaitCommands.js";
|
||||
|
||||
/** @interface */
|
||||
export class JustLoggedInState {
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
// Show welcome screen
|
||||
onAttach() {
|
||||
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.setState(new PartyCreationState(this.session));
|
||||
return;
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
this.session.setState(new AwaitCommandsState(this.session));
|
||||
}
|
||||
// Show welcome screen
|
||||
onAttach() {
|
||||
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.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));
|
||||
}
|
||||
}
|
||||
|
||||
191
server/tui.md
191
server/tui.md
@@ -1,191 +0,0 @@
|
||||
Here are some ASCII and UTF-8 characters commonly used for "shading" effects in text art or terminal displays. These characters provide varying levels of density or shading:
|
||||
|
||||
### ASCII Shading Characters
|
||||
|
||||
These are basic ASCII characters often used for shading:
|
||||
|
||||
```
|
||||
Light shade: ░ (U+2591)
|
||||
Medium shade: ▒ (U+2592)
|
||||
Dark shade: ▓ (U+2593)
|
||||
Full block: █ (U+2588)
|
||||
Half block: ▄ (U+2584), ▀ (U+2580)
|
||||
```
|
||||
|
||||
### Additional UTF-8 Block Characters
|
||||
|
||||
These Unicode characters offer more granular shading or block patterns:
|
||||
|
||||
```
|
||||
Light block: ░ (U+2591)
|
||||
Medium block: ▒ (U+2592)
|
||||
Dark block: ▓ (U+2593)
|
||||
Solid block: █ (U+2588)
|
||||
Upper half block: ▀ (U+2580)
|
||||
Lower half block: ▄ (U+2584)
|
||||
Left half block: ▌ (U+258C)
|
||||
Right half block: ▐ (U+2590)
|
||||
Quadrant blocks: ▖ (U+2596), ▗ (U+2597), ▘ (U+2598), ▝ (U+259D)
|
||||
Checkerboard: ▚ (U+259A), ▞ (U+259E)
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
|
||||
Here’s an example of a simple shading gradient using some of these characters:
|
||||
|
||||
```
|
||||
Light to Dark: ░ ▒ ▓ █
|
||||
Half blocks: ▀ ▄ ▌ ▐
|
||||
Quadrant pattern: ▖ ▗ ▘ ▝
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Not all terminals or text editors display Unicode characters consistently, so ASCII characters like `.:;#+` are sometimes used for basic shading in simpler environments.
|
||||
- If you want to create specific patterns or need more complex ASCII art, let me know, and I can generate or suggest more detailed designs!
|
||||
- If you meant something specific by "shading" (e.g., for a particular programming context or art style), please clarify, and I’ll tailor the response further.
|
||||
|
||||
Below is a collection of ASCII and UTF-8 characters suitable for creating frames, borders, or "windows" in a text-based user interface (TUI), such as those built with ncurses. These characters can be used to draw boxes, lines, and corners to simulate window-like structures in a terminal.
|
||||
|
||||
### ASCII Characters for Frames
|
||||
|
||||
These are basic ASCII characters that work universally in most terminals:
|
||||
|
||||
```
|
||||
Horizontal line: - (U+002D)
|
||||
Vertical line: | (U+007C)
|
||||
Cross/intersection: + (U+002B)
|
||||
Corners:
|
||||
Top-left: + (U+002B) or `
|
||||
Top-right: + (U+002B) or '
|
||||
Bottom-left: + (U+002B) or ,
|
||||
Bottom-right: + (U+002B) or .
|
||||
```
|
||||
|
||||
Example simple ASCII window:
|
||||
|
||||
```
|
||||
+----------+
|
||||
| Content |
|
||||
+----------+
|
||||
```
|
||||
|
||||
### UTF-8 Box-Drawing Characters
|
||||
|
||||
Unicode provides a dedicated **Box Drawing** block (U+2500–U+257F) for creating more refined frames. These are widely supported in modern terminals and ncurses:
|
||||
|
||||
#### Single-Line Box Drawing
|
||||
|
||||
```
|
||||
Horizontal line: ─ (U+2500)
|
||||
Vertical line: │ (U+2502)
|
||||
Corners:
|
||||
Top-left: ┌ (U+250C)
|
||||
Top-right: ┐ (U+2510)
|
||||
Bottom-left: └ (U+2514)
|
||||
Bottom-right: ┘ (U+2518)
|
||||
Intersections:
|
||||
T-junction top: ┬ (U+252C)
|
||||
T-junction bottom: ┴ (U+2534)
|
||||
T-junction left: ├ (U+251C)
|
||||
T-junction right: ┤ (U+2524)
|
||||
Cross: ┼ (U+253C)
|
||||
```
|
||||
|
||||
Example single-line window:
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Content │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
#### Double-Line Box Drawing
|
||||
|
||||
```
|
||||
Horizontal line: ═ (U+2550)
|
||||
Vertical line: ║ (U+2551)
|
||||
Corners:
|
||||
Top-left: ╔ (U+2554)
|
||||
Top-right: ╗ (U+2557)
|
||||
Bottom-left: ╚ (U+255A)
|
||||
Bottom-right: ╝ (U+255D)
|
||||
Intersections:
|
||||
T-junction top: ╦ (U+2566)
|
||||
T-junction bottom: ╩ (U+2569)
|
||||
T-junction left: ╠ (U+2560)
|
||||
T-junction right: ╣ (U+2563)
|
||||
Cross: ╬ (U+256C)
|
||||
```
|
||||
|
||||
Example double-line window:
|
||||
|
||||
```
|
||||
╔══════════╗
|
||||
║ Content ║
|
||||
╚══════════╝
|
||||
```
|
||||
|
||||
#### Mixed and Other Box-Drawing Characters
|
||||
|
||||
For more complex designs, you can mix single and double lines or use specialized characters:
|
||||
|
||||
```
|
||||
Single to double transitions:
|
||||
Horizontal single to double: ╼ (U+257C)
|
||||
Vertical single to double: ╽ (U+257D)
|
||||
Rounded corners (less common, not always supported):
|
||||
Top-left: ╭ (U+256D)
|
||||
Top-right: ╮ (U+256E)
|
||||
Bottom-left: ╰ (U+2570)
|
||||
Bottom-right: ╯ (U+256F)
|
||||
```
|
||||
|
||||
Example with rounded corners:
|
||||
|
||||
```
|
||||
╭──────────╮
|
||||
│ Content │
|
||||
╰──────────╯
|
||||
```
|
||||
|
||||
### Additional UTF-8 Characters for Decoration
|
||||
|
||||
These can enhance the appearance of your TUI:
|
||||
|
||||
```
|
||||
Block elements for borders or shading:
|
||||
Full block: █ (U+2588)
|
||||
Half blocks: ▀ (U+2580), ▄ (U+2584), ▌ (U+258C), ▐ (U+2590)
|
||||
Light shade for background: ░ (U+2591)
|
||||
Medium shade: ▒ (U+2592)
|
||||
Dark shade: ▓ (U+2593)
|
||||
```
|
||||
|
||||
### Example TUI Window with Content
|
||||
|
||||
Here’s a sample of a more complex window using single-line box-drawing characters:
|
||||
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ My TUI Window │
|
||||
├────────────────────┤
|
||||
│ Item 1 [ OK ] │
|
||||
│ Item 2 [Cancel] │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
### Notes for ncurses
|
||||
|
||||
- **ncurses Compatibility**: ncurses supports both ASCII and UTF-8 box-drawing characters, but you must ensure the terminal supports Unicode (e.g., `LANG=en_US.UTF-8` environment variable). Use `initscr()` and `start_color()` in ncurses to handle rendering.
|
||||
- **Terminal Support**: Some older terminals may not render UTF-8 characters correctly. Test your TUI in the target environment (e.g., xterm, gnome-terminal, or Alacritty).
|
||||
- **Fallback**: If Unicode support is unreliable, stick to ASCII (`-`, `|`, `+`) for maximum compatibility.
|
||||
- **ncurses Functions**: Use `box()` in ncurses to draw a border around a window automatically, or manually print characters with `mvaddch()` for custom designs.
|
||||
|
||||
### Tips
|
||||
|
||||
- Combine single and double lines for visual hierarchy (e.g., double lines for outer windows, single lines for inner sections).
|
||||
- If you need specific examples (e.g., a multi-window layout or a dialog box), let me know, and I can provide a detailed ASCII/UTF-8 mockup or even pseudocode for ncurses.
|
||||
- If you want a particular style (e.g., heavy lines, dashed lines, or specific layouts), please clarify, and I’ll tailor the response.
|
||||
|
||||
Let me know if you need help implementing this in ncurses or want more specific frame designs!
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user