rearrage_stuff
This commit is contained in:
50
scenes/authentication/authenticationScene.js
Executable file
50
scenes/authentication/authenticationScene.js
Executable file
@@ -0,0 +1,50 @@
|
||||
import { PasswordPrompt } from "./passwordPrompt.js";
|
||||
import { Player } from "../../models/player.js";
|
||||
import { Scene } from "../scene.js";
|
||||
import { UsernamePrompt } from "./usernamePrompt.js";
|
||||
import { PlayerCreationScene } from "../playerCreation/playerCreationSene.js";
|
||||
|
||||
/** @property {Session} session */
|
||||
export class AuthenticationScene extends Scene {
|
||||
introText = [
|
||||
"= Welcome!", //
|
||||
];
|
||||
|
||||
/** @type {Player} */
|
||||
player;
|
||||
|
||||
onReady() {
|
||||
// current prompt
|
||||
this.show(UsernamePrompt);
|
||||
}
|
||||
|
||||
/** @param {Player} player */
|
||||
usernameAccepted(player) {
|
||||
this.player = player;
|
||||
this.session.sendSystemMessage("salt", player.salt);
|
||||
this.show(PasswordPrompt);
|
||||
}
|
||||
|
||||
passwordAccepted() {
|
||||
this.player.loggedIn = true;
|
||||
this.session.player = this.player;
|
||||
|
||||
this.session.sendText(["= Success!", "((but I don't know what to do now...))"]);
|
||||
return;
|
||||
|
||||
if (this.player.admin) {
|
||||
this.session.setScene("new AdminJustLoggedInScene");
|
||||
} else {
|
||||
this.session.setScene("new JustLoggedInScene");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User typed `:create`
|
||||
*
|
||||
* Create new player
|
||||
*/
|
||||
onColon__create() {
|
||||
this.session.setScene(new PlayerCreationScene(this.session));
|
||||
}
|
||||
}
|
||||
83
scenes/authentication/passwordPrompt.js
Executable file
83
scenes/authentication/passwordPrompt.js
Executable file
@@ -0,0 +1,83 @@
|
||||
import { Prompt } from "../prompt.js";
|
||||
import * as security from "../../utils/security.js";
|
||||
import { Config } from "../../config.js";
|
||||
import { AuthenticationScene } from "./authenticationScene.js";
|
||||
|
||||
export class PasswordPrompt extends Prompt {
|
||||
//
|
||||
promptText = "Please enter your password";
|
||||
|
||||
//
|
||||
// Let the client know that we're asking for a password
|
||||
// so it can set <input type="password">
|
||||
promptOptions = { password: true };
|
||||
|
||||
get player() {
|
||||
return this.scene.player;
|
||||
}
|
||||
|
||||
/** @returns {AuthenticationScene} */
|
||||
get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
onReply(text) {
|
||||
//
|
||||
// Check of the password is sane. This is both bad from a security point
|
||||
// of view, and technically not necessary as insane passwords couldn't
|
||||
// reside in the player lists. However, let's save some CPU cycles on
|
||||
// not hashing an insane password 1000+ times.
|
||||
// This is technically bad practice, but since this is just a game,
|
||||
// do it anyway.
|
||||
if (!security.isPasswordSane(text)) {
|
||||
this.sendError("Insane password");
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Block users who enter bad passwords too many times.
|
||||
if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
|
||||
this.blockedUntil = Date.now() + Config.accountLockoutSeconds;
|
||||
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle blocked users.
|
||||
// They don't even get to have their password verified.
|
||||
if (this.player.blockedUntil > Date.now()) {
|
||||
//
|
||||
// Try to re-login too soon, and your lockout lasts longer.
|
||||
this.blockedUntil += Config.accountLockoutSeconds;
|
||||
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Verify the password against the hash we've stored.
|
||||
if (!security.verifyPassword(text, this.player.passwordHash)) {
|
||||
this.sendError("Incorrect password!");
|
||||
this.player.failedPasswordsSinceLastLogin++;
|
||||
|
||||
this.session.sendDebug(`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`);
|
||||
this.execute();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.lastSucessfulLoginAt = new Date();
|
||||
this.player.failedPasswordsSinceLastLogin = 0;
|
||||
|
||||
//
|
||||
// We do not allow a player to be logged in more than once!
|
||||
if (this.player.loggedIn) {
|
||||
this.calamity("This player is already logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
// Password was correct, go to main game
|
||||
// this.scene.passwordAccepted();
|
||||
this.scene.passwordAccepted();
|
||||
}
|
||||
}
|
||||
65
scenes/authentication/usernamePrompt.js
Executable file
65
scenes/authentication/usernamePrompt.js
Executable file
@@ -0,0 +1,65 @@
|
||||
import { Player } from "../../models/player.js";
|
||||
import { Prompt } from "../prompt.js";
|
||||
import * as security from "../../utils/security.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
import { Config } from "../../config.js";
|
||||
import { AuthenticationScene } from "./authenticationScene.js";
|
||||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @property {AuthenticationScene} scene
|
||||
*/
|
||||
export class UsernamePrompt extends Prompt {
|
||||
//
|
||||
promptText = [
|
||||
"Please enter your username:", //
|
||||
"(((type *:create* if you want to create a new user)))", //
|
||||
];
|
||||
|
||||
//
|
||||
// When player types :help
|
||||
helpText = [
|
||||
"This is where you log in.",
|
||||
"If you don't already have a player profile on this server, you can type *:create* to create one",
|
||||
];
|
||||
|
||||
//
|
||||
// Let the client know that we're asking for a username
|
||||
promptOptions = { username: true };
|
||||
|
||||
/** @returns {AuthenticationScene} */
|
||||
get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
//
|
||||
// User replied to our prompt
|
||||
onReply(text) {
|
||||
//
|
||||
// do basic syntax checks on usernames
|
||||
if (!security.isUsernameSane(text)) {
|
||||
console.info("Someone entered insane username: '%s'", text);
|
||||
this.sendError("Incorrect username, try again");
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// try and fetch the player object from the game
|
||||
const player = gGame.getPlayer(text);
|
||||
|
||||
//
|
||||
// handle invalid username
|
||||
if (!player) {
|
||||
console.info("Someone entered incorrect username: '%s'", text);
|
||||
this.sendError("Incorrect username, try again");
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Tell daddy that we're done
|
||||
this.scene.usernameAccepted(player);
|
||||
}
|
||||
}
|
||||
26
scenes/gameLoop/gameScene.js
Executable file
26
scenes/gameLoop/gameScene.js
Executable file
@@ -0,0 +1,26 @@
|
||||
import { Scene } from "../scene.js";
|
||||
|
||||
/**
|
||||
* Main game state
|
||||
*
|
||||
* It's here we listen for player commands.
|
||||
*/
|
||||
export class GameScene extends Scene {
|
||||
introText = "= Welcome";
|
||||
|
||||
onReady() {
|
||||
//
|
||||
// Find out which state the player and their characters are in
|
||||
// Find out where we are
|
||||
// Re-route to the relevant scene if necessary.
|
||||
//
|
||||
// IDEA:
|
||||
// Does a player have a previous state?
|
||||
// The state that was on the previous session?
|
||||
//
|
||||
// If player does not have a previous session
|
||||
// then we start in the Adventurers Guild in the Hovedstad
|
||||
//
|
||||
this.doPrompt("new commandprompt or whatever");
|
||||
}
|
||||
}
|
||||
13
scenes/interface.js
Executable file
13
scenes/interface.js
Executable file
@@ -0,0 +1,13 @@
|
||||
import { WebsocketMessage } from "../utils/messages.js";
|
||||
import { Session } from "../models/session.js";
|
||||
|
||||
/** @interface */
|
||||
export class StateInterface {
|
||||
/** @param {Session} session */
|
||||
constructor(session) {}
|
||||
|
||||
onAttach() {}
|
||||
|
||||
/** @param {WebsocketMessage} message */
|
||||
onMessage(message) {}
|
||||
}
|
||||
72
scenes/justLoggedIn/justLoggedInScene.js
Executable file
72
scenes/justLoggedIn/justLoggedInScene.js
Executable file
@@ -0,0 +1,72 @@
|
||||
import { Session } from "../models/session.js";
|
||||
|
||||
const castle = `
|
||||
▄
|
||||
█▐▀▀▀▌▄
|
||||
█ ▐▀▀▀▌▌▓▌
|
||||
█ ▄▄ ▄▄▀
|
||||
█ ▐▀▀▀▀
|
||||
▄█▄
|
||||
▓▀ ▀▌
|
||||
▓▀ ▓▄
|
||||
▄▓ ▐▓
|
||||
▄▓ ▀▌
|
||||
▓▀▀▀▀▀▓ ▓▀▀▀▀▓ ▐█▀▀▀▀▓
|
||||
█ █ █ ▓░ ▓▌ ▓░
|
||||
█ ▀▀▀▀▀ ▀▀▀▀▀ ▓░
|
||||
▓▒ ▓░
|
||||
▀▓▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█
|
||||
▐▌ █
|
||||
▓▀▀▀▀█ ▐█▀▀▀█ ▐█▀▀▀▓▒ ▐▌ █ ▐▓▀▀▀▓▒ ▓▀▀▀▓▒ █▀▀▀▀▓
|
||||
█ █ ▐▌ █ ▐▌ ▓▒ ▐▌ ▐██░ █ ▐█ ▓▄ █ ▐▌ █ ▐█
|
||||
▓░ ▐▀▀▀ ▐▀▀▀ █░ ▐▌ ▓██▌ █ ▐█ ▀▀▀▀ ▀▀▀ ▐▌
|
||||
▓▒ █ ▐▌ ▀██▌ █ █ ▐▌
|
||||
▀▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▓▀ ▐▌ █ ▀▌▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▓
|
||||
▐▌ █ ▐▌ █ █ ▐▌
|
||||
▐▌ █ ▐▌ █ █ ▐▌
|
||||
▐▌ ▓▌ █▀▀▀▀▀█ ▐▌▐▌▀▀▀▀█ ▓▀▀▀▀▓▄ █ █▀▀▀▀▀█ ▓▌ ▐▌
|
||||
▓▌ ██▌ █ █ ▓▌▓▒ █ █ ▐▌ █ █ █ ▓██ ▐▓
|
||||
▓▒ ▐██▌ █ █ ▓██░ ▐█
|
||||
▓ ▐▐ █ █ ▐▐ █
|
||||
█ █ █ █
|
||||
█ █ ▄▄▄ █ █
|
||||
█ █ ▄▀▀ ▀▀▓▄ █ █
|
||||
█ █ ▄▌ ▀▓ █ █
|
||||
▐█ █ ▓▀ ▐█ █ ▓▒
|
||||
▐▌ █ ▐▓ ▐▌ ▐█ ▓▒
|
||||
▐▌ █ █ █ ▐█ ▐▌
|
||||
▐▌ ▓░ █ █ ▐▌ ▐▌
|
||||
▓▒ ▓░ █ ▓▒ ▐▌ ▐▓
|
||||
▓░ ▓░ ▐▌ ▀▌ ▐▌ ▐█
|
||||
▀▌▄▄ ▓▄▄ ▐█ ▓▌ ▄▄▄▐▌ ▄▄▄▀
|
||||
▐▐▐▀▀▀▀▐▐▐ ▐▐▀▀▀▀▀▀▐▐
|
||||
`;
|
||||
|
||||
/** @interface */
|
||||
export class JustLoggedInState {
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
// Show welcome screen
|
||||
onAttach() {
|
||||
this.session.sendText(castle);
|
||||
this.session.sendText(["", "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.sendText("You haven't got any characters, so let's make some\n\n");
|
||||
this.session.setState(new PartyCreationState(this.session));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const character of this.session.player.characters) {
|
||||
this.session.sendText(`Character: ${character.name} (${character.foundation})`);
|
||||
}
|
||||
|
||||
this.session.setState(new AwaitCommandsState(this.session));
|
||||
}
|
||||
}
|
||||
97
scenes/partyCreation/partyCreationScene.js
Executable file
97
scenes/partyCreation/partyCreationScene.js
Executable file
@@ -0,0 +1,97 @@
|
||||
import figlet from "figlet";
|
||||
import { Session } from "../models/session.js";
|
||||
import { WebsocketMessage } from "../utils/messages.js";
|
||||
import { frameText } from "../utils/tui.js";
|
||||
import { Config } from "../config.js";
|
||||
import { State } from "./state.js";
|
||||
|
||||
export class PartyCreationState extends State {
|
||||
/**
|
||||
* @proteted
|
||||
* @type {(msg: WebsocketMessage) => }
|
||||
*
|
||||
* NOTE: Should this be a stack?
|
||||
*/
|
||||
_dynamicMessageHandler;
|
||||
|
||||
/** @param {Session} session */
|
||||
constructor(session) {
|
||||
/** @type {Session} */
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
/** We attach (and execute) the next state */
|
||||
onAttach() {
|
||||
const charCount = this.session.player.characters.size;
|
||||
|
||||
//NOTE: could use async to optimize performance
|
||||
const createPartyLogo = frameText(figlet.textSync("Create Your Party"), {
|
||||
vPadding: 0,
|
||||
frameChars: "§=§§§§§§",
|
||||
});
|
||||
|
||||
this.sendText(createPartyLogo, { preformatted: true });
|
||||
|
||||
this.session.sendText([
|
||||
"",
|
||||
`Current party size: ${charCount}`,
|
||||
`Max party size: ${Config.maxPartySize}`,
|
||||
]);
|
||||
const min = 1;
|
||||
const max = Config.maxPartySize - charCount;
|
||||
const prompt = [
|
||||
`Please enter an integer between ${min} - ${max}`,
|
||||
"((type *:help* to get more info about party size))",
|
||||
];
|
||||
|
||||
this.sendText(`You can create a party with ${min} - ${max} characters, how big should your party be?`);
|
||||
|
||||
/** @param {WebsocketMessage} message */
|
||||
this.sendPrompt(prompt, (m) => this.receivePlayerCount(m));
|
||||
}
|
||||
|
||||
/** @param {WebsocketMessage} m */
|
||||
receivePlayerCount(m) {
|
||||
if (m.isHelpRequest()) {
|
||||
return this.partySizeHelp();
|
||||
}
|
||||
|
||||
if (!m.isInteger()) {
|
||||
this.sendError("You didn't enter an integer");
|
||||
this.sendPrompt(prompt, (m) => this.receivePlayerCount(m));
|
||||
return;
|
||||
}
|
||||
|
||||
const numCharactersToCreate = Number(message.text);
|
||||
if (numCharactersToCreate > max) {
|
||||
this.sendError("Number too high");
|
||||
this.sendPrompt(prompt, (m) => this.receivePlayerCount(m));
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCharactersToCreate < min) {
|
||||
this.sendError("Number too low");
|
||||
this.sendPrompt(prompt, (m) => this.receivePlayerCount(m));
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendText(`Let's create ${numCharactersToCreate} character(s) for you :)`);
|
||||
}
|
||||
|
||||
partySizeHelp() {
|
||||
this.sendText([
|
||||
`Your party can consist of 1 to ${mps} characters.`,
|
||||
"",
|
||||
"* Large parties tend live longer",
|
||||
`* If you have fewer than ${Config.maxPartySize} characters, you can`,
|
||||
" hire extra characters in your local inn.",
|
||||
"* large parties level slower because there are more",
|
||||
" characters to share the Experience Points",
|
||||
"* The individual members of small parties get better",
|
||||
" loot because they don't have to share, but it",
|
||||
" a lot of skill to accumulate loot as fast a larger",
|
||||
" party can",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
79
scenes/playerCreation/createUasswprdPrompt.js
Normal file
79
scenes/playerCreation/createUasswprdPrompt.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Prompt } from "../prompt.js";
|
||||
import * as security from "../../utils/security.js";
|
||||
import { Config } from "../../config.js";
|
||||
|
||||
export class CreatePasswordPrompt extends Prompt {
|
||||
//
|
||||
promptText = ["Enter a password"];
|
||||
|
||||
//
|
||||
// Let the client know that we're asking for a password
|
||||
// so it can set <input type="password">
|
||||
promptOptions = { password: true };
|
||||
|
||||
get player() {
|
||||
return this.scene.player;
|
||||
}
|
||||
|
||||
onReply(text) {
|
||||
//
|
||||
// Check of the password is sane. This is both bad from a security point
|
||||
// of view, and technically not necessary as insane passwords couldn't
|
||||
// reside in the player lists. However, let's save some CPU cycles on
|
||||
// not hashing an insane password 1000+ times.
|
||||
// This is technically bad practice, but since this is just a game,
|
||||
// do it anyway.
|
||||
if (!security.isPasswordSane(text)) {
|
||||
this.sendError("Insane password");
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Block users who enter bad passwords too many times.
|
||||
if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
|
||||
this.blockedUntil = Date.now() + Config.accountLockoutSeconds;
|
||||
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Handle blocked users.
|
||||
// They don't even get to have their password verified.
|
||||
if (this.player.blockedUntil > Date.now()) {
|
||||
//
|
||||
// Try to re-login too soon, and your lockout lasts longer.
|
||||
this.blockedUntil += Config.accountLockoutSeconds;
|
||||
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Verify the password against the hash we've stored.
|
||||
if (!security.verifyPassword(text, this.player.passwordHash)) {
|
||||
this.sendError("Incorrect password!");
|
||||
this.player.failedPasswordsSinceLastLogin++;
|
||||
|
||||
this.session.sendDebug(`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`);
|
||||
this.execute();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.lastSucessfulLoginAt = new Date();
|
||||
this.player.failedPasswordsSinceLastLogin = 0;
|
||||
|
||||
//
|
||||
// We do not allow a player to be logged in more than once!
|
||||
if (this.player.loggedIn) {
|
||||
this.calamity("This player is already logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.passwordAccepted();
|
||||
|
||||
//
|
||||
// Password was correct, go to main game
|
||||
this.session.setState(new JustLoggedInState(this.session));
|
||||
}
|
||||
}
|
||||
62
scenes/playerCreation/createUsernamePrompt.js
Executable file
62
scenes/playerCreation/createUsernamePrompt.js
Executable file
@@ -0,0 +1,62 @@
|
||||
import { Prompt } from "../prompt.js";
|
||||
import * as security from "../../utils/security.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
import { PlayerCreationScene } from "./playerCreationSene.js";
|
||||
import { Config } from "../../config.js";
|
||||
|
||||
export class CreateUsernamePrompt extends Prompt {
|
||||
//
|
||||
promptText = [
|
||||
"Enter your username", //
|
||||
"((type *:help* for more info))", //
|
||||
];
|
||||
|
||||
//
|
||||
// When player types :help
|
||||
helpText = [
|
||||
"Your username.",
|
||||
"It's used, along with your password, when you log in.",
|
||||
"Other players can see it.",
|
||||
"Other players can use it to chat or trade with you",
|
||||
"It may only consist of the letters _a-z_, _A-Z_, _0-9_, and _underscore_",
|
||||
];
|
||||
|
||||
//
|
||||
// Let the client know that we're asking for a username
|
||||
promptOptions = { username: true };
|
||||
|
||||
/**
|
||||
* @returns {PlayerCreationScene}
|
||||
*/
|
||||
get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
onReply(username) {
|
||||
//
|
||||
// do basic syntax checks on usernames
|
||||
if (!security.isUsernameSane(username)) {
|
||||
console.info("Someone entered insane username: '%s'", username);
|
||||
this.sendError("Incorrect username, try again.");
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// try and fetch the player object from the game
|
||||
const player = gGame.getPlayer(username);
|
||||
|
||||
//
|
||||
// handle invalid username
|
||||
if (player) {
|
||||
console.info("Someone tried to create a user with an occupied username: '%s'", username);
|
||||
this.sendError("Occupied, try something else");
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Tell daddy that we're done
|
||||
this.scene.usernameAccepted(username);
|
||||
}
|
||||
}
|
||||
52
scenes/playerCreation/playerCreationSene.js
Executable file
52
scenes/playerCreation/playerCreationSene.js
Executable file
@@ -0,0 +1,52 @@
|
||||
import { Config } from "../../config.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
import { Scene } from "../scene.js";
|
||||
import { CreateUsernamePrompt } from "./createUsernamePrompt.js";
|
||||
|
||||
export class PlayerCreationScene extends Scene {
|
||||
introText = "= Create Player";
|
||||
|
||||
/** @protected @type {Player} */
|
||||
player;
|
||||
|
||||
/** @protected @type {string} */
|
||||
password;
|
||||
|
||||
onReady() {
|
||||
//
|
||||
// If there are too many players, stop allowing new players in.
|
||||
if (gGame._players.size >= Config.maxPlayers) {
|
||||
this.session.calamity("Server is full, no more players can be created");
|
||||
}
|
||||
|
||||
this.showPrompt(new CreateUsernamePrompt(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the player has entered a valid and available username.
|
||||
*
|
||||
* @param {string} username
|
||||
*/
|
||||
usernameAccepted(username) {
|
||||
const player = gGame.createPlayer(username);
|
||||
this.player = player;
|
||||
|
||||
this.session.sendSystemMessage("salt", player.salt);
|
||||
this.session.sendText(`Username _*${username}*_ is available, and I've reserved it for you :)`);
|
||||
|
||||
//
|
||||
this.session.sendError("TODO: create a createPasswordPrompt and display it.");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Called when the player has entered a password and confirmed it.
|
||||
*
|
||||
* @param {string} password
|
||||
*/
|
||||
passwordAccepted(password) {
|
||||
this.password = password;
|
||||
this.session.sendText("*_Success_* ✅ You will now be asked to log in again, sorry for that ;)");
|
||||
this.player.setPasswordHash(security.generateHash(this.password));
|
||||
}
|
||||
}
|
||||
212
scenes/prompt.js
Executable file
212
scenes/prompt.js
Executable file
@@ -0,0 +1,212 @@
|
||||
import figlet from "figlet";
|
||||
import { gGame } from "../models/globals.js";
|
||||
import { Session } from "../models/session.js";
|
||||
import { Scene } from "./scene.js";
|
||||
import { MessageType, WebsocketMessage } from "../utils/messages.js";
|
||||
import { mustBe, mustBeString } from "../utils/mustbe.js";
|
||||
import { sprintf } from "sprintf-js";
|
||||
|
||||
/**
|
||||
* @typedef {object} PromptMethods
|
||||
* @property {function(...any): any} [onColon_*] - Any method starting with "onColon_"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @implements {PromptMethods}
|
||||
* @class
|
||||
* @dynamic Methods are dynamically created:
|
||||
* - onColon(...)
|
||||
*/
|
||||
export class Prompt {
|
||||
/** @private @readonly @type {Scene} */
|
||||
_scene;
|
||||
|
||||
/** @type {Scene} */
|
||||
get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
//
|
||||
// Extra info about the prompt we send to the client.
|
||||
promptOptions = undefined;
|
||||
|
||||
/**
|
||||
* Dictionary of help topics.
|
||||
* Keys: string matching /^[a-z]+$/ (topic name)
|
||||
* Values: string containing the help text
|
||||
*
|
||||
* @constant
|
||||
* @readonly
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
helpText = {
|
||||
"": "Sorry, no help available. Figure it out yourself, adventurer", // default help text
|
||||
};
|
||||
|
||||
/** @type {string|string[]} Default prompt text to send if we don't want to send something in the execute() call. */
|
||||
promptText = [
|
||||
"Please enter some very important info", // Stupid placeholder text
|
||||
"((or type :quit to run away))", // strings in double parentheses is rendered shaded/faintly
|
||||
];
|
||||
|
||||
/** @type {object|string} If string: the prompt's context (username, password, etc) of object, it's all the message's options */
|
||||
promptOptions = {};
|
||||
|
||||
/** @type {Session} */
|
||||
get session() {
|
||||
return this.scene.session;
|
||||
}
|
||||
|
||||
/** @param {Scene} scene */
|
||||
constructor(scene) {
|
||||
if (!(scene instanceof Scene)) {
|
||||
throw new Error("Expected an instance of >>Scene<< but got " + typeof scene);
|
||||
}
|
||||
this._scene = scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the prompt has been attached to a scene, and is ready to go.
|
||||
*
|
||||
* It's here you want to send the prompt text via the sendPrompt() method
|
||||
*/
|
||||
execute() {
|
||||
this.sendPrompt(this.promptText, this.promptOptions);
|
||||
}
|
||||
|
||||
/** Triggered when user types `:help` without any topic */
|
||||
onHelp(topic) {
|
||||
let h = this.helpText;
|
||||
if (typeof h === "string" || Array.isArray(h)) {
|
||||
h = { "": h };
|
||||
}
|
||||
|
||||
//
|
||||
// Fix data formatting shorthand
|
||||
// So lazy dev set help = "fooo" instead of help = { "": "fooo" }.
|
||||
if (h[topic]) {
|
||||
this.sendText(h[topic]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.onHelpFallback(topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a user types a :command that begins with a colon
|
||||
*
|
||||
* @param {string} command
|
||||
* @param {any[]} args
|
||||
*/
|
||||
|
||||
onColon(command, args) {
|
||||
const methodName = "onColon__" + command;
|
||||
const property = this[methodName];
|
||||
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
|
||||
//
|
||||
// If the prompt 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 (typeof property === "string") {
|
||||
this.sendText(property);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// We found a property that has the right name but the wrong type.
|
||||
throw new Error(
|
||||
[
|
||||
`Logic error. Prompt has a handler for a command called ${command}`,
|
||||
`but it is neither a function or a string, but a ${typeof property}`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the player asks for help on a topic, and we dont have an onHelp_thatParticularTopic method.
|
||||
*
|
||||
* @param {string} topic
|
||||
*/
|
||||
onHelpFallback(topic) {
|
||||
this.sendError(`Sorry, no help available for topic “${topic}”`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ":quit" messages
|
||||
*
|
||||
* The session will terminate no matter what. This just gives the State a chance to clean up before dying.
|
||||
*
|
||||
* @param {WebsocketMessage} message
|
||||
*
|
||||
*/
|
||||
onQuit() {}
|
||||
|
||||
/**
|
||||
* Triggered when the player replies to the prompt-message sent by this prompt-object.
|
||||
*
|
||||
* @param {WebsocketMessage} message The incoming reply
|
||||
*/
|
||||
onReply(message) {}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {string|string[]} text The prompt message (the request to get the user to enter some info).
|
||||
* @param {string} context
|
||||
*/ /**
|
||||
* @overload
|
||||
* @param {string|string[]} text The prompt message (the request to get the user to enter some info).
|
||||
* @param {object} options Any options for the text (client side text formatting, color-, font-, or style info, etc.).
|
||||
*/
|
||||
sendPrompt(...args) {
|
||||
this.session.sendPrompt(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text to be displayed to the client
|
||||
*
|
||||
* @param {string|string[]} text Text to send. If array, it will be joined/imploded with newline characters.
|
||||
* @param {object?} options message options for the client.
|
||||
*/
|
||||
sendText(...args) {
|
||||
this.session.sendText(...args);
|
||||
}
|
||||
|
||||
/** @param {string} errorMessage */
|
||||
sendError(...args) {
|
||||
this.session.sendError(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} systemMessageType The subtype of the system message (dev, salt, username, etc.)
|
||||
* @param {any?} value
|
||||
*/
|
||||
sendSystemMessage(...args) {
|
||||
this.session.sendSystemMessage(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a calamity text and then close the connection.
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
calamity(...args) {
|
||||
this.session.calamity();
|
||||
}
|
||||
|
||||
//
|
||||
// Easter ægg
|
||||
// Example of having a string as a colon-handler
|
||||
onColon__pull_out_wand = "You cannot pull out your wand right now! But thanks for trying 😘🍌🍆";
|
||||
}
|
||||
128
scenes/scene.js
Executable file
128
scenes/scene.js
Executable file
@@ -0,0 +1,128 @@
|
||||
import { sprintf } from "sprintf-js";
|
||||
import { Session } from "../models/session.js";
|
||||
import { Prompt } from "./prompt.js";
|
||||
|
||||
/**
|
||||
* Scene - a class for showing one or more prompts in a row.
|
||||
*
|
||||
* Scenes are mostly there to keep track of which prompt to show,
|
||||
* and to store data for subsequent prompts to access.
|
||||
*
|
||||
* The prompts themselves are responsible for data validation and
|
||||
* interpretation.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class Scene {
|
||||
/**
|
||||
* @type {string|string[]} This text is shown when the scene begins
|
||||
*/
|
||||
introText = "";
|
||||
|
||||
/** @readonly @constant @type {Session} */
|
||||
_session;
|
||||
get session() {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Prompt that is currently active.
|
||||
* I.e. the handler for the latest question we asked.
|
||||
*
|
||||
* @readonly
|
||||
* @type {Prompt}
|
||||
*/
|
||||
_prompt;
|
||||
get prompt() {
|
||||
return this._prompt;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
/** @param {Session} session */
|
||||
execute(session) {
|
||||
this._session = session;
|
||||
if (this.introText) {
|
||||
this.session.sendText(this.introText);
|
||||
}
|
||||
this.onReady();
|
||||
}
|
||||
|
||||
/** @abstract */
|
||||
onReady() {
|
||||
throw new Error("Abstract method must be implemented by subclass");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Prompt} prompt
|
||||
*/
|
||||
showPrompt(prompt) {
|
||||
this._prompt = prompt;
|
||||
prompt.execute();
|
||||
}
|
||||
|
||||
/** @param {new (scene: Scene) => Prompt} promptClassReference */
|
||||
show(promptClassReference) {
|
||||
this.showPrompt(new promptClassReference(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a user types a :command that begins with a colon
|
||||
* and the current Prompt cannot handle that command.
|
||||
*
|
||||
* @param {string} command
|
||||
* @param {any[]} args
|
||||
*/
|
||||
onColon(command, args) {
|
||||
const propertyName = "onColon__" + command;
|
||||
const property = this[propertyName];
|
||||
|
||||
//
|
||||
// Default: we have no handler for the Foo command
|
||||
if (property === undefined) {
|
||||
this.session.sendError(`You cannot ${command.toUpperCase()} right now`); // :foo ==> you cannot FOO right now
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// If the prompt 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 (typeof property === "string") {
|
||||
this.session.sendText(property);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// We found a property that has the right name but the wrong type.
|
||||
throw new Error(
|
||||
[
|
||||
`Logic error. Scene has a handler for a command called ${command}`,
|
||||
`but it is neither a function or a string, but a ${typeof property}`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Easter ægg
|
||||
// Example dynamic colon handler
|
||||
/** @param {any[]} args */
|
||||
onColon__imperial(args) {
|
||||
if (args.length === 0) {
|
||||
this.session.sendText("The imperial system is the freeest system ever. Also the least good");
|
||||
}
|
||||
|
||||
const n = Number(args[0]);
|
||||
|
||||
this.session.sendText(
|
||||
sprintf("%.2f centimeters is only %.2f inches. This is american wands are so short!", n, n / 2.54),
|
||||
);
|
||||
}
|
||||
|
||||
onColon__hi = "Hoe";
|
||||
}
|
||||
Reference in New Issue
Block a user