diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/dda.txt b/dda.txt old mode 100644 new mode 100755 diff --git a/frontend/ascii_tile_map.js b/frontend/ascii_tile_map.js index 26710ee..a392904 100755 --- a/frontend/ascii_tile_map.js +++ b/frontend/ascii_tile_map.js @@ -149,14 +149,19 @@ export class TileMap { } behavesLikeFloor(x, y) { + console.log("behavesLikeFloor???", { x, y }); x |= 0; y |= 0; if (x < 0 || x >= this.width || y < 0 || y >= this.height) { + console.log(" behavesLikeFloor: YES"); return true; } - return this.tiles[y][x].isFloorlike(); + const result = this.tiles[y][x].isFloorlike(); + console.log(result ? " YES" : " NOPE"); + + return result; } /** diff --git a/frontend/ascii_tile_types.js b/frontend/ascii_tile_types.js index 6a26030..14ca02e 100755 --- a/frontend/ascii_tile_types.js +++ b/frontend/ascii_tile_types.js @@ -1,7 +1,7 @@ import { mustBe, mustBeString } from "../utils/mustbe.js"; import shallowCopy from "../utils/shallowCopy.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 */ @@ -145,7 +145,7 @@ export class Tile { "REQUIRED_ symbol encountered in Tile constructor. ", "REQUIRED_ is a placeholder, and cannot be used as a value directly", ].join("\n"), - { key, val, options: properties }, + { key, val, properties }, ); throw new Error("Incomplete data in constructor. Args may not contain a data placeholder"); } @@ -198,7 +198,6 @@ export class Tile { // // Normalize Orientation. - // if (this.orientation !== undefined && typeof this.orientation === "string") { const valueMap = { 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) { mustBe(this.id, "number", "string"); } @@ -234,8 +233,8 @@ export class Tile { } /** @returns {Tile} */ - static createEncounterStartPoint() { - return this.fromChar(TileChars.ENCOUNTER_START_POINT); + static createEncounterStartPoint(encounterId) { + return this.fromChar(TileChars.ENCOUNTER_START_POINT, { encounterId }); } /** @returns {Tile} */ @@ -270,7 +269,7 @@ export class Tile { // Normalize options into a TileOptions object, // if (!(options instanceof TileOptions)) { - options = TileOptions.fromObject(options); + options = TileOptions.fromObject(typeId, options); } let optionPos = 0; @@ -278,7 +277,7 @@ export class Tile { const getOption = (name) => options.getValue(name, optionPos++); 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); } @@ -287,7 +286,7 @@ export class Tile { } clone() { - return new this.constructor(this); + return new Tile(this.typeId, this); } isWallLike() { @@ -303,6 +302,10 @@ export class Tile { } isFloorlike() { + if (this.typeId === TileChars.FLOOR) { + return true; + } + if (this.is === TileChars.FLOOR) { return true; } @@ -318,7 +321,3 @@ export class Tile { return this.typeId === TileChars.FLOOR; } } - -if (Math.PI < 0 && TileOptions && Orientation && Vector2i) { - ("STFU Linda"); -} diff --git a/frontend/cellular_automata_map_generator.html b/frontend/cellular_automata_map_generator.html old mode 100644 new mode 100755 diff --git a/frontend/dungeon_studio.js b/frontend/dungeon_studio.js index 8826501..e611657 100755 --- a/frontend/dungeon_studio.js +++ b/frontend/dungeon_studio.js @@ -1,5 +1,5 @@ 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"; class DungeonGenerator { @@ -312,7 +312,16 @@ class DungeonGenerator { addPortals() { 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) { // There are no isolated areas, return @@ -381,7 +390,7 @@ class DungeonGenerator { for (let i = 0; i < encouterCount; i++) { const pos = floorTiles[this.random(0, floorTiles.length - 1)]; 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. } } @@ -401,7 +410,8 @@ class DungeonGenerator { } } -let currentDungeon = ""; +/** @type {string} */ +window.currentDungeon = ""; window.generateDungeon = () => { const width = parseInt(document.getElementById("width").value); @@ -409,9 +419,9 @@ window.generateDungeon = () => { const roomCount = parseInt(document.getElementById("roomCount").value); 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 = () => { diff --git a/frontend/progen.scss b/frontend/progen.scss old mode 100644 new mode 100755 diff --git a/models/game.js b/models/game.js index 82bdfe6..eb34908 100755 --- a/models/game.js +++ b/models/game.js @@ -56,7 +56,8 @@ export class Game { this._random = new Xorshift32(rngSeed); } - getPlayer(username) { + getPlayerByUsername(username) { + console.log("GETTING PLAYER: `%s`", username); return this._players.get(username); } diff --git a/models/location.js b/models/location.js index 8cd2b17..6ee982d 100755 --- a/models/location.js +++ b/models/location.js @@ -1,4 +1,4 @@ -import { Portal } from "./portal"; +/** @typedef {import("./portal.js").Portal} Portal */ /** * Location in the world. @@ -9,37 +9,38 @@ import { Portal } from "./portal"; * or magical portals to distant locations. */ export class Location { - /** @protected @type string */ - _id; - get id() { - return this._id; - } + /** @protected @type {string} */ + _id; + get id() { + return this._id; + } - /** @protected @type string */ - _name; - get name() { - return this._name; - } + /** @protected @type {string} */ + _name; + get name() { + return this._name; + } - /** @protected @type string */ - _description; - get description() { - return this._description; - } + /** @protected @type {string} */ + _description; + get description() { + return this._description; + } - /** @protected @type {Map} */ - _portals = new Map(); - get portals() { - return this._portals; - } + /** @protected @type {Map} */ + _portals = new Map(); + get portals() { + return this._portals; + } - /** - */ - constructor(id, name, description) { - this._id = id; - this._name = name; - this._description = description; - } + /** + * @param {string} id + * @param {string} name + * @param {string} description + */ + constructor(id, name, description) { + this._id = id; + this._name = name; + this._description = description; + } } - -const l = new Location("foo", "bar", "baz"); diff --git a/models/session.js b/models/session.js index 4ed3b95..86d9154 100755 --- a/models/session.js +++ b/models/session.js @@ -10,35 +10,27 @@ export class Session { _websocket; /** @protected @type {Scene} */ - _scene; + _currentScene; - /** @readonly @constant @type {Scene} */ + /** @readonly @type {Scene} */ get scene() { - return this._scene; + return this._currentScene; } - /** @type {Player} */ + /** @constant @type {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() { 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 */ @@ -54,10 +46,30 @@ export class Session { if (!(scene instanceof Scene)) { throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`); } - this._scene = scene; + this._currentScene = scene; 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() { if (this._websocket) { @@ -65,7 +77,7 @@ export class Session { this._websocket = null; } this._player = null; - this._scene = null; + this._currentScene = null; } /** diff --git a/scenes/authentication/authenticationScene.js b/scenes/authentication/authenticationScene.js index aa3969c..aafb79a 100755 --- a/scenes/authentication/authenticationScene.js +++ b/scenes/authentication/authenticationScene.js @@ -29,10 +29,9 @@ export class AuthenticationScene extends Scene { passwordAccepted() { 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.setScene(new GameScene()); } diff --git a/scenes/authentication/usernamePrompt.js b/scenes/authentication/usernamePrompt.js index 123deb4..0b52c1e 100755 --- a/scenes/authentication/usernamePrompt.js +++ b/scenes/authentication/usernamePrompt.js @@ -30,11 +30,11 @@ export class UsernamePrompt extends Prompt { // // User replied to our prompt - onReply(text) { + onReply(username) { // // do basic syntax checks on usernames - if (!security.isUsernameSane(text)) { - console.info("Someone entered insane username: '%s'", text); + if (!security.isUsernameSane(username)) { + console.info("Someone entered insane username: '%s'", username); this.sendError("Incorrect username, try again"); this.execute(); return; @@ -42,12 +42,12 @@ export class UsernamePrompt extends Prompt { // // try and fetch the player object from the game - const player = gGame.getPlayer(text); + const player = gGame.getPlayerByUsername(username); // // handle invalid username 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.execute(); return; diff --git a/scenes/playerCreation/createUsernamePrompt.js b/scenes/playerCreation/createUsernamePrompt.js index ae4f18b..a75219a 100755 --- a/scenes/playerCreation/createUsernamePrompt.js +++ b/scenes/playerCreation/createUsernamePrompt.js @@ -44,7 +44,7 @@ export class CreateUsernamePrompt extends Prompt { // // try and fetch the player object from the game - const player = gGame.getPlayer(username); + const player = gGame.getPlayerByUsername(username); // // handle invalid username diff --git a/scenes/playerCreation/playerCreationScene.js b/scenes/playerCreation/playerCreationScene.js old mode 100644 new mode 100755 diff --git a/scenes/prompt.js b/scenes/prompt.js index d2dcdb7..b74120b 100755 --- a/scenes/prompt.js +++ b/scenes/prompt.js @@ -1,8 +1,7 @@ -import { Scene } from "./scene.js"; - /** @typedef {import("../models/session.js").Session} Session */ /** @typedef {import("../utils/message.js").MessageType} MessageType */ /** @typedef {import("../utils/message.js").WebsocketMessage} WebsocketMessage */ +/** @typedef {import("./scene.js").Scene} Scene */ /** * @typedef {object} PromptMethods @@ -66,9 +65,6 @@ export class Prompt { /** @param {Scene} scene */ constructor(scene) { - if (!(scene instanceof Scene)) { - throw new Error("Expected an instance of >>Scene<< but got " + typeof scene); - } this._scene = scene; // @@ -103,6 +99,8 @@ export class Prompt { * * @param {string} command * @param {any[]} args + * + * @returns {boolean} true if the command was handled in the prompt. */ onColon(command, args) { @@ -113,21 +111,21 @@ export class Prompt { // Default: we have no handler for the Foo command, // So let's see if daddy can handle it. if (property === undefined) { - return this.scene.onColon(command, args); + return false; } // // If the prompt has a method called onColon_foo() => if (typeof property === "function") { property.call(this, args); - return; + return true; } // // If the prompt has a _string_ called onColon_foo => if (typeof property === "string") { this.sendText(property); - return; + return true; } // diff --git a/scenes/scene.js b/scenes/scene.js index 34238e2..d8c040f 100755 --- a/scenes/scene.js +++ b/scenes/scene.js @@ -1,6 +1,8 @@ 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. @@ -19,7 +21,7 @@ export class Scene { */ introText = ""; - /** @readonly @constant @type {Session} */ + /** @readonly @constant @protected @type {Session} */ _session; get session() { return this._session; @@ -32,9 +34,9 @@ export class Scene { * @readonly * @type {Prompt} */ - _prompt; - get prompt() { - return this._prompt; + _currentPrompt; + get currentPrompt() { + return this._currentPrompt; } constructor() {} @@ -57,7 +59,7 @@ export class Scene { * @param {Prompt} prompt */ showPrompt(prompt) { - this._prompt = prompt; + this._currentPrompt = prompt; prompt.execute(); } @@ -66,6 +68,55 @@ export class Scene { 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 * and the current Prompt cannot handle that command. @@ -73,7 +124,7 @@ export class Scene { * @param {string} command * @param {any[]} args */ - onColon(command, args) { + onColonFallback(command, args) { const propertyName = "onColon__" + command; 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") { property.call(this, args); return; } // - // If the prompt has a _string_ called onColon_foo => + // If this scene has a string property called onColon_foo => if (typeof property === "string") { this.session.sendText(property); 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 + // Example dynamic colon handler (also easter egg) /** @param {any[]} args */ onColon__imperial(args) { if (args.length === 0) { @@ -124,9 +194,5 @@ export class Scene { ); } - onColon__hi = "Hoe"; -} - -if (Math.PI < 0 && Session && Prompt) { - ("STFU Linda"); + onColon__hi = "Ho!"; } diff --git a/seeders/gameSeeder.js b/seeders/gameSeeder.js index 87142e0..1447dc4 100755 --- a/seeders/gameSeeder.js +++ b/seeders/gameSeeder.js @@ -18,7 +18,7 @@ export class GameSeeder { gGame.rngSeed = Config.rngSeed; new PlayerSeeder().seed(); // Create debug players 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 diff --git a/server.js b/server.js index 267fc19..732a298 100755 --- a/server.js +++ b/server.js @@ -174,15 +174,15 @@ class MudServer { 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()) { - return session.scene.prompt.onReply(msgObj.text); + return session.scene.onReply(msgObj); } // // Handle :help commands 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 if (msgObj.isColon()) { - return session.scene.prompt.onColon(msgObj.command, msgObj.args); + return session.scene.onColon(msgObj); } // diff --git a/utils/crackdown.js b/utils/crackdown.js old mode 100644 new mode 100755 diff --git a/utils/security.js b/utils/security.js old mode 100644 new mode 100755 index eca4506..cae5770 --- a/utils/security.js +++ b/utils/security.js @@ -54,5 +54,5 @@ export function isUsernameSane(candidate) { export function isPasswordSane(candidate) { // 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 - return Config.passwordSanityRegex.test(candidate); + return Config.passwordHashSanityRegex.test(candidate); } diff --git a/utils/tui.js b/utils/tui.js old mode 100644 new mode 100755