File perms

This commit is contained in:
Kim Ravn Hansen
2025-10-15 12:28:14 +02:00
parent e12dfd0981
commit 59d73955d0
21 changed files with 202 additions and 111 deletions

0
.gitignore vendored Normal file → Executable file
View File

0
dda.txt Normal file → Executable file
View File

View File

@@ -149,14 +149,19 @@ export class TileMap {
} }
behavesLikeFloor(x, y) { behavesLikeFloor(x, y) {
console.log("behavesLikeFloor???", { x, y });
x |= 0; x |= 0;
y |= 0; y |= 0;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
console.log(" behavesLikeFloor: YES");
return true; return true;
} }
return this.tiles[y][x].isFloorlike(); const result = this.tiles[y][x].isFloorlike();
console.log(result ? " YES" : " NOPE");
return result;
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { mustBe, mustBeString } from "../utils/mustbe.js"; import { mustBe, mustBeString } from "../utils/mustbe.js";
import shallowCopy from "../utils/shallowCopy.js"; import shallowCopy from "../utils/shallowCopy.js";
import { TileOptions } from "../utils/tileOptionsParser.js"; import { TileOptions } from "../utils/tileOptionsParser.js";
import { Orientation, Vector2i } from "./ascii_types.js"; import { Orientation } from "./ascii_types.js";
/** @typedef {string} TileTypeId - a string with a length of 1 */ /** @typedef {string} TileTypeId - a string with a length of 1 */
@@ -145,7 +145,7 @@ export class Tile {
"REQUIRED_ symbol encountered in Tile constructor. ", "REQUIRED_ symbol encountered in Tile constructor. ",
"REQUIRED_ is a placeholder, and cannot be used as a value directly", "REQUIRED_ is a placeholder, and cannot be used as a value directly",
].join("\n"), ].join("\n"),
{ key, val, options: properties }, { key, val, properties },
); );
throw new Error("Incomplete data in constructor. Args may not contain a data placeholder"); throw new Error("Incomplete data in constructor. Args may not contain a data placeholder");
} }
@@ -198,7 +198,6 @@ export class Tile {
// //
// Normalize Orientation. // Normalize Orientation.
//
if (this.orientation !== undefined && typeof this.orientation === "string") { if (this.orientation !== undefined && typeof this.orientation === "string") {
const valueMap = { const valueMap = {
north: Orientation.NORTH, north: Orientation.NORTH,
@@ -210,7 +209,7 @@ export class Tile {
} }
// //
// Tiles are not necessarily required to have an ID, but if they have one, it must be string or number // Tiles are not required to have IDs, but IDs must be numbers or strings
if (this.id !== undefined) { if (this.id !== undefined) {
mustBe(this.id, "number", "string"); mustBe(this.id, "number", "string");
} }
@@ -234,8 +233,8 @@ export class Tile {
} }
/** @returns {Tile} */ /** @returns {Tile} */
static createEncounterStartPoint() { static createEncounterStartPoint(encounterId) {
return this.fromChar(TileChars.ENCOUNTER_START_POINT); return this.fromChar(TileChars.ENCOUNTER_START_POINT, { encounterId });
} }
/** @returns {Tile} */ /** @returns {Tile} */
@@ -270,7 +269,7 @@ export class Tile {
// Normalize options into a TileOptions object, // Normalize options into a TileOptions object,
// //
if (!(options instanceof TileOptions)) { if (!(options instanceof TileOptions)) {
options = TileOptions.fromObject(options); options = TileOptions.fromObject(typeId, options);
} }
let optionPos = 0; let optionPos = 0;
@@ -278,7 +277,7 @@ export class Tile {
const getOption = (name) => options.getValue(name, optionPos++); const getOption = (name) => options.getValue(name, optionPos++);
for (let [key, val] of Object.entries(typeInfo)) { for (let [key, val] of Object.entries(typeInfo)) {
// //
const fetchFromOption = typeof val === "symbol" && val.descript.startsWith("REQUIRED_"); const fetchFromOption = typeof val === "symbol" && val.description.startsWith("REQUIRED_");
creationArgs[key] = fetchFromOption ? getOption(key) : shallowCopy(val); creationArgs[key] = fetchFromOption ? getOption(key) : shallowCopy(val);
} }
@@ -287,7 +286,7 @@ export class Tile {
} }
clone() { clone() {
return new this.constructor(this); return new Tile(this.typeId, this);
} }
isWallLike() { isWallLike() {
@@ -303,6 +302,10 @@ export class Tile {
} }
isFloorlike() { isFloorlike() {
if (this.typeId === TileChars.FLOOR) {
return true;
}
if (this.is === TileChars.FLOOR) { if (this.is === TileChars.FLOOR) {
return true; return true;
} }
@@ -318,7 +321,3 @@ export class Tile {
return this.typeId === TileChars.FLOOR; return this.typeId === TileChars.FLOOR;
} }
} }
if (Math.PI < 0 && TileOptions && Orientation && Vector2i) {
("STFU Linda");
}

0
frontend/cellular_automata_map_generator.html Normal file → Executable file
View File

View File

@@ -1,5 +1,5 @@
import { CharType, TileMap } from "./ascii_tile_map"; import { CharType, TileMap } from "./ascii_tile_map";
import { Tile } from "./ascii_tile_types"; import { Tile, TileChars } from "./ascii_tile_types";
import { Orientation } from "./ascii_types"; import { Orientation } from "./ascii_types";
class DungeonGenerator { class DungeonGenerator {
@@ -312,7 +312,16 @@ class DungeonGenerator {
addPortals() { addPortals() {
let traversableTileCount = this.map.getFloorlikeTileCount(); let traversableTileCount = this.map.getFloorlikeTileCount();
const result = this.map.getAllTraversableTilesConnectedTo(/** TODO PlayerPos */); //
// Find the player's start point, and let this be the
// bases of area 0
const [x, y] = this.map.forEach((tile, x, y) => {
if (tile.typeId === TileChars.PLAYER_START_POINT) {
return [x, y];
}
});
const result = this.map.getAllTraversableTilesConnectedTo(x, y);
if (result.size === traversableTileCount) { if (result.size === traversableTileCount) {
// There are no isolated areas, return // There are no isolated areas, return
@@ -381,7 +390,7 @@ class DungeonGenerator {
for (let i = 0; i < encouterCount; i++) { for (let i = 0; i < encouterCount; i++) {
const pos = floorTiles[this.random(0, floorTiles.length - 1)]; const pos = floorTiles[this.random(0, floorTiles.length - 1)];
if (this.map.tiles[pos.y][pos.x].isFloor()) { if (this.map.tiles[pos.y][pos.x].isFloor()) {
this.map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint(); this.map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint("PLACEHOLDER_ENCOUNTER_ID");
// TODO: Add encounter to the dungeon's "roaming entities" array. // TODO: Add encounter to the dungeon's "roaming entities" array.
} }
} }
@@ -401,7 +410,8 @@ class DungeonGenerator {
} }
} }
let currentDungeon = ""; /** @type {string} */
window.currentDungeon = "";
window.generateDungeon = () => { window.generateDungeon = () => {
const width = parseInt(document.getElementById("width").value); const width = parseInt(document.getElementById("width").value);
@@ -409,9 +419,9 @@ window.generateDungeon = () => {
const roomCount = parseInt(document.getElementById("roomCount").value); const roomCount = parseInt(document.getElementById("roomCount").value);
const generator = new DungeonGenerator(width, height, roomCount); const generator = new DungeonGenerator(width, height, roomCount);
currentDungeon = generator.generate(); window.currentDungeon = generator.generate();
document.getElementById("dungeonDisplay").textContent = currentDungeon; document.getElementById("dungeonDisplay").textContent = window.currentDungeon;
}; };
window.downloadDungeon = () => { window.downloadDungeon = () => {

0
frontend/progen.scss Normal file → Executable file
View File

View File

@@ -56,7 +56,8 @@ export class Game {
this._random = new Xorshift32(rngSeed); this._random = new Xorshift32(rngSeed);
} }
getPlayer(username) { getPlayerByUsername(username) {
console.log("GETTING PLAYER: `%s`", username);
return this._players.get(username); return this._players.get(username);
} }

View File

@@ -1,4 +1,4 @@
import { Portal } from "./portal"; /** @typedef {import("./portal.js").Portal} Portal */
/** /**
* Location in the world. * Location in the world.
@@ -9,37 +9,38 @@ import { Portal } from "./portal";
* 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;
get id() { get id() {
return this._id; return this._id;
} }
/** @protected @type string */ /** @protected @type {string} */
_name; _name;
get name() { get name() {
return this._name; return this._name;
} }
/** @protected @type string */ /** @protected @type {string} */
_description; _description;
get description() { get description() {
return this._description; return this._description;
} }
/** @protected @type {Map<string,Portal>} */ /** @protected @type {Map<string,Portal>} */
_portals = new Map(); _portals = new Map();
get portals() { get portals() {
return this._portals; return this._portals;
} }
/** /**
*/ * @param {string} id
constructor(id, name, description) { * @param {string} name
this._id = id; * @param {string} description
this._name = name; */
this._description = description; constructor(id, name, description) {
} this._id = id;
this._name = name;
this._description = description;
}
} }
const l = new Location("foo", "bar", "baz");

View File

@@ -10,35 +10,27 @@ export class Session {
_websocket; _websocket;
/** @protected @type {Scene} */ /** @protected @type {Scene} */
_scene; _currentScene;
/** @readonly @constant @type {Scene} */ /** @readonly @type {Scene} */
get scene() { get scene() {
return this._scene; return this._currentScene;
} }
/** @type {Player} */ /** @constant @type {Player} */
_player; _player;
/**
* The player that owns this session.
* This value is undefined until the player has logged in,
* and after that it is _constant_
*
* @constant @type {Player}
*/
get player() { get player() {
return this._player; return this._player;
} }
/** @param {Player} player */
set player(player) {
if (player instanceof Player) {
this._player = player;
return;
}
if (player === null) {
this._player = null;
return;
}
throw Error(`Can only set player to null or instance of Player, but received ${typeof player}`);
}
/** /**
* @param {WebSocket} websocket * @param {WebSocket} websocket
*/ */
@@ -54,10 +46,30 @@ export class Session {
if (!(scene instanceof Scene)) { if (!(scene instanceof Scene)) {
throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`); throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`);
} }
this._scene = scene; this._currentScene = scene;
scene.execute(this); scene.execute(this);
} }
/**
* May only be called when the current player property has not yet been populated.
* May only called once.
*
* @param {Player} player
*/
setPlayer(player) {
if (player instanceof Player) {
this._player = player;
return;
}
if (player === null) {
this._player = null;
return;
}
throw Error(`Can only set player to null or instance of Player, but received ${typeof player}`);
}
/** Close the session and websocket */ /** Close the session and websocket */
close() { close() {
if (this._websocket) { if (this._websocket) {
@@ -65,7 +77,7 @@ export class Session {
this._websocket = null; this._websocket = null;
} }
this._player = null; this._player = null;
this._scene = null; this._currentScene = null;
} }
/** /**

View File

@@ -29,10 +29,9 @@ export class AuthenticationScene extends Scene {
passwordAccepted() { passwordAccepted() {
this.player.loggedIn = true; this.player.loggedIn = true;
this.session.player = this.player;
this.session.setPlayer(this.player);
this.session.sendText(["= Success!", "((but I don't know what to do now...))"]); this.session.sendText(["= Success!", "((but I don't know what to do now...))"]);
this.session.setScene(new GameScene()); this.session.setScene(new GameScene());
} }

View File

@@ -30,11 +30,11 @@ export class UsernamePrompt extends Prompt {
// //
// User replied to our prompt // User replied to our prompt
onReply(text) { onReply(username) {
// //
// do basic syntax checks on usernames // do basic syntax checks on usernames
if (!security.isUsernameSane(text)) { if (!security.isUsernameSane(username)) {
console.info("Someone entered insane username: '%s'", text); console.info("Someone entered insane username: '%s'", username);
this.sendError("Incorrect username, try again"); this.sendError("Incorrect username, try again");
this.execute(); this.execute();
return; return;
@@ -42,12 +42,12 @@ export class UsernamePrompt extends Prompt {
// //
// try and fetch the player object from the game // try and fetch the player object from the game
const player = gGame.getPlayer(text); const player = gGame.getPlayerByUsername(username);
// //
// handle invalid username // handle invalid username
if (!player) { if (!player) {
console.info("Someone entered incorrect username: '%s'", text); console.info("Someone entered incorrect username: '%s'", username);
this.sendError("Incorrect username, try again"); this.sendError("Incorrect username, try again");
this.execute(); this.execute();
return; return;

View File

@@ -44,7 +44,7 @@ export class CreateUsernamePrompt extends Prompt {
// //
// try and fetch the player object from the game // try and fetch the player object from the game
const player = gGame.getPlayer(username); const player = gGame.getPlayerByUsername(username);
// //
// handle invalid username // handle invalid username

0
scenes/playerCreation/playerCreationScene.js Normal file → Executable file
View File

View File

@@ -1,8 +1,7 @@
import { Scene } from "./scene.js";
/** @typedef {import("../models/session.js").Session} Session */ /** @typedef {import("../models/session.js").Session} Session */
/** @typedef {import("../utils/message.js").MessageType} MessageType */ /** @typedef {import("../utils/message.js").MessageType} MessageType */
/** @typedef {import("../utils/message.js").WebsocketMessage} WebsocketMessage */ /** @typedef {import("../utils/message.js").WebsocketMessage} WebsocketMessage */
/** @typedef {import("./scene.js").Scene} Scene */
/** /**
* @typedef {object} PromptMethods * @typedef {object} PromptMethods
@@ -66,9 +65,6 @@ export class Prompt {
/** @param {Scene} scene */ /** @param {Scene} scene */
constructor(scene) { constructor(scene) {
if (!(scene instanceof Scene)) {
throw new Error("Expected an instance of >>Scene<< but got " + typeof scene);
}
this._scene = scene; this._scene = scene;
// //
@@ -103,6 +99,8 @@ export class Prompt {
* *
* @param {string} command * @param {string} command
* @param {any[]} args * @param {any[]} args
*
* @returns {boolean} true if the command was handled in the prompt.
*/ */
onColon(command, args) { onColon(command, args) {
@@ -113,21 +111,21 @@ export class Prompt {
// Default: we have no handler for the Foo command, // Default: we have no handler for the Foo command,
// So let's see if daddy can handle it. // So let's see if daddy can handle it.
if (property === undefined) { if (property === undefined) {
return this.scene.onColon(command, args); return false;
} }
// //
// If the prompt has a method called onColon_foo() => // If the prompt has a method called onColon_foo() =>
if (typeof property === "function") { if (typeof property === "function") {
property.call(this, args); property.call(this, args);
return; return true;
} }
// //
// If the prompt has a _string_ called onColon_foo => // If the prompt has a _string_ called onColon_foo =>
if (typeof property === "string") { if (typeof property === "string") {
this.sendText(property); this.sendText(property);
return; return true;
} }
// //

View File

@@ -1,6 +1,8 @@
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { Session } from "../models/session.js";
import { Prompt } from "./prompt.js"; /** @typedef {import("../utils/messages.js").WebsocketMessage} WebsocketMessage */
/** @typedef {import("../models/session.js").Session} Session */
/** @typedef {import("./prompt.js").Prompt } Prompt */
/** /**
* Scene - a class for showing one or more prompts in a row. * Scene - a class for showing one or more prompts in a row.
@@ -19,7 +21,7 @@ export class Scene {
*/ */
introText = ""; introText = "";
/** @readonly @constant @type {Session} */ /** @readonly @constant @protected @type {Session} */
_session; _session;
get session() { get session() {
return this._session; return this._session;
@@ -32,9 +34,9 @@ export class Scene {
* @readonly * @readonly
* @type {Prompt} * @type {Prompt}
*/ */
_prompt; _currentPrompt;
get prompt() { get currentPrompt() {
return this._prompt; return this._currentPrompt;
} }
constructor() {} constructor() {}
@@ -57,7 +59,7 @@ export class Scene {
* @param {Prompt} prompt * @param {Prompt} prompt
*/ */
showPrompt(prompt) { showPrompt(prompt) {
this._prompt = prompt; this._currentPrompt = prompt;
prompt.execute(); prompt.execute();
} }
@@ -66,6 +68,55 @@ export class Scene {
this.showPrompt(new promptClassReference(this)); this.showPrompt(new promptClassReference(this));
} }
/**
* The user has been prompted, and has replied.
*
* We route that message to the current prompt.
*
* It should not be necessary to override this function
*
* @param {WebsocketMessage} message
*/
onReply(message) {
console.log("REPLY", {
message,
type: typeof message,
});
this.currentPrompt.onReply(message.text);
}
/**
* The user has declared their intention to quit.
*
* We route that message to the current prompt.
*
* It should may be necessary to override this method
* in case you want to trigger specific behavior before
* quitting.
*
* Default behavior is to route this message to the current prompt.
*/
onQuit() {
this.currentPrompt.onQuit();
}
/**
* The user has typed :help [topic]
*
* We route that message to the current prompt.
*
* It should not be necessary to override this function
* unless you want some special prompt-agnostic event
* to be triggered - however, scenes should not have too
* many prompts, so handling this behavior inside the prompt
* should be the primary choice.
*
* @param {WebsocketMessage} message
*/
onHelp(message) {
this.currentPrompt.onHelp(message.text);
}
/** /**
* Triggered when a user types a :command that begins with a colon * Triggered when a user types a :command that begins with a colon
* and the current Prompt cannot handle that command. * and the current Prompt cannot handle that command.
@@ -73,7 +124,7 @@ export class Scene {
* @param {string} command * @param {string} command
* @param {any[]} args * @param {any[]} args
*/ */
onColon(command, args) { onColonFallback(command, args) {
const propertyName = "onColon__" + command; const propertyName = "onColon__" + command;
const property = this[propertyName]; const property = this[propertyName];
@@ -85,14 +136,14 @@ export class Scene {
} }
// //
// If the prompt has a method called onColon_foo() => // If this scene has a method called onColon_foo() =>
if (typeof property === "function") { if (typeof property === "function") {
property.call(this, args); property.call(this, args);
return; return;
} }
// //
// If the prompt has a _string_ called onColon_foo => // If this scene has a string property called onColon_foo =>
if (typeof property === "string") { if (typeof property === "string") {
this.session.sendText(property); this.session.sendText(property);
return; return;
@@ -108,9 +159,28 @@ export class Scene {
); );
} }
/**
* The user has typed :help [topic]
*
* We route that message to the current prompt.
*
* There is no need to override this method.
*
* onColonFallback will be called if the current prompt
* cannot handle the :colon command.
*
* @param {WebsocketMessage} message
*/
onColon(message) {
const handledByPrompt = this.currentPrompt.onColon(message.command, message.args);
if (!handledByPrompt) {
this.onColonFallback(message.command, message.args);
}
}
// //
// Easter ægg // Example dynamic colon handler (also easter egg)
// Example dynamic colon handler
/** @param {any[]} args */ /** @param {any[]} args */
onColon__imperial(args) { onColon__imperial(args) {
if (args.length === 0) { if (args.length === 0) {
@@ -124,9 +194,5 @@ export class Scene {
); );
} }
onColon__hi = "Hoe"; onColon__hi = "Ho!";
}
if (Math.PI < 0 && Session && Prompt) {
("STFU Linda");
} }

View File

@@ -18,7 +18,7 @@ export class GameSeeder {
gGame.rngSeed = Config.rngSeed; gGame.rngSeed = Config.rngSeed;
new PlayerSeeder().seed(); // Create debug players new PlayerSeeder().seed(); // Create debug players
new ItemSeeder().seed(); // Create items, etc. new ItemSeeder().seed(); // Create items, etc.
new CharacterSeeder().createParty(gGame.getPlayer("user"), 3); // Create debug characters. new CharacterSeeder().createParty(gGame.getPlayerByUsername("user"), 3); // Create debug characters.
// //
// Done // Done

View File

@@ -174,15 +174,15 @@ class MudServer {
const msgObj = new WebsocketMessage(data.toString()); const msgObj = new WebsocketMessage(data.toString());
// //
// Handle replies to prompts. The main workhorse of the game. // Route reply-messages to the current scene
if (msgObj.isReply()) { if (msgObj.isReply()) {
return session.scene.prompt.onReply(msgObj.text); return session.scene.onReply(msgObj);
} }
// //
// Handle :help commands // Handle :help commands
if (msgObj.isHelp()) { if (msgObj.isHelp()) {
return session.scene.prompt.onHelp(msgObj.text); return session.scene.onHelp(msgObj);
} }
// //
@@ -196,7 +196,7 @@ class MudServer {
// //
// Handle any text that starts with ":" that isn't :help or :quit // Handle any text that starts with ":" that isn't :help or :quit
if (msgObj.isColon()) { if (msgObj.isColon()) {
return session.scene.prompt.onColon(msgObj.command, msgObj.args); return session.scene.onColon(msgObj);
} }
// //

0
utils/crackdown.js Normal file → Executable file
View File

2
utils/security.js Normal file → Executable file
View File

@@ -54,5 +54,5 @@ export function isUsernameSane(candidate) {
export function isPasswordSane(candidate) { export function isPasswordSane(candidate) {
// We know the password must adhere to one of our client-side-hashed crypto schemes, // We know the password must adhere to one of our client-side-hashed crypto schemes,
// so we can be fairly strict with the allowed passwords // so we can be fairly strict with the allowed passwords
return Config.passwordSanityRegex.test(candidate); return Config.passwordHashSanityRegex.test(candidate);
} }

0
utils/tui.js Normal file → Executable file
View File