Compare commits
10 Commits
15f648535c
...
7ecb4f724b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ecb4f724b | ||
|
|
de96d45ade | ||
|
|
87f8add864 | ||
|
|
4c2b2dcdfe | ||
|
|
cda8392795 | ||
|
|
6a25b15530 | ||
|
|
3ce96deeea | ||
|
|
9de5140e47 | ||
|
|
bebd4ce944 | ||
|
|
ccd0f248fc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
resources
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
@@ -398,14 +398,14 @@ class MUDClient {
|
||||
// prompted.
|
||||
// In fact, we should ALWAYS be in a state of just-having-been-prompted.
|
||||
handlePromptMessage(data) {
|
||||
let [promptText, options = {}] = data;
|
||||
let [prompt, options = {}] = data;
|
||||
|
||||
this.shouldReply = true;
|
||||
|
||||
this.promptOptions = { ...{ class: "prompt" }, ...options };
|
||||
|
||||
//
|
||||
this.writeToOutput(promptText, this.promptOptions);
|
||||
this.writeToOutput(prompt, this.promptOptions);
|
||||
|
||||
//
|
||||
// The server has asked for a password, so we set the
|
||||
|
||||
@@ -13,35 +13,47 @@ import { Orientation } from "./ascii_types";
|
||||
/** Dungeon Generator - generates TileMaps populated with rooms, traps, encounters, etc. */
|
||||
class DungeonFactory {
|
||||
/** @type {number} */
|
||||
roomCount;
|
||||
#roomCount;
|
||||
|
||||
/** @type {RoomConfig[]} */
|
||||
rooms;
|
||||
#rooms;
|
||||
|
||||
/** @type {TileMap} */
|
||||
map;
|
||||
#map;
|
||||
|
||||
get roomCount() {
|
||||
return this.#roomCount;
|
||||
}
|
||||
|
||||
get rooms() {
|
||||
return this.#rooms;
|
||||
}
|
||||
|
||||
get map() {
|
||||
return this.#map;
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.map.width;
|
||||
return this.#map.width;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.map.height;
|
||||
return this.#map.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {number} roomCount
|
||||
* @param {number} #roomCount
|
||||
*/
|
||||
constructor(width, height, roomCount) {
|
||||
this.roomCount = roomCount | 0;
|
||||
this.rooms = [];
|
||||
this.#roomCount = roomCount | 0;
|
||||
this.#rooms = [];
|
||||
|
||||
// 2d array of pure wall tiles
|
||||
const tiles = new Array(height | 0).fill().map(() => Array(width | 0).fill(Tile.createWall()));
|
||||
|
||||
this.map = new TileMap(tiles);
|
||||
this.#map = new TileMap(tiles);
|
||||
}
|
||||
|
||||
generate() {
|
||||
@@ -52,18 +64,18 @@ class DungeonFactory {
|
||||
this.addFeatures();
|
||||
this.addPlayerStart();
|
||||
this.addPortals();
|
||||
return this.map.toString(CharType.TYPE_ID);
|
||||
return this.#map.toString(CharType.TYPE_ID);
|
||||
}
|
||||
|
||||
generateRooms() {
|
||||
this.rooms = [];
|
||||
const maxAttempts = this.roomCount * 10;
|
||||
this.#rooms = [];
|
||||
const maxAttempts = this.#roomCount * 10;
|
||||
let attempts = 0;
|
||||
|
||||
while (this.rooms.length < this.roomCount && attempts < maxAttempts) {
|
||||
while (this.#rooms.length < this.#roomCount && attempts < maxAttempts) {
|
||||
const room = this.generateRoom();
|
||||
if (room && !this.roomOverlaps(room)) {
|
||||
this.rooms.push(room);
|
||||
this.#rooms.push(room);
|
||||
this.carveRoom(room);
|
||||
}
|
||||
attempts++;
|
||||
@@ -83,7 +95,7 @@ class DungeonFactory {
|
||||
}
|
||||
|
||||
roomOverlaps(newRoom) {
|
||||
return this.rooms.some(
|
||||
return this.#rooms.some(
|
||||
(room) =>
|
||||
newRoom.x < room.x + room.width + 2 &&
|
||||
newRoom.x + newRoom.width + 2 > room.x &&
|
||||
@@ -95,26 +107,26 @@ class DungeonFactory {
|
||||
carveRoom(room) {
|
||||
for (let y = room.y; y < room.y + room.height; y++) {
|
||||
for (let x = room.x; x < room.x + room.width; x++) {
|
||||
this.map.tiles[y][x] = Tile.createFloor();
|
||||
this.#map.tiles[y][x] = Tile.createFloor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectRooms() {
|
||||
if (this.rooms.length < 2) return;
|
||||
if (this.#rooms.length < 2) return;
|
||||
|
||||
// Connect each room to at least one other room
|
||||
for (let i = 1; i < this.rooms.length >> 1; i++) {
|
||||
const roomA = this.rooms[i - 1];
|
||||
const roomB = this.rooms[i];
|
||||
for (let i = 1; i < this.#rooms.length >> 1; i++) {
|
||||
const roomA = this.#rooms[i - 1];
|
||||
const roomB = this.#rooms[i];
|
||||
this.createCorridor(roomA, roomB);
|
||||
}
|
||||
|
||||
// Add some extra connections for more interesting layouts
|
||||
const extraConnections = Math.floor(this.rooms.length / 3);
|
||||
const extraConnections = Math.floor(this.#rooms.length / 3);
|
||||
for (let i = 0; i < extraConnections; i++) {
|
||||
const roomA = this.rooms[this.random(0, this.rooms.length - 1)];
|
||||
const roomB = this.rooms[this.random(0, this.rooms.length - 1)];
|
||||
const roomA = this.#rooms[this.random(0, this.#rooms.length - 1)];
|
||||
const roomB = this.#rooms[this.random(0, this.#rooms.length - 1)];
|
||||
if (roomA !== roomB) {
|
||||
this.createCorridor(roomA, roomB);
|
||||
}
|
||||
@@ -138,7 +150,7 @@ class DungeonFactory {
|
||||
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
//
|
||||
if (this.map.get(x, y).isWall()) {
|
||||
if (this.#map.get(x, y).isWall()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,7 +192,7 @@ class DungeonFactory {
|
||||
row.push(Tile.createWall()); // Initial wall tile on this row
|
||||
for (let x = dungeonStartX; x <= dungeonEndX; x++) {
|
||||
/**/
|
||||
const tile = this.map.get(x, y);
|
||||
const tile = this.#map.get(x, y);
|
||||
row.push(tile);
|
||||
}
|
||||
row.push(Tile.createWall()); // Final wall tile on this row
|
||||
@@ -190,7 +202,7 @@ class DungeonFactory {
|
||||
// Final row is all walls
|
||||
newTiles.push(new Array(newWidth).fill(Tile.createWall()));
|
||||
|
||||
this.map = new TileMap(newTiles);
|
||||
this.#map = new TileMap(newTiles);
|
||||
}
|
||||
|
||||
createCorridor(roomA, roomB) {
|
||||
@@ -220,7 +232,7 @@ class DungeonFactory {
|
||||
|
||||
while (x !== x2 || y !== y2) {
|
||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
||||
this.map.tiles[y][x] = Tile.createFloor();
|
||||
this.#map.tiles[y][x] = Tile.createFloor();
|
||||
}
|
||||
|
||||
if (x !== x2) x += dx;
|
||||
@@ -229,7 +241,7 @@ class DungeonFactory {
|
||||
|
||||
// Ensure endpoint is carved
|
||||
if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) {
|
||||
this.map.tiles[y2][x2] = Tile.createFloor();
|
||||
this.#map.tiles[y2][x2] = Tile.createFloor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,14 +250,14 @@ class DungeonFactory {
|
||||
for (let y = 1; y < this.height - 1; y++) {
|
||||
//
|
||||
for (let x = 1; x < this.width - 1; x++) {
|
||||
const cell = this.map.get(x, y);
|
||||
const cell = this.#map.get(x, y);
|
||||
|
||||
if (!cell) {
|
||||
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.map.get(x, y).isFloor()) {
|
||||
if (this.#map.get(x, y).isFloor()) {
|
||||
walkabilityCache.push([x, y]);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +275,7 @@ class DungeonFactory {
|
||||
|
||||
for (let [x, y] of walkabilityCache) {
|
||||
//
|
||||
const walkable = (offsetX, offsetY) => this.map.isFloorLike(x + offsetX, y + offsetY);
|
||||
const walkable = (offsetX, offsetY) => this.#map.isFloorLike(x + offsetX, y + offsetY);
|
||||
|
||||
const surroundingFloorCount =
|
||||
0 +
|
||||
@@ -283,7 +295,7 @@ class DungeonFactory {
|
||||
|
||||
if (surroundingFloorCount >= 7) {
|
||||
// MAGIC NUMBER 7
|
||||
this.map.tiles[y][x] = Tile.createWall();
|
||||
this.#map.tiles[y][x] = Tile.createWall();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,14 +305,14 @@ class DungeonFactory {
|
||||
for (let y = 1; y < this.height - 1; y++) {
|
||||
//
|
||||
for (let x = 1; x < this.width - 1; x++) {
|
||||
const cell = this.map.get(x, y);
|
||||
const cell = this.#map.get(x, y);
|
||||
|
||||
if (!cell) {
|
||||
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.map.isFloorLike(x, y)) {
|
||||
if (this.#map.isFloorLike(x, y)) {
|
||||
walkabilityCache.push([x, y]);
|
||||
}
|
||||
}
|
||||
@@ -309,7 +321,7 @@ class DungeonFactory {
|
||||
const idx = this.random(0, walkabilityCache.length - 1);
|
||||
const [x, y] = walkabilityCache[idx];
|
||||
|
||||
const walkable = (offsetX, offsetY) => this.map.isFloorLike(x + offsetX, y + offsetY);
|
||||
const walkable = (offsetX, offsetY) => this.#map.isFloorLike(x + offsetX, y + offsetY);
|
||||
|
||||
//
|
||||
// When spawning in, which direction should the player be oriented?
|
||||
@@ -324,23 +336,23 @@ class DungeonFactory {
|
||||
// they don't face a wall upon spawning.
|
||||
const dirIdx = this.random(0, directions.length - 1);
|
||||
|
||||
this.map.tiles[y][x] = Tile.createPlayerStart(directions[dirIdx]);
|
||||
this.#map.tiles[y][x] = Tile.createPlayerStart(directions[dirIdx]);
|
||||
}
|
||||
|
||||
// Add portals to isolated areas
|
||||
addPortals() {
|
||||
let traversableTileCount = this.map.getFloorlikeTileCount();
|
||||
let traversableTileCount = this.#map.getFloorlikeTileCount();
|
||||
|
||||
//
|
||||
// Find the player's start point, and let this be the
|
||||
// bases of area 0
|
||||
const [x, y] = this.map.forEach((tile, x, y) => {
|
||||
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);
|
||||
const result = this.#map.getAllTraversableTilesConnectedTo(x, y);
|
||||
|
||||
if (result.size === traversableTileCount) {
|
||||
// There are no isolated areas, return
|
||||
@@ -385,7 +397,7 @@ class DungeonFactory {
|
||||
const floorTiles = [];
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
if (this.map.get(x, y).isFloor()) {
|
||||
if (this.#map.get(x, y).isFloor()) {
|
||||
floorTiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
@@ -405,11 +417,11 @@ class DungeonFactory {
|
||||
// }
|
||||
|
||||
// Add monsters
|
||||
const encouterCount = Math.min(5, this.rooms.length);
|
||||
const encouterCount = Math.min(5, this.#rooms.length);
|
||||
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("PLACEHOLDER_ENCOUNTER_ID");
|
||||
if (this.#map.tiles[pos.y][pos.x].isFloor()) {
|
||||
this.#map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint("PLACEHOLDER_ENCOUNTER_ID");
|
||||
// TODO: Add encounter to the dungeon's "roaming entities" array.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,6 @@ h2 {
|
||||
}
|
||||
|
||||
.faint {
|
||||
opacity: 0.42;
|
||||
opacity: 0.6;
|
||||
color: #44f;
|
||||
}
|
||||
|
||||
@@ -58,10 +58,13 @@ export class Character {
|
||||
itemSlots;
|
||||
|
||||
/** @type {Set<string>} Things the character is particularly proficient at. */
|
||||
skills = new Set();
|
||||
proficiencies = new Set();
|
||||
|
||||
/** @type {Map<Item,number} Things the character is particularly proficient at. */
|
||||
items = new Map();
|
||||
/** @type {Set<Item} Things the character is particularly proficient at. */
|
||||
items = new Set();
|
||||
|
||||
/** @type {string[]} */
|
||||
freeSlots = [];
|
||||
|
||||
/**
|
||||
* @param {string} name The name of the character
|
||||
@@ -72,26 +75,18 @@ export class Character {
|
||||
|
||||
/** Add an item to the equipment list
|
||||
* @param {Item} item
|
||||
* @param {number} count
|
||||
*
|
||||
* Maybe return the accumulated ItemSlots used?
|
||||
*/
|
||||
addItem(item, count = 1) {
|
||||
if (!Number.isInteger(count)) {
|
||||
throw new Error("Number must be an integer");
|
||||
}
|
||||
addItem(item) {
|
||||
if (!(item instanceof Item)) {
|
||||
console.debug("bad item", item);
|
||||
throw new Error("item must be an instance of Item!");
|
||||
}
|
||||
if (count <= 0) {
|
||||
throw new Error("Number must be > 0");
|
||||
|
||||
this.items.add(item)
|
||||
}
|
||||
|
||||
const existingItemCount = this.items.get(item) || 0;
|
||||
|
||||
this.items.set(item, count + existingItemCount);
|
||||
}
|
||||
|
||||
// todo removeItem(item, count)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { isIdSane, miniUid } from "../utils/id.js";
|
||||
import { Xorshift32 } from "../utils/random.js";
|
||||
import { Security } from "../utils/security.js";
|
||||
import { ItemBlueprint } from "./item.js";
|
||||
import { Player } from "./player.js";
|
||||
|
||||
@@ -17,13 +18,22 @@ import { Player } from "./player.js";
|
||||
/** @typedef {import("./item.js").ItemBlueprint} ItemBlueprint */
|
||||
|
||||
export class Game {
|
||||
_counter = 1_000_000;
|
||||
#counter = 1_000_000;
|
||||
get counter() {
|
||||
return this.#counter;
|
||||
}
|
||||
|
||||
/** @type {Map<string,ItemBlueprint>} List of all item blueprints in the game */
|
||||
_itemBlueprints = new Map();
|
||||
#itemBlueprints = new Map();
|
||||
get itemBlueprints() {
|
||||
return this.#itemBlueprints;
|
||||
}
|
||||
|
||||
/** @type {Map<string,Location>} The list of locations in the game */
|
||||
_locations = new Map();
|
||||
#locations = new Map();
|
||||
get locations() {
|
||||
return this.#locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* The characters in the game.
|
||||
@@ -31,34 +41,40 @@ export class Game {
|
||||
* @protected
|
||||
* @type {Map<string,Character>}
|
||||
*/
|
||||
_characters = new Map();
|
||||
#characters = new Map();
|
||||
get characters() {
|
||||
return this.#characters;
|
||||
}
|
||||
|
||||
/*
|
||||
* @protected
|
||||
* @type {Map<string,Player>} Map of users in the game username->Player
|
||||
*/
|
||||
_players = new Map();
|
||||
#players = new Map();
|
||||
get players() {
|
||||
return this.#players;
|
||||
}
|
||||
|
||||
/** @protected @type {Xorshift32} */
|
||||
_random;
|
||||
#random;
|
||||
|
||||
/** @type {Xorshift32} */
|
||||
get random() {
|
||||
return this._random;
|
||||
return this.#random;
|
||||
}
|
||||
|
||||
/** @param {number} rngSeed Seed number used for randomization */
|
||||
constructor() {
|
||||
this.rngSeed = Date.now();
|
||||
constructor(rngSeed) {
|
||||
this.seedRNG(rngSeed);
|
||||
}
|
||||
|
||||
set rngSeed(rngSeed) {
|
||||
this._random = new Xorshift32(rngSeed);
|
||||
seedRNG(rngSeed) {
|
||||
this.#random = new Xorshift32(rngSeed);
|
||||
}
|
||||
|
||||
getPlayerByUsername(username) {
|
||||
console.log("GETTING PLAYER: `%s`", username);
|
||||
return this._players.get(username);
|
||||
return this.#players.get(username);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,20 +84,19 @@ export class Game {
|
||||
* @param {string?} passwordHash
|
||||
* @param {string?} salt
|
||||
*
|
||||
* @returns {Player|null} Returns the player if username wasn't already taken, or null otherwise.
|
||||
* @returns {Player|false} Returns the player if username wasn't already taken, or null otherwise.
|
||||
*/
|
||||
createPlayer(username, passwordHash = undefined, salt = undefined) {
|
||||
if (this._players.has(username)) {
|
||||
if (this.#players.has(username)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const player = new Player(
|
||||
username,
|
||||
typeof passwordHash === "string" ? passwordHash : "",
|
||||
typeof salt === "string" && salt.length > 0 ? salt : miniUid(),
|
||||
);
|
||||
passwordHash ??= "";
|
||||
salt ??= Security.generateHash(miniUid());
|
||||
|
||||
this._players.set(username, player);
|
||||
const player = new Player(username, passwordHash, salt);
|
||||
|
||||
this.#players.set(username, player);
|
||||
|
||||
return player;
|
||||
}
|
||||
@@ -99,7 +114,7 @@ export class Game {
|
||||
throw new Error("Invalid blueprintId!");
|
||||
}
|
||||
|
||||
const existing = this._itemBlueprints.get(blueprintId);
|
||||
const existing = this.#itemBlueprints.get(blueprintId);
|
||||
|
||||
if (existing) {
|
||||
console.warn("we tried to create the same item blueprint more than once", blueprintId, attributes);
|
||||
@@ -110,7 +125,7 @@ export class Game {
|
||||
|
||||
const result = new ItemBlueprint(attributes);
|
||||
|
||||
this._itemBlueprints.set(blueprintId, result);
|
||||
this.#itemBlueprints.set(blueprintId, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -123,6 +138,6 @@ export class Game {
|
||||
if (!isIdSane(blueprintId)) {
|
||||
throw new Error(`blueprintId >>${blueprintId}<< is not a valid id`);
|
||||
}
|
||||
return this._itemBlueprints.get(blueprintId);
|
||||
return this.#itemBlueprints.get(blueprintId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Config } from "../config.js";
|
||||
import { Game } from "./game.js";
|
||||
|
||||
/** @constant @readonly @type {Game} Global instance of Game */
|
||||
export const gGame = new Game();
|
||||
export const gGame = new Game(Config.rngSeed);
|
||||
|
||||
@@ -41,6 +41,9 @@ export class ItemAttributes {
|
||||
|
||||
/** @constant @readonly @type {string[]} Type of ammo that this item is, or that this item uses */
|
||||
skills = [];
|
||||
|
||||
/** @constant @readonly @type {boolean} Can a person wearing this armor be stealthy? */
|
||||
sneak;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +56,7 @@ export class ItemBlueprint extends ItemAttributes {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {object} o Object whose attributes we copy
|
||||
* @param {ItemAttributes} o Object whose attributes we copy
|
||||
*/
|
||||
constructor(o) {
|
||||
super();
|
||||
|
||||
@@ -9,28 +9,28 @@
|
||||
* or magical portals to distant locations.
|
||||
*/
|
||||
export class Location {
|
||||
/** @protected @type {string} */
|
||||
_id;
|
||||
/** @type {string} */
|
||||
#id;
|
||||
get id() {
|
||||
return this._id;
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
/** @protected @type {string} */
|
||||
_name;
|
||||
/** @type {string} */
|
||||
#name;
|
||||
get name() {
|
||||
return this._name;
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
/** @protected @type {string} */
|
||||
_description;
|
||||
/** @type {string} */
|
||||
#description;
|
||||
get description() {
|
||||
return this._description;
|
||||
return this.#description;
|
||||
}
|
||||
|
||||
/** @protected @type {Map<string,Portal>} */
|
||||
_portals = new Map();
|
||||
/** @type {Map<string,Portal>} */
|
||||
#portals = new Map();
|
||||
get portals() {
|
||||
return this._portals;
|
||||
return this.#portals;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +39,8 @@ export class Location {
|
||||
* @param {string} description
|
||||
*/
|
||||
constructor(id, name, description) {
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._description = description;
|
||||
this.#id = id;
|
||||
this.#name = name;
|
||||
this.#description = description;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import WebSocket from "ws";
|
||||
import { Character } from "./character.js";
|
||||
import { Config } from "./../config.js";
|
||||
import { Scene } from "../scenes/scene.js";
|
||||
|
||||
/** @typedef {import("../scenes/scene.js").Scene} Scene */
|
||||
/** @typedef {import("./characer.js").Character} Character */
|
||||
/** @typedef {import("ws").Websocket} Websocket */
|
||||
|
||||
/**
|
||||
* Player Account.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Player } from "./player.js";
|
||||
import { mustBeString, mustBe } from "../utils/mustbe.js";
|
||||
import { Scene } from "../scenes/scene.js";
|
||||
import { formatMessage, MessageType } from "../utils/messages.js";
|
||||
import * as Messages from "../utils/messages.js";
|
||||
|
||||
/** @typedef {import("ws").WebSocket} WebSocket */
|
||||
|
||||
@@ -42,7 +42,8 @@ export class Session {
|
||||
* @param {Scene} scene
|
||||
*/
|
||||
setScene(scene) {
|
||||
console.debug("changing scene", scene.constructor.name);
|
||||
this.frankofil = stil;
|
||||
console.debug("Changing scene", { scene: scene.constructor.name });
|
||||
if (!(scene instanceof Scene)) {
|
||||
throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`);
|
||||
}
|
||||
@@ -91,7 +92,7 @@ export class Session {
|
||||
console.error("Trying to send a message without a valid websocket", messageType, args);
|
||||
return;
|
||||
}
|
||||
this._websocket.send(formatMessage(messageType, ...args));
|
||||
this._websocket.send(Messages.formatMessage(messageType, ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +113,7 @@ export class Session {
|
||||
}
|
||||
|
||||
this.send(
|
||||
MessageType.PROMPT, // message type
|
||||
Messages.PROMPT, // message type
|
||||
text, // TODO: prompt text must be string or an array of strings
|
||||
mustBe(options, "object"),
|
||||
);
|
||||
@@ -125,12 +126,17 @@ export class Session {
|
||||
* @param {object?} options message options for the client.
|
||||
*/
|
||||
sendText(text, options = {}) {
|
||||
this.send(MessageType.TEXT, text, options);
|
||||
this.send(Messages.TEXT, text, options);
|
||||
}
|
||||
|
||||
/** @param {string|string[]} errorMessage */
|
||||
sendError(errorMessage, options = { verbatim: true, error: true }) {
|
||||
this.send(MessageType.ERROR, mustBeString(errorMessage), options);
|
||||
this.send(Messages.ERROR, mustBeString(errorMessage), options);
|
||||
}
|
||||
|
||||
/** @param {string|string[]} debugMessage */
|
||||
sendDebug(debugMessage, options = { verbatim: true, debug: true }) {
|
||||
this.send(Messages.DEBUG, debugMessage, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +147,7 @@ export class Session {
|
||||
//
|
||||
// The client should know not to format calamaties anyway, but we add “preformatted” anyway
|
||||
console.info("CALAMITY", errorMessage);
|
||||
this.send(MessageType.CALAMITY, errorMessage, { verbatim: true, calamity: true });
|
||||
this.send(Messages.CALAMITY, errorMessage, { verbatim: true, calamity: true });
|
||||
this.close();
|
||||
}
|
||||
|
||||
@@ -150,6 +156,6 @@ export class Session {
|
||||
* @param {any?} value
|
||||
*/
|
||||
sendSystemMessage(systemMessageType, value = undefined) {
|
||||
this.send(MessageType.SYSTEM, mustBeString(systemMessageType), value);
|
||||
this.send(Messages.SYSTEM, mustBeString(systemMessageType), value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { Security } from "../../utils/security.js";
|
||||
import { Config } from "../../config.js";
|
||||
import { GameScene } from "../gameLoop/gameScene.js";
|
||||
import { PlayerCreationScene } from "../playerCreation/playerCreationSene.js";
|
||||
import { PlayerCreationScene } from "../playerCreation/playerCreationScene.js";
|
||||
import { Prompt } from "../prompt.js";
|
||||
import { Scene } from "../scene.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
|
||||
/** @typedef {import("../../models/player.js").Player} Player */
|
||||
|
||||
/** @property {Session} session */
|
||||
export class AuthenticationScene extends Scene {
|
||||
introText = [
|
||||
"= Welcome!", //
|
||||
];
|
||||
|
||||
/** @type {Player} */
|
||||
player;
|
||||
|
||||
onReady() {
|
||||
this.session.sendText("= Welcome");
|
||||
this.show(UsernamePrompt);
|
||||
}
|
||||
|
||||
@@ -57,25 +53,24 @@ export class AuthenticationScene extends Scene {
|
||||
// | __/| | | (_) | | | | | | |_) | |_
|
||||
// |_| |_| \___/|_| |_| |_| .__/ \__|
|
||||
// |_|
|
||||
/** @property {AuthenticationScene} scene */
|
||||
class UsernamePrompt extends Prompt {
|
||||
//
|
||||
promptText = [
|
||||
message = [
|
||||
"Please enter your username:", //
|
||||
"(((type *:create* if you want to create a new user)))", //
|
||||
"((type _*:help*_ to see your other options))",
|
||||
];
|
||||
|
||||
//
|
||||
// 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",
|
||||
help = [
|
||||
"Enter your username to proceed with loggin in",
|
||||
"Type _*:create*_ if you are not already registered, and want to create a new account",
|
||||
"Only a username and password is required - not your email",
|
||||
];
|
||||
options = { username: true };
|
||||
|
||||
//
|
||||
// Let the client know that we're asking for a username
|
||||
promptOptions = { username: true };
|
||||
|
||||
/** @returns {AuthenticationScene} */
|
||||
/** @returns {AuthenticationScene} workaround for proper type hinting */
|
||||
get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
@@ -126,12 +121,8 @@ class UsernamePrompt extends Prompt {
|
||||
// |_|
|
||||
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 };
|
||||
message = "Please enter your password";
|
||||
options = { password: true };
|
||||
|
||||
get player() {
|
||||
return this.scene.player;
|
||||
@@ -156,9 +147,11 @@ class PasswordPrompt extends Prompt {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this.scene.player;
|
||||
|
||||
//
|
||||
// Block users who enter bad passwords too many times.
|
||||
if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
|
||||
if (player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
|
||||
this.blockedUntil = Date.now() + Config.accountLockoutSeconds * 1000;
|
||||
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
||||
return;
|
||||
@@ -167,7 +160,7 @@ class PasswordPrompt extends Prompt {
|
||||
//
|
||||
// Handle blocked users.
|
||||
// They don't even get to have their password verified.
|
||||
if (this.player.blockedUntil > Date.now()) {
|
||||
if (player.blockedUntil > Date.now()) {
|
||||
//
|
||||
// Try to re-login too soon, and your lockout lasts longer.
|
||||
this.blockedUntil += Config.accountLockoutSeconds * 1000;
|
||||
@@ -177,23 +170,23 @@ class PasswordPrompt extends Prompt {
|
||||
|
||||
//
|
||||
// Verify the password against the hash we've stored.
|
||||
if (!Security.verifyPassword(text, this.player.passwordHash)) {
|
||||
if (!Security.verifyPassword(text, player.passwordHash)) {
|
||||
this.sendError("Incorrect password!");
|
||||
this.player.failedPasswordsSinceLastLogin++;
|
||||
player.failedPasswordsSinceLastLogin++;
|
||||
|
||||
this.session.sendDebug(`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`);
|
||||
this.session.sendDebug(`Failed login attempt #${player.failedPasswordsSinceLastLogin}`);
|
||||
this.execute();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.lastSucessfulLoginAt = new Date();
|
||||
this.player.failedPasswordsSinceLastLogin = 0;
|
||||
player.lastSucessfulLoginAt = new Date();
|
||||
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");
|
||||
if (player.loggedIn) {
|
||||
this.calamity("player is already logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Prompt } from "../prompt.js";
|
||||
import { Scene } from "../scene.js";
|
||||
|
||||
/**
|
||||
@@ -6,7 +7,38 @@ import { Scene } from "../scene.js";
|
||||
* It's here we listen for player commands.
|
||||
*/
|
||||
export class GameScene extends Scene {
|
||||
introText = `
|
||||
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.
|
||||
//
|
||||
// IF player has stored state THEN
|
||||
// restore it and resume [main flow]
|
||||
// END
|
||||
//
|
||||
// IF player has no characters THEN
|
||||
// go to createCharacterScene
|
||||
// END
|
||||
//
|
||||
// set player's current location = Hovedstad
|
||||
// display the welcome to Hovedstad stuff, and
|
||||
// await the player's commands.
|
||||
//
|
||||
//
|
||||
// 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.show(GameScenePlaceholderPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
class GameScenePlaceholderPrompt extends Prompt {
|
||||
message = `
|
||||
▄
|
||||
█▐▀▀▀▌▄
|
||||
█ ▐▀▀▀▌▌▓▌
|
||||
@@ -49,33 +81,6 @@ export class GameScene extends Scene {
|
||||
|
||||
|
||||
= Welcome to Hovedstad
|
||||
((type :quit to quit))
|
||||
`;
|
||||
|
||||
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.
|
||||
//
|
||||
// IF player has stored state THEN
|
||||
// restore it and resume [main flow]
|
||||
// END
|
||||
//
|
||||
// IF player has no characters THEN
|
||||
// go to createCharacterScene
|
||||
// END
|
||||
//
|
||||
// set player's current location = Hovedstad
|
||||
// display the welcome to Hovedstad stuff, and
|
||||
// await the player's commands.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
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";
|
||||
|
||||
// _____ ___ ____ ___ ____ _ _____
|
||||
// |_ _/ _ \| _ \ / _ \ _ / ___|___ _ ____ _____ _ __| |_ |_ _|__
|
||||
// | || | | | | | | | | (_) | | / _ \| '_ \ \ / / _ \ '__| __| | |/ _ \
|
||||
@@ -17,93 +10,93 @@ 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) {
|
||||
super();
|
||||
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} m */
|
||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
}
|
||||
|
||||
/** @param {WebsocketMessage} m */
|
||||
receiveCharacterCount(m) {
|
||||
if (m.isHelpRequest()) {
|
||||
return this.partySizeHelp();
|
||||
}
|
||||
|
||||
if (!m.isInteger()) {
|
||||
this.sendError("You didn't enter an integer");
|
||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
return;
|
||||
}
|
||||
|
||||
const numCharactersToCreate = Number(m.text);
|
||||
if (numCharactersToCreate > Config.maxPartySize) {
|
||||
this.sendError("Number too high");
|
||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCharactersToCreate < 1) {
|
||||
this.sendError("Number too low");
|
||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendText(`Let's create ${numCharactersToCreate} character(s) for you :)`);
|
||||
}
|
||||
|
||||
partySizeHelp() {
|
||||
this.sendText([
|
||||
`Your party can consist of 1 to ${Config.maxPartySize} 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.PI < 0 && Session && WebsocketMessage) {
|
||||
("STFU Linda");
|
||||
}
|
||||
// export class PartyCreationState extends State {
|
||||
// /**
|
||||
// * @proteted
|
||||
// * @type {(msg: WebsocketMessage) => }
|
||||
// *
|
||||
// * NOTE: Should this be a stack?
|
||||
// */
|
||||
// _dynamicMessageHandler;
|
||||
//
|
||||
// /** @param {Session} session */
|
||||
// constructor(session) {
|
||||
// super();
|
||||
// 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} m */
|
||||
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
// }
|
||||
//
|
||||
// /** @param {WebsocketMessage} m */
|
||||
// receiveCharacterCount(m) {
|
||||
// if (m.isHelpRequest()) {
|
||||
// return this.partySizeHelp();
|
||||
// }
|
||||
//
|
||||
// if (!m.isInteger()) {
|
||||
// this.sendError("You didn't enter an integer");
|
||||
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const numCharactersToCreate = Number(m.text);
|
||||
// if (numCharactersToCreate > Config.maxPartySize) {
|
||||
// this.sendError("Number too high");
|
||||
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (numCharactersToCreate < 1) {
|
||||
// this.sendError("Number too low");
|
||||
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// this.sendText(`Let's create ${numCharactersToCreate} character(s) for you :)`);
|
||||
// }
|
||||
//
|
||||
// partySizeHelp() {
|
||||
// this.sendText([
|
||||
// `Your party can consist of 1 to ${Config.maxPartySize} 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;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (Math.PI < 0 && Session && WebsocketMessage) {
|
||||
// ("STFU Linda");
|
||||
// }
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Prompt } from "../prompt.js";
|
||||
import { 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();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Prompt } from "../prompt.js";
|
||||
import { Security } from "../../utils/security.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
|
||||
/** @typedef {import("./playerCreationScene.js").PlayerCreationScene} PlayerCreationScene */
|
||||
|
||||
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.getPlayerByUsername(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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Config } from "../../config.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
import { Security } from "../../utils/security.js";
|
||||
import { Prompt } from "../prompt.js";
|
||||
import { Scene } from "../scene.js";
|
||||
import { CreateUsernamePrompt } from "./createUsernamePrompt.js";
|
||||
import { Security } from "../../utils/security.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
import { AuthenticationScene } from "../authentication/authenticationScene.js";
|
||||
|
||||
const MAX_PASSWORD_ATTEMPTS = 3;
|
||||
|
||||
export class PlayerCreationScene extends Scene {
|
||||
introText = "= Create Player";
|
||||
intro = "= Create Player";
|
||||
|
||||
/** @protected @type {Player} */
|
||||
player;
|
||||
@@ -16,11 +19,11 @@ export class PlayerCreationScene extends Scene {
|
||||
onReady() {
|
||||
//
|
||||
// If there are too many players, stop allowing new players in.
|
||||
if (gGame._players.size >= Config.maxPlayers) {
|
||||
if (gGame.players.size >= Config.maxPlayers) {
|
||||
this.session.calamity("Server is full, no more players can be created");
|
||||
}
|
||||
|
||||
this.showPrompt(new CreateUsernamePrompt(this));
|
||||
this.showPrompt(new UsernamePrompt(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,10 +36,10 @@ export class PlayerCreationScene extends Scene {
|
||||
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.");
|
||||
this.session.sendText(`Username _*${username}*_ has been reserved for you`);
|
||||
|
||||
this.show(PasswordPrompt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +50,153 @@ export class PlayerCreationScene extends Scene {
|
||||
*/
|
||||
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));
|
||||
this.session.sendText("*_Success_* ✅ You will now be asked to log in again, sorry about that ;)");
|
||||
this.session.setScene(new AuthenticationScene(this.session));
|
||||
}
|
||||
}
|
||||
|
||||
// _ _
|
||||
// | | | |___ ___ _ __ _ __ __ _ _ __ ___ ___
|
||||
// | | | / __|/ _ \ '__| '_ \ / _` | '_ ` _ \ / _ \
|
||||
// | |_| \__ \ __/ | | | | | (_| | | | | | | __/
|
||||
// \___/|___/\___|_| |_| |_|\__,_|_| |_| |_|\___|
|
||||
//
|
||||
// ____ _
|
||||
// | _ \ _ __ ___ _ __ ___ _ __ | |_
|
||||
// | |_) | '__/ _ \| '_ ` _ \| '_ \| __|
|
||||
// | __/| | | (_) | | | | | | |_) | |_
|
||||
// |_| |_| \___/|_| |_| |_| .__/ \__|
|
||||
// |_|
|
||||
class UsernamePrompt extends Prompt {
|
||||
//
|
||||
message = [
|
||||
"Enter your username", //
|
||||
"((type *:help* for more info))", //
|
||||
];
|
||||
|
||||
//
|
||||
// When player types :help
|
||||
help = [
|
||||
"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
|
||||
options = { username: true };
|
||||
|
||||
/** @type {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.getPlayerByUsername(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;
|
||||
}
|
||||
|
||||
this.scene.usernameAccepted(username);
|
||||
}
|
||||
}
|
||||
|
||||
// ____ _
|
||||
// | _ \ __ _ ___ _____ _____ _ __ __| |
|
||||
// | |_) / _` / __/ __\ \ /\ / / _ \| '__/ _` |
|
||||
// | __/ (_| \__ \__ \\ V V / (_) | | | (_| |
|
||||
// |_| \__,_|___/___/ \_/\_/ \___/|_| \__,_|
|
||||
//
|
||||
// ____ _
|
||||
// | _ \ _ __ ___ _ __ ___ _ __ | |_
|
||||
// | |_) | '__/ _ \| '_ ` _ \| '_ \| __|
|
||||
// | __/| | | (_) | | | | | | |_) | |_
|
||||
// |_| |_| \___/|_| |_| |_| .__/ \__|
|
||||
// |_|
|
||||
class PasswordPrompt extends Prompt {
|
||||
//
|
||||
message = "Enter a password";
|
||||
|
||||
//
|
||||
// Let the client know that we're asking for a password
|
||||
// so it can set <input type="password">
|
||||
options = { password: true };
|
||||
|
||||
/** @type {string?} Password that was previously entered. */
|
||||
firstPassword = undefined;
|
||||
|
||||
errorCount = 0;
|
||||
|
||||
/** @type {PlayerCreationScene} */
|
||||
get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
beforeExecute() {
|
||||
if (this.errorCount > MAX_PASSWORD_ATTEMPTS) {
|
||||
this.firstPassword = false;
|
||||
this.errorCount = 0;
|
||||
this.message = ["Too many errors - starting over", "Enter password"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.firstPassword && this.errorCount === 0) {
|
||||
this.message = "Repeat the password";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.firstPassword && this.errorCount > 0) {
|
||||
this.message = [
|
||||
"Repeat the password",
|
||||
`((attempt nr. ${this.errorCount + 1} of ${MAX_PASSWORD_ATTEMPTS + 1}))`,
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorCount = 0;
|
||||
this.message = "Enter a password";
|
||||
}
|
||||
|
||||
onReply(str) {
|
||||
if (!Security.isPasswordSane(str)) {
|
||||
this.sendError("Invalid password format.");
|
||||
this.errorCount++;
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.firstPassword) {
|
||||
this.firstPassword = str;
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.firstPassword !== str) {
|
||||
this.errorCount++;
|
||||
this.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.passwordAccepted(str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Config } from "../../config.js";
|
||||
import { gGame } from "../../models/globals.js";
|
||||
import { Security } from "../../utils/security.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));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
/** @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 */
|
||||
|
||||
@@ -16,7 +15,7 @@
|
||||
* - onColon(...)
|
||||
*/
|
||||
export class Prompt {
|
||||
/** @type {Scene} */
|
||||
/** @protected @type {Scene} */
|
||||
_scene;
|
||||
|
||||
/** @type {Scene} */
|
||||
@@ -30,20 +29,25 @@ export class Prompt {
|
||||
* Values: string containing the help text
|
||||
*
|
||||
* If you want truly custom help texts, you must override the onHelpFallback function,
|
||||
* but overriding the onHelp() function gives you more control and skips this helpText
|
||||
* but overriding the onHelp() function gives you more control and skips this help
|
||||
* dictionary entirely.
|
||||
*
|
||||
* @constant
|
||||
* @readonly
|
||||
* @type {Record<string, string|string[]>}
|
||||
*/
|
||||
helpText = {
|
||||
"": "Sorry, no help available. Figure it out yourself, adventurer", // default help text
|
||||
};
|
||||
help = {};
|
||||
|
||||
/** @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
|
||||
/**
|
||||
* Default prompt text to send if we don't want to send something in the execute() call.
|
||||
*
|
||||
* Array values will be converted to multiline strings with newlines between each string
|
||||
* in the array.
|
||||
*
|
||||
* @type {string|string[]}
|
||||
*/
|
||||
message = [
|
||||
"Please enter some very important info", // Silly placeholder text
|
||||
"((or type :quit to run away))", // strings in double parentheses is rendered shaded/faintly
|
||||
];
|
||||
|
||||
@@ -51,28 +55,21 @@ export class Prompt {
|
||||
/* @example
|
||||
*
|
||||
* // if the prompt expects a username
|
||||
* promptOptions = { username : true };
|
||||
* options = { username : true };
|
||||
*
|
||||
* // if the prompt expects a password
|
||||
* promptOptions = { password : true };
|
||||
* options = { password : true };
|
||||
*/
|
||||
promptOptions = {};
|
||||
options = {};
|
||||
|
||||
/** @type {Session} */
|
||||
get session() {
|
||||
return this.scene.session;
|
||||
return this._scene.session;
|
||||
}
|
||||
|
||||
/** @param {Scene} scene */
|
||||
constructor(scene) {
|
||||
this._scene = scene;
|
||||
|
||||
//
|
||||
// Fix data formatting shorthand
|
||||
// So lazy dev set property helpText = "fooo" instead of helpText = { "": "fooo" }.
|
||||
if (typeof this.helpText === "string" || Array.isArray(this.helpText)) {
|
||||
this.helpText = { "": this.helpText };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,13 +78,37 @@ export class Prompt {
|
||||
* It's here you want to send the prompt text via the sendPrompt() method
|
||||
*/
|
||||
execute() {
|
||||
this.sendPrompt(this.promptText, this.promptOptions);
|
||||
this.prepareProperties();
|
||||
this.beforeExecute();
|
||||
|
||||
this.sendPrompt(this.message, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize / massage the properties of the Prompt.
|
||||
*
|
||||
* This function cannot be called from the Prompt base constructor, as the
|
||||
* properties of the child class have not been set yet.
|
||||
*/
|
||||
prepareProperties() {
|
||||
//
|
||||
// Lazy dev set property help = "fooo" instead of help = { "": "fooo" }.
|
||||
if (this.help && (typeof this.help === "string" || Array.isArray(this.help))) {
|
||||
this.help = { "": this.help };
|
||||
}
|
||||
}
|
||||
|
||||
beforeExecute() {}
|
||||
|
||||
/** Triggered when user types `:help [some optional topic]` */
|
||||
onHelp(topic) {
|
||||
if (this.helpText[topic]) {
|
||||
this.sendText(this.helpText[topic]);
|
||||
if (!this.help) {
|
||||
this.sendText("No help available at this moment - figure it out yourself, adventurer");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.help[topic]) {
|
||||
this.sendText(this.help[topic]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { sprintf } from "sprintf-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 */
|
||||
/** @typedef {new (scene: Scene) => Prompt} PromptClassReference */
|
||||
|
||||
/**
|
||||
* Scene - a class for showing one or more prompts in a row.
|
||||
@@ -16,13 +18,8 @@ import { sprintf } from "sprintf-js";
|
||||
* @abstract
|
||||
*/
|
||||
export class Scene {
|
||||
/**
|
||||
* @type {string|string[]} This text is shown when the scene begins
|
||||
*/
|
||||
introText = "";
|
||||
|
||||
/** @constant @readonly @type {Prompt?} */
|
||||
introPrompt;
|
||||
/** @constant @readonly @type {string|string[]|PromptClassReference} Text or prompt to show when this scene begins */
|
||||
intro;
|
||||
|
||||
/** @readonly @constant @protected @type {Session} */
|
||||
#session;
|
||||
@@ -47,21 +44,15 @@ export class Scene {
|
||||
/** @param {Session} session */
|
||||
execute(session) {
|
||||
this.#session = session;
|
||||
|
||||
if (this.introText) {
|
||||
this.session.sendText(this.introText);
|
||||
}
|
||||
|
||||
if (this.introPrompt) {
|
||||
this.showPrompt(this.introPrompt);
|
||||
} else {
|
||||
this.onReady();
|
||||
}
|
||||
|
||||
onReady() {
|
||||
if (!this.intro) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @abstract */
|
||||
onReady() {
|
||||
throw new Error("Abstract method must be implemented by subclass");
|
||||
this.show(this.intro);
|
||||
}
|
||||
|
||||
/** @param {Prompt} prompt */
|
||||
@@ -70,9 +61,33 @@ export class Scene {
|
||||
prompt.execute();
|
||||
}
|
||||
|
||||
/** @param {new (scene: Scene) => Prompt} promptClassReference */
|
||||
show(promptClassReference) {
|
||||
this.showPrompt(new promptClassReference(this));
|
||||
/** @param {string|string[]} text */
|
||||
showText(text) {
|
||||
this.session.sendText(text);
|
||||
}
|
||||
|
||||
/** @param {PromptClassReference|string|string[]|Prompt} value */
|
||||
show(value) {
|
||||
if (value instanceof Prompt) {
|
||||
this.showPrompt(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value[0] === "string") {
|
||||
this.showText(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== "function") {
|
||||
throw new Error("Invalid type. Value must be string, string[], Prompt, or a class reference to Prompt");
|
||||
}
|
||||
|
||||
const prompt = new value(this);
|
||||
if (!(prompt instanceof Prompt)) {
|
||||
throw new Error("Invalid class reference");
|
||||
}
|
||||
|
||||
this.showPrompt(new value(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +106,11 @@ export class Scene {
|
||||
message,
|
||||
type: typeof message,
|
||||
});
|
||||
|
||||
if (!this.currentPrompt) {
|
||||
throw new Error("LogicError: cannot get a reply when you have not prompted the player");
|
||||
}
|
||||
|
||||
this.currentPrompt.onReply(message.text);
|
||||
}
|
||||
|
||||
@@ -107,7 +127,7 @@ export class Scene {
|
||||
* - call this method directly
|
||||
*/
|
||||
onQuit() {
|
||||
this.currentPrompt.onQuit();
|
||||
this.currentPrompt?.onQuit();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +147,7 @@ export class Scene {
|
||||
* @param {WebsocketMessage} message
|
||||
*/
|
||||
onHelp(message) {
|
||||
this.currentPrompt.onHelp(message.text);
|
||||
this.currentPrompt?.onHelp(message.text);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +205,7 @@ export class Scene {
|
||||
* @param {WebsocketMessage} message
|
||||
*/
|
||||
onColon(message) {
|
||||
const handledByPrompt = this.currentPrompt.onColon(message.command, message.args);
|
||||
const handledByPrompt = this.currentPrompt?.onColon(message.command, message.args);
|
||||
|
||||
if (!handledByPrompt) {
|
||||
this.onColonFallback(message.command, message.args);
|
||||
@@ -203,7 +223,7 @@ export class Scene {
|
||||
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),
|
||||
sprintf("%.2f centimeters is only %.2f inches. This is why american wands are so short!", n, n / 2.54),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,19 +20,11 @@ let roll = {};
|
||||
|
||||
export class CharacterSeeder {
|
||||
constructor() {
|
||||
// stupid convenience hack that only works if we only have a single Game in the system.
|
||||
// stupid hack that ensures we populate roll AFTER gGame is available
|
||||
// Which we easily could have.!!
|
||||
roll = {
|
||||
d: (max, min = 1) => {
|
||||
return gGame.random.within(min, max);
|
||||
},
|
||||
d6: () => {
|
||||
return gGame.random.within(1, 6);
|
||||
},
|
||||
d8: () => {
|
||||
return gGame.random.within(1, 8);
|
||||
},
|
||||
};
|
||||
roll.d = (max, min = 1) => gGame.random.within(min, max)
|
||||
roll.d6 = () => roll.d(6)
|
||||
roll.d8 = () => roll.d(8)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,14 +44,14 @@ export class CharacterSeeder {
|
||||
|
||||
/**
|
||||
* @param {Character} character
|
||||
* @param {...string} skills
|
||||
* @param {...string} proficiencies
|
||||
*/
|
||||
addSkillsToCharacter(character, ...skills) {
|
||||
for (const skill of skills) {
|
||||
if (!isIdSane(skill)) {
|
||||
throw new Error(`Skill id >>${skill}<< is insane!`);
|
||||
addProficienciesToCharacter(character, ...proficiencies) {
|
||||
for (const prof of proficiencies) {
|
||||
if (!isIdSane(prof)) {
|
||||
throw new Error(`Proficiency id >>${prof}<< is insane!`);
|
||||
}
|
||||
character.skills.add(skill);
|
||||
character.proficiencies.add(prof);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,17 +64,24 @@ export class CharacterSeeder {
|
||||
|
||||
createCharacter() {
|
||||
const c = new Character();
|
||||
//
|
||||
// Initializing
|
||||
//
|
||||
|
||||
// Rolling skills
|
||||
this.generateName(c);
|
||||
this.rollSkills(c);
|
||||
this.applyAncestry(c);
|
||||
this.applyFoundation(c);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
generateName(c) {
|
||||
/** @todo use actual random name generator */
|
||||
c.name =
|
||||
gGame.random.oneOf("sir ", "madam ", "mister ", "miss ", "", "", "") + // prefix
|
||||
gGame.random.oneOf("sir ", "madam ", "mister ", "miss ", "", "", "") +
|
||||
"random " + // name
|
||||
gGame.random.get().toString(); // suffix
|
||||
gGame.random.next().toString();
|
||||
}
|
||||
|
||||
rollSkills(c) {
|
||||
c.awareness = roll.d6() + 2;
|
||||
c.grit = roll.d6() + 2;
|
||||
c.knowledge = roll.d6() + 2;
|
||||
@@ -90,11 +89,6 @@ export class CharacterSeeder {
|
||||
c.meleeCombat = roll.d6() + 2;
|
||||
c.rangedCombat = roll.d6() + 2;
|
||||
c.skulduggery = roll.d6() + 2;
|
||||
|
||||
this.applyAncestry(c);
|
||||
this.applyFoundation(c);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
applyAncestry(c) {
|
||||
@@ -168,44 +162,61 @@ export class CharacterSeeder {
|
||||
* @param {string|number} Foundation to add to character
|
||||
*/
|
||||
applyFoundation(c, foundation = ":random") {
|
||||
switch (foundation) {
|
||||
case ":random":
|
||||
return this.applyFoundation(c, roll.d(3));
|
||||
if (foundation == ":random") {
|
||||
return this.applyFoundation(c, roll.d(20)); // according to the rulebook, roll a d20 and reroll any invalid results.
|
||||
}
|
||||
|
||||
//
|
||||
// Brawler
|
||||
// ------
|
||||
case 1:
|
||||
case ":brawler":
|
||||
if (foundation === 1 || foundation === ":brawler") {
|
||||
c.foundation = "Brawler";
|
||||
c.skills.add(":armor.light");
|
||||
c.silver = 40;
|
||||
|
||||
c.maxHitPoints = c.currentHitPoints = 15;
|
||||
c.itemSlots = 7;
|
||||
c.silver = 40;
|
||||
|
||||
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
||||
c.knowledge = Math.min(c.knowledge, 10);
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c,
|
||||
":armor.light",
|
||||
":weapon.weird.spiked_gauntlets"
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.light.studded_leather",
|
||||
":weapon.weird.spiked_gauntlets",
|
||||
);
|
||||
|
||||
this.addSkillsToCharacter(c, ":weapon.weird.spiked_gauntlets");
|
||||
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// DRUID
|
||||
// ------
|
||||
case 2:
|
||||
case ":druid":
|
||||
if (foundation === 2 || foundation === ":druid") {
|
||||
|
||||
c.foundation = "Druid";
|
||||
c.silver = 40;
|
||||
c.maxHitPoints = this.currentHitPoints = 15;
|
||||
c.itemSlots = 7;
|
||||
c.meleeCombat = Math.max(this.meleeCombat, 10);
|
||||
c.knowledge = Math.min(this.knowledge, 10);
|
||||
|
||||
c.silver = 10;
|
||||
c.itemSlots = 5;
|
||||
c.maxHitPoints = this.currentHitPoints = 10;
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":armor.light.cloth",
|
||||
":armor.light.hide",
|
||||
":armor.light.leather",
|
||||
":kit.healers_kit",
|
||||
":kit.poisoners_kit",
|
||||
":weapon.light.sickle",
|
||||
":weapon.light.quarterstaff",
|
||||
":weapon.light.sling",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.light.leather",
|
||||
@@ -213,385 +224,350 @@ export class CharacterSeeder {
|
||||
":kit.poisoners_kit",
|
||||
":kit.healers_kit",
|
||||
);
|
||||
this.addSkillsToCharacter(
|
||||
c, //
|
||||
":armor.light.sleather",
|
||||
":armor.light.hide",
|
||||
":weapon.light.sickle",
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
case ":fencer":
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// FENCER
|
||||
// -------
|
||||
if (foundation === 3 || foundation === ":fencer") {
|
||||
|
||||
c.foundation = "Fencer";
|
||||
|
||||
//
|
||||
// Stats
|
||||
c.maxHitPoints = c.currentHitPoints = 15;
|
||||
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
||||
c.magic = Math.min(c.magic, 10);
|
||||
|
||||
//
|
||||
// Skills
|
||||
this.addSkillsToCharacter(
|
||||
c, //
|
||||
":weapon.style.two_weapons",
|
||||
":armor.light",
|
||||
);
|
||||
|
||||
//
|
||||
// Gear
|
||||
c.silver = 40;
|
||||
c.itemSlots = 5;
|
||||
c.maxHitPoints = c.currentHitPoints = 15;
|
||||
|
||||
c.magic = Math.min(c.magic, 10)
|
||||
c.meleeCombat = Math.max(c.meleeCombat, 10)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":perk.riposte",
|
||||
":armor.light",
|
||||
":weapon.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.light.leather",
|
||||
":weapon.basic.dagger",
|
||||
":weapon.light.rapier",
|
||||
":weapon.light.dagger",
|
||||
);
|
||||
break;
|
||||
case 4:
|
||||
case ":guard":
|
||||
}
|
||||
|
||||
if (foundation === 4 || foundation === ":guard") {
|
||||
|
||||
c.foundation = "Guard";
|
||||
|
||||
//
|
||||
// Stats
|
||||
c.maxHitPoints = c.currentHitPoints = 15;
|
||||
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
||||
c.magic = Math.min(c.magic, 10);
|
||||
|
||||
//
|
||||
// Skills
|
||||
this.addSkillsToCharacter(
|
||||
c, //
|
||||
":armor.medium",
|
||||
":weapon.weird.halberd",
|
||||
);
|
||||
|
||||
//
|
||||
// Gear
|
||||
c.silver = 50;
|
||||
c.itemSlots = 5;
|
||||
c.maxHitPoints = c.currentHitPoints = 10
|
||||
|
||||
c.awareness = Math.max(c.awareness, 10)
|
||||
c.meleeCombat = Math.max(c.meleeCombat, 10)
|
||||
c.skulduggery = Math.min(c.skulduggery, 10)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":armor.medium",
|
||||
":weapon.heavy",
|
||||
":weapon.specialist.halberd",
|
||||
":wepaon.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.medium.breastplate",
|
||||
":weapon.weird.halberd",
|
||||
":lighting.bulls_eye_lantern",
|
||||
":map.city.hovedstad",
|
||||
":misc.lamp_oil",
|
||||
":misc.signal_whistle",
|
||||
":maps.area.hvedstad",
|
||||
":weapon.specialist.halberd",
|
||||
);
|
||||
break;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: GUARD
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Guard
|
||||
|
||||
|[unstyled]
|
||||
* Medium Armor
|
||||
|
||||
|[unstyled]
|
||||
* Halberd
|
||||
* Bull's Eye Lantern
|
||||
* Signal Whistle
|
||||
* Map of Local Area
|
||||
* 50 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 5 Item Slots
|
||||
* Awareness raised to 10
|
||||
* Melee Combat raised to 10
|
||||
* Skulduggery limited to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: MAGICIAN
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Magician
|
||||
|
||||
|[unstyled]
|
||||
* None
|
||||
|
||||
|[unstyled]
|
||||
* Tier 2 Wand with random spell.
|
||||
* Tier 1 Wand with random spell.
|
||||
* 10 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 6 Item Slots
|
||||
* Melee Combat limited to 10
|
||||
* Ranged Combat limited to 5
|
||||
* Magic raised to 10
|
||||
* Grit limited to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: MEDIC
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
|Medic
|
||||
|
||||
|[unstyled]
|
||||
* Light Armor
|
||||
* Medium Armor
|
||||
|
||||
|[unstyled]
|
||||
* Club
|
||||
* Sling
|
||||
* 3 Daggers
|
||||
* Healer's Kit
|
||||
* 40 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 6 Item Slots
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: RECKLESS
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Reckless
|
||||
|
||||
|[unstyled]
|
||||
|
||||
|[unstyled]
|
||||
* Great Axe
|
||||
* 50 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 20 Hit Points
|
||||
* 7 Item Slots
|
||||
* Melee Combat raised to 10
|
||||
* Awareness raised to 10
|
||||
* Grit raised to 10
|
||||
* Magic limited to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: ROVER
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Rover
|
||||
|
||||
|[unstyled]
|
||||
* Light Armor
|
||||
|
||||
|[unstyled]
|
||||
* Leather Armor
|
||||
* Short Sword
|
||||
* Longbow
|
||||
* Snare Maker's Kit
|
||||
* 25 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 5 Item Slots
|
||||
* Magic Reduced to 10
|
||||
* Awareness raised to 10
|
||||
* Ranged Combat raised to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: SKIRMISHER
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Skirmisher
|
||||
|
||||
|[unstyled]
|
||||
* Light Armor
|
||||
* Shields
|
||||
|
||||
|[unstyled]
|
||||
* Spear
|
||||
* Small Shield
|
||||
* 50 Silver Pieces
|
||||
|
||||
|
||||
|[unstyled]
|
||||
* 15 Hit Points
|
||||
* 6 Item Slots
|
||||
* Melee Combat raised to 10
|
||||
* Awareness raised to 10
|
||||
* Skulduggery raised to 10
|
||||
* Grit raised to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: SNEAK
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Sneak
|
||||
|
||||
|[unstyled]
|
||||
* Light Armor
|
||||
|
||||
|[unstyled]
|
||||
* 3 daggers
|
||||
* Small Crossbow
|
||||
* Poisoner's Kit
|
||||
* 30 Silver Pieces
|
||||
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 6 Item Slots
|
||||
* Melee Combat raised to 10
|
||||
* Awareness raised to 10
|
||||
* Skulduggery raised to 10
|
||||
* Grit raised to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: SPELLSWORD
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Spellsword
|
||||
|
||||
|[unstyled]
|
||||
|
||||
|[unstyled]
|
||||
* Tier 1 Wand with random spell.
|
||||
* Longsword
|
||||
* 30 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 12 Hit Points
|
||||
* 5 Item Slots
|
||||
* Melee Combat raised to 10
|
||||
* Ranged Combat limited to 10
|
||||
* Magic raised to 10
|
||||
* Skulduggery limited to 10
|
||||
* Grit raised to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: SPELUNKER
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Spelunker
|
||||
|
||||
|[unstyled]
|
||||
* None
|
||||
|
||||
|[unstyled]
|
||||
* Spear
|
||||
* Caltrops
|
||||
* Bull's Eye Lantern
|
||||
* Map Maker's Kit
|
||||
* Chalk
|
||||
* Caltrops
|
||||
* 5 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 4 Item Slots
|
||||
* Awareness raised to 10
|
||||
* Melee Combat raised to 10
|
||||
* Skulduggery raised to 10
|
||||
* Magic limited to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: SPIT'N'POLISH
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Spit'n' Polish
|
||||
|
||||
|[unstyled]
|
||||
* Heavy Armor
|
||||
* Shield
|
||||
|
||||
|[unstyled]
|
||||
* Half-Plate
|
||||
* Large Shield
|
||||
* Long Sword
|
||||
* 10 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 2 Item Slots
|
||||
* Melee Combat raised to 10
|
||||
* Magic Reduced to 6
|
||||
* Awareness Reduced to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: STILETTO
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
| Stiletto
|
||||
|
||||
|[unstyled]
|
||||
* Light Armor
|
||||
|
||||
|[unstyled]
|
||||
* Padded Armor
|
||||
* 3 Daggers
|
||||
* Small Crossbow
|
||||
* Poisoner's Kit
|
||||
* 20 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 5 Item Slots
|
||||
* Melee Combat raised to 10
|
||||
* Ranged Combat raised to 10
|
||||
* Awareness raised to 10
|
||||
* Magic limited to 6
|
||||
* Knowledge limited to 10
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------------
|
||||
//HEADLINE: Tinkerer
|
||||
//---------------------------------------------------------------------------------------
|
||||
| {counter:foundation}
|
||||
|
||||
|Tinkerer
|
||||
|
||||
|[unstyled]
|
||||
* Light Armor
|
||||
|
||||
|[unstyled]
|
||||
* Studded Leather
|
||||
* Wrench (club)
|
||||
* Tinkerer's Kit
|
||||
* 30 Silver Pieces
|
||||
|
||||
|[unstyled]
|
||||
* 10 Hit Points
|
||||
* 5 Item Slots
|
||||
* Awareness raised to 10
|
||||
* Knowledge raised to 10
|
||||
|
||||
*/
|
||||
//
|
||||
// WTF ?!
|
||||
// ------
|
||||
default:
|
||||
throw new Error(`Invalid foundation id ${foundation}`);
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Math.PI < 0 && Player) {
|
||||
("STFU Linda");
|
||||
if (foundation === 5 || foundation === ":magician") {
|
||||
|
||||
c.foundation = "Magician"
|
||||
|
||||
c.silver = 10;
|
||||
c.itemSlots = 6
|
||||
c.maxHitPoints = c.currentHitPoints = 10
|
||||
|
||||
c.grit = Math.min(c.grit, 10)
|
||||
c.magic = Math.max(c.magic, 10)
|
||||
c.meleeCombat = Math.min(c.meleeCombat, 10)
|
||||
c.rangedCombat = Math.min(c.rangedCombat, 10)
|
||||
|
||||
/* ---- NO PROFICIENCIES ---- */
|
||||
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
// "TODO: [TEIR 2 WAND WITH RANDOM SPELL]",
|
||||
// "TODO: [TEIR 1 WAND WITH RANDOM SPELL]",
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 6 || foundation === ":medic") {
|
||||
|
||||
c.foundation = "Medic";
|
||||
|
||||
c.silver = 40;
|
||||
c.itemSlots = 7;
|
||||
c.maxHitPoints = c.currentHitPoints = 10
|
||||
|
||||
c.awareness = Math.max(10, c.awareness)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":armor.light",
|
||||
":armor.medium",
|
||||
":weapon.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.light.studded_leather",
|
||||
":kit.healers_kit",
|
||||
":weapon.basic.club",
|
||||
":weapon.basic.dagger",
|
||||
":weapon.light.sling",
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 7 || foundation === ":reckless") {
|
||||
|
||||
c.foundation = "Reckless";
|
||||
|
||||
c.silver = 50;
|
||||
c.itemSlots = 7;
|
||||
c.maxHitPoints = c.currentHitPoints = 20
|
||||
|
||||
c.awareness = Math.max(10, c.awareness);
|
||||
c.grit = Math.max(10, c.grit);
|
||||
c.magic = Math.min(10, c.magic);
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat);
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":wepaon.heavy",
|
||||
":wepaon.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":weapon.heavy.great_axe",
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 8 || foundation === ":rover") {
|
||||
|
||||
c.foundation = "Rover";
|
||||
|
||||
c.silver = 25;
|
||||
c.itemSlots = 5;
|
||||
c.maxHitPoints = c.currentHitPoints = 10
|
||||
|
||||
c.awareness = Math.max(10, c.awareness)
|
||||
c.magic = Math.min(10, c.magic)
|
||||
c.rangedCombat = Math.max(10, c.rangedCombat)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":armor.light",
|
||||
":weapon.light",
|
||||
":weapon.heavy.longbow",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.light.leather",
|
||||
":kit.snare_makers_kit",
|
||||
":weapon.heavy.longbow",
|
||||
":weapon.light.short_sword",
|
||||
":map.shayland", // Shayland is the region around Hovedstad
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 9 || foundation === ":skrimisher") {
|
||||
|
||||
c.foundation = "Skirmisher";
|
||||
|
||||
c.silver = 15;
|
||||
c.itemSlots = 6;
|
||||
c.maxHitPoints = c.currentHitPoints = 15
|
||||
|
||||
c.awareness = Math.max(10, c.awareness)
|
||||
c.grit = Math.max(10, c.grit)
|
||||
c.magic = Math.max(5, c.magic)
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat)
|
||||
c.skulduggery = Math.max(10, c.skulduggery)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":armor.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":armor.light.small_shield",
|
||||
":weapon.light.spear",
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 10 || foundation === ":sneak") {
|
||||
|
||||
c.foundation = "Sneak";
|
||||
|
||||
c.silver = 30;
|
||||
c.itemSlots = 6;
|
||||
c.maxHitPoints = c.currentHitPoints = 10
|
||||
|
||||
c.awareness = Math.max(10, c.awareness)
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat)
|
||||
c.skulduggery = Math.max(10, c.skulduggery)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":armor.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":weapon.basic.dagger",
|
||||
":weapon.light.small_crossbow",
|
||||
":kit.poisoners_kit",
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 11 || foundation === ":spellsword") {
|
||||
|
||||
c.foundation = "Spellsword";
|
||||
|
||||
c.silver = 30;
|
||||
c.itemSlots = 5;
|
||||
c.maxHitPoints = c.currentHitPoints = 12;
|
||||
|
||||
c.grit = Math.max(10, c.grit)
|
||||
c.magic = Math.max(10, c.magic)
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat)
|
||||
c.rangedCombat = Math.min(10, c.rangedCombat)
|
||||
c.skulduggery = Math.min(10, c.skulduggery)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":weapon.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":weapon.light.rapier",
|
||||
// "[TODO TIER 1 WAND WITH RANDOM SPELL]",
|
||||
);
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 12 || foundation === ":spelunker") {
|
||||
|
||||
c.foundation = "Spelunker";
|
||||
|
||||
c.silver = 5;
|
||||
c.itemSlots = 4;
|
||||
c.maxHitPoints = c.currentHitPoints = 10;
|
||||
|
||||
c.awareness = Math.max(10, c.awareness)
|
||||
c.magic = Math.min(10, c.magic)
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat)
|
||||
c.skulduggery = Math.max(10, c.skulduggery)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":weapon.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":kit.map_makers_kit",
|
||||
":lighting.bulls_eye_lantern",
|
||||
":misc.caltrops",
|
||||
":misc.chalk",
|
||||
":misc.lamp_oil",
|
||||
":weapon.light.spear",
|
||||
);
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 13 || foundation === ":spit_n_polish") {
|
||||
|
||||
c.foundation = "Spit'n'Polish";
|
||||
|
||||
c.silver = 10;
|
||||
c.itemSlots = 2;
|
||||
c.maxHitPoints = c.currentHitPoints = 10;
|
||||
|
||||
c.magic = Math.min(6, c.magic)
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":weapon.light",
|
||||
":weapon.heavy",
|
||||
":armor.heavy",
|
||||
":armor.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":weapon.heavy.longsword",
|
||||
":armor.heavy.half_plate",
|
||||
":armor.heavy.large_shield",
|
||||
);
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (foundation === 14 || foundation === ":stiletto") {
|
||||
|
||||
c.foundation = "Stiletto";
|
||||
|
||||
c.silver = 10;
|
||||
c.itemSlots = 5;
|
||||
c.maxHitPoints = c.currentHitPoints = 10;
|
||||
|
||||
|
||||
c.magic = Math.min(6, c.magic)
|
||||
c.meleeCombat = Math.max(10, c.meleeCombat)
|
||||
|
||||
this.addProficienciesToCharacter(
|
||||
c, //
|
||||
":weapon.light",
|
||||
":weapon.heavy",
|
||||
":armor.heavy",
|
||||
":armor.light",
|
||||
);
|
||||
|
||||
this.addItemsToCharacter(
|
||||
c, //
|
||||
":weapon.heavy.longsword",
|
||||
":armor.heavy.half_plate",
|
||||
":armor.heavy.large_shield",
|
||||
);
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return this.applyFoundation(c, ":random")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,23 @@ export class ItemSeeder {
|
||||
// \_/\_/ \___|\__,_| .__/ \___/|_| |_|___/
|
||||
// |_|
|
||||
//-------------------------------------------------------
|
||||
gGame.addItemBlueprint(":weapon.light.dagger", {
|
||||
gGame.addItemBlueprint(":weapon.basic.club", {
|
||||
name: "Club",
|
||||
description: "A club, it's light, what's more to say?",
|
||||
itemSlots: 1,
|
||||
damage: 4,
|
||||
specialEffect: "TBD",
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":weapon.basic.dagger", {
|
||||
name: "Dagger",
|
||||
description: "Small shady blady",
|
||||
itemSlots: 0.5,
|
||||
description: "Basic small shady blady",
|
||||
itemSlots: 1,
|
||||
damage: 3,
|
||||
melee: true,
|
||||
ranged: true,
|
||||
ranged: true, //
|
||||
count: 3, // basic daggers always come in a bundle of three
|
||||
maxCount: 3,
|
||||
specialEffect: ":effect.weapon.fast",
|
||||
});
|
||||
|
||||
@@ -52,6 +62,31 @@ export class ItemSeeder {
|
||||
specialEffect: "TBD",
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":weapon.light.small_crossbow", {
|
||||
name: "Rapier",
|
||||
description: "Small Crossbow",
|
||||
itemSlots: 2,
|
||||
damage: 8,
|
||||
specialEffect: "TBD",
|
||||
ammoType: "bolt",
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":weapon.heavy.longsword", {
|
||||
name: "Rapier",
|
||||
description: "Long one-handed sword",
|
||||
itemSlots: 2,
|
||||
damage: 8,
|
||||
specialEffect: "TBD",
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":weapon.heavy.longbow", {
|
||||
name: "Rapier",
|
||||
description: "Longbow",
|
||||
itemSlots: 3,
|
||||
damage: 8,
|
||||
specialEffect: "TBD",
|
||||
});
|
||||
|
||||
// _
|
||||
// / \ _ __ _ __ ___ ___ _ __ ___
|
||||
// / _ \ | '__| '_ ` _ \ / _ \| '__/ __|
|
||||
@@ -63,8 +98,10 @@ export class ItemSeeder {
|
||||
description: "Padded and hardened leather with metal stud reinforcement",
|
||||
itemSlots: 3,
|
||||
specialEffect: "TBD",
|
||||
sneak: false,
|
||||
armorHitPoints: 10,
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":armor.light.leather", {
|
||||
name: "Leather Armor",
|
||||
description: "Padded and hardened leather",
|
||||
@@ -72,6 +109,29 @@ export class ItemSeeder {
|
||||
specialEffect: "TBD",
|
||||
armorHitPoints: 6,
|
||||
});
|
||||
gGame.addItemBlueprint(":armor.medium.breastplate", {
|
||||
name: "Breastplate",
|
||||
description: "Plate that covers chest, cloth and leather covers the rest",
|
||||
itemSlots: 3,
|
||||
specialEffect: "TBD",
|
||||
armorHitPoints: 10,
|
||||
})
|
||||
|
||||
gGame.addItemBlueprint(":armor.heavy.half_plate", {
|
||||
name: "Half-Plate",
|
||||
description: "Platemail with near-total coverage",
|
||||
itemSlots: 4,
|
||||
specialEffect: "TBD",
|
||||
armorHitPoints: 6,
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":armor.heavy.large_shield", {
|
||||
name: "Large Shield",
|
||||
description: "Platemail with near-total coverage",
|
||||
itemSlots: 4,
|
||||
specialEffect: "TBD",
|
||||
armorHitPoints: 6,
|
||||
});
|
||||
|
||||
// _ ___ _
|
||||
// | |/ (_) |_ ___
|
||||
@@ -96,5 +156,21 @@ export class ItemSeeder {
|
||||
count: 20,
|
||||
maxCount: 20,
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":kit.snare_makers_kit", {
|
||||
name: "Healer's Kit",
|
||||
description: "Allows you to create traps and snares",
|
||||
itemSlots: 2,
|
||||
specialEffect: "TBD",
|
||||
count: 20,
|
||||
maxCount: 20,
|
||||
});
|
||||
|
||||
gGame.addItemBlueprint(":kit.map_makers_kit", {
|
||||
name: "Healer's Kit",
|
||||
description: "Allows you to create traps and snares",
|
||||
itemSlots: 1,
|
||||
specialEffect: "TBD",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { gGame } from "./models/globals.js";
|
||||
import { AuthenticationScene } from "./scenes/authentication/authenticationScene.js";
|
||||
import { MessageType, WebsocketMessage, formatMessage } from "./utils/messages.js";
|
||||
|
||||
|
||||
// __ __ _ _ ____ ____
|
||||
// | \/ | | | | _ \ / ___| ___ _ ____ _____ _ __
|
||||
// | |\/| | | | | | | | \___ \ / _ \ '__\ \ / / _ \ '__|
|
||||
|
||||
2
test.js
Normal file → Executable file
2
test.js
Normal file → Executable file
@@ -22,4 +22,4 @@ class TestChild extends TestParent {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(new TestChild());
|
||||
console.log(Function.prototype.toString.call(TestChild));
|
||||
|
||||
@@ -22,15 +22,17 @@ const htmlEscapeRegex = /[&<>"'`]/g; // used to escape html characters
|
||||
* The order of the elements of this array matters.
|
||||
*/
|
||||
const opcodes = [
|
||||
["(^|\\n)=", "($|\\n)", "$1<h1>$2</h1>$3"],
|
||||
["(^|\\n)==", "($|\\n)", "$1<h2>$2</h2>$3"],
|
||||
["---", "---", "<span class='strike'>$1</span>"],
|
||||
["___", "___", "<span class='underline'>$1</span>"],
|
||||
["(?:[,]{3})", "(?:[,]{3})", "<span class='undercurl'>$1</span>"],
|
||||
["(?:[(]{2})", "(?:[)]{2})", "<span class='faint'>$1</span>"],
|
||||
["_", "_", "<span class='italic'>$1</span>"],
|
||||
["\\*", "\\*", "<span class='bold'>$1</span>"],
|
||||
["\\[\\[([a-zA-Z0-9_ ]+)\\[\\[", "\\]\\]", "<span class='$1'>$2</span>"],
|
||||
["(^|\\n)=", "($|\\n)", "$1<h1>$2</h1>$3"], // lines with large headline begins with =
|
||||
["(^|\\n)==", "($|\\n)", "$1<h2>$2</h2>$3"], // lines with sub-headline begins with ==
|
||||
["---", "---", "<span class='strike'>$1</span>"], // ---trike through---
|
||||
["___", "___", "<span class='underline'>$1</span>"], // ___underline___
|
||||
["(?:[,]{3})", "(?:[,]{3})", "<span class='undercurl'>$1</span>"], // ,,,undercurl,,,
|
||||
["(?:[(]{2})", "(?:[)]{2})", "<span class='faint'>$1</span>"], // ((faint text))
|
||||
["(?:_\\*)", "(?:\\*_)", "<span class='bold italic'>$1</span>"], // _*bold and italic*_
|
||||
["(?:\\*_)", "(?:_\\*)", "<span class='bold italic'>$1</span>"], // *_bold and italic_*
|
||||
["_", "_", "<span class='italic'>$1</span>"], // _italic_
|
||||
["\\*", "\\*", "<span class='bold'>$1</span>"], // *bold*
|
||||
["\\[\\[([a-zA-Z0-9_ ]+)\\[\\[", "\\]\\]", "<span class='$1'>$2</span>"], // [[custom_class[[text with custom class]]
|
||||
];
|
||||
/** @type{Array.Array.<Regexp,string>} */
|
||||
const regexes = [];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as regex from "./regex.js";
|
||||
|
||||
const MINI_UID_REGEX = regex.pretty(
|
||||
const MINI_UID_REGEX = regex.compileMultilineRegex(
|
||||
"\.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(
|
||||
const ID_SANITY_REGEX = regex.compileMultilineRegex(
|
||||
"^:", // 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.
|
||||
@@ -33,7 +33,7 @@ export function isIdSane(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} crypto-unsafe pseudo random numbe"r.
|
||||
* @returns {string} crypto-unsafe pseudo random number.
|
||||
*
|
||||
* Generate a random number, convert it to base36, and return it as a string with 7-8 characters.
|
||||
*/
|
||||
|
||||
@@ -83,12 +83,15 @@ export const MessageType = Object.freeze({
|
||||
* @property {any[]} args
|
||||
*/
|
||||
export class WebsocketMessage {
|
||||
/** @protected @type {any[]} _arr The array that contains the message data */
|
||||
/** @protected @type {any[]} The array that contains the message data */
|
||||
_data;
|
||||
|
||||
/** @constant @readonly @type {string} _arr The array that contains the message data */
|
||||
/** @constant @readonly @type {string} The array that contains the message data */
|
||||
type;
|
||||
|
||||
/** @constant @readonly @type {string?} the text payload (if any) of the decoded message */
|
||||
text;
|
||||
|
||||
/**
|
||||
* @param {string} msgData the raw text data in the websocket message.
|
||||
*/
|
||||
@@ -98,13 +101,12 @@ export class WebsocketMessage {
|
||||
"Could not create client message. Attempting to parse json, but data was not even a string, it was a " +
|
||||
typeof msgData,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(msgData);
|
||||
} catch (_) {
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
|
||||
);
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
* using the xorshift32 method.
|
||||
*/
|
||||
export class Xorshift32 {
|
||||
/* @type {number} */
|
||||
initialSeed;
|
||||
|
||||
/**
|
||||
* State holds a single uint32.
|
||||
* It's useful for staying within modulo 2**32.
|
||||
*
|
||||
* @type {Uint32Array}
|
||||
*/
|
||||
state;
|
||||
#state;
|
||||
get state() {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
/** @param {number} seed */
|
||||
constructor(seed) {
|
||||
@@ -21,41 +21,44 @@ export class Xorshift32 {
|
||||
seed = Math.floor(Math.random() * (maxInt32 - 1)) + 1;
|
||||
}
|
||||
seed = seed | 0;
|
||||
this.state = Uint32Array.of(seed);
|
||||
this.#state = Uint32Array.of(seed);
|
||||
}
|
||||
|
||||
/** @protected Shuffle the internal state. */
|
||||
shuffle() {
|
||||
this.state[0] ^= this.state[0] << 13;
|
||||
this.state[0] ^= this.state[0] >>> 17;
|
||||
this.state[0] ^= this.state[0] << 5;
|
||||
this.#state[0] ^= this.#state[0] << 13;
|
||||
this.#state[0] ^= this.#state[0] >>> 17;
|
||||
this.#state[0] ^= this.#state[0] << 5;
|
||||
|
||||
// We could also do something like this:
|
||||
// x ^= x << 13;
|
||||
// x ^= x >> 17;
|
||||
// x ^= x >>> 17;
|
||||
// x ^= x << 5;
|
||||
// return x;
|
||||
// But we'd have to xor the x with 2^32 after every op,
|
||||
// we get that "for free" by using the uint32array
|
||||
// And even though bitwise operations coerce numbers
|
||||
// into int32 (except >>> which converts into uint32).
|
||||
// But using Uint32Array ensures the number stays
|
||||
// uint32 all the way through, thus avoiding the pitfalls
|
||||
// of potentially dipping into negative number territory
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random number and shuffle the internal state.
|
||||
* @returns {number} a pseudo-random positive integer.
|
||||
*/
|
||||
get() {
|
||||
next() {
|
||||
this.shuffle();
|
||||
return this.state[0];
|
||||
return this.#state[0];
|
||||
}
|
||||
|
||||
/** @param {number} x @returns {number} a positive integer lower than x */
|
||||
lowerThan(x) {
|
||||
return this.get() % x;
|
||||
return this.next() % x;
|
||||
}
|
||||
|
||||
/** @param {number} x @returns {number} a positive integer lower than or equal to x */
|
||||
lowerThanOrEqual(x) {
|
||||
return this.get() % (x + 1);
|
||||
return this.next() % (x + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,13 +76,13 @@ export class Xorshift32 {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...<T>} ... pick random function argument
|
||||
* @returns {<T>}
|
||||
* @method
|
||||
* @template T
|
||||
* @param {...T} args pick random function argument
|
||||
* @returns {T}
|
||||
*/
|
||||
oneOf(...args) {
|
||||
const idx = this.lowerThan(args.length);
|
||||
|
||||
return args[idx];
|
||||
return this.randomElement(args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @param {...string} args
|
||||
* @returns {Regexp}
|
||||
*/
|
||||
export function pretty(...args) {
|
||||
export function compileMultilineRegex(...args) {
|
||||
const regexprStr = args.join("");
|
||||
return new RegExp(regexprStr);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class FramingOptions {
|
||||
frameChars = FrameType.values.Double;
|
||||
|
||||
/**
|
||||
* @param {object} o
|
||||
* @param {FramingOptions} o
|
||||
* @returns {FramingOptions}
|
||||
*/
|
||||
static fromObject(o) {
|
||||
|
||||
Reference in New Issue
Block a user