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
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -398,14 +398,14 @@ class MUDClient {
|
|||||||
// prompted.
|
// prompted.
|
||||||
// In fact, we should ALWAYS be in a state of just-having-been-prompted.
|
// In fact, we should ALWAYS be in a state of just-having-been-prompted.
|
||||||
handlePromptMessage(data) {
|
handlePromptMessage(data) {
|
||||||
let [promptText, options = {}] = data;
|
let [prompt, options = {}] = data;
|
||||||
|
|
||||||
this.shouldReply = true;
|
this.shouldReply = true;
|
||||||
|
|
||||||
this.promptOptions = { ...{ class: "prompt" }, ...options };
|
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
|
// 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. */
|
/** Dungeon Generator - generates TileMaps populated with rooms, traps, encounters, etc. */
|
||||||
class DungeonFactory {
|
class DungeonFactory {
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
roomCount;
|
#roomCount;
|
||||||
|
|
||||||
/** @type {RoomConfig[]} */
|
/** @type {RoomConfig[]} */
|
||||||
rooms;
|
#rooms;
|
||||||
|
|
||||||
/** @type {TileMap} */
|
/** @type {TileMap} */
|
||||||
map;
|
#map;
|
||||||
|
|
||||||
|
get roomCount() {
|
||||||
|
return this.#roomCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rooms() {
|
||||||
|
return this.#rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
get map() {
|
||||||
|
return this.#map;
|
||||||
|
}
|
||||||
|
|
||||||
get width() {
|
get width() {
|
||||||
return this.map.width;
|
return this.#map.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
get height() {
|
get height() {
|
||||||
return this.map.height;
|
return this.#map.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} width
|
* @param {number} width
|
||||||
* @param {number} height
|
* @param {number} height
|
||||||
* @param {number} roomCount
|
* @param {number} #roomCount
|
||||||
*/
|
*/
|
||||||
constructor(width, height, roomCount) {
|
constructor(width, height, roomCount) {
|
||||||
this.roomCount = roomCount | 0;
|
this.#roomCount = roomCount | 0;
|
||||||
this.rooms = [];
|
this.#rooms = [];
|
||||||
|
|
||||||
// 2d array of pure wall tiles
|
// 2d array of pure wall tiles
|
||||||
const tiles = new Array(height | 0).fill().map(() => Array(width | 0).fill(Tile.createWall()));
|
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() {
|
generate() {
|
||||||
@@ -52,18 +64,18 @@ class DungeonFactory {
|
|||||||
this.addFeatures();
|
this.addFeatures();
|
||||||
this.addPlayerStart();
|
this.addPlayerStart();
|
||||||
this.addPortals();
|
this.addPortals();
|
||||||
return this.map.toString(CharType.TYPE_ID);
|
return this.#map.toString(CharType.TYPE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateRooms() {
|
generateRooms() {
|
||||||
this.rooms = [];
|
this.#rooms = [];
|
||||||
const maxAttempts = this.roomCount * 10;
|
const maxAttempts = this.#roomCount * 10;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
|
|
||||||
while (this.rooms.length < this.roomCount && attempts < maxAttempts) {
|
while (this.#rooms.length < this.#roomCount && attempts < maxAttempts) {
|
||||||
const room = this.generateRoom();
|
const room = this.generateRoom();
|
||||||
if (room && !this.roomOverlaps(room)) {
|
if (room && !this.roomOverlaps(room)) {
|
||||||
this.rooms.push(room);
|
this.#rooms.push(room);
|
||||||
this.carveRoom(room);
|
this.carveRoom(room);
|
||||||
}
|
}
|
||||||
attempts++;
|
attempts++;
|
||||||
@@ -83,7 +95,7 @@ class DungeonFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
roomOverlaps(newRoom) {
|
roomOverlaps(newRoom) {
|
||||||
return this.rooms.some(
|
return this.#rooms.some(
|
||||||
(room) =>
|
(room) =>
|
||||||
newRoom.x < room.x + room.width + 2 &&
|
newRoom.x < room.x + room.width + 2 &&
|
||||||
newRoom.x + newRoom.width + 2 > room.x &&
|
newRoom.x + newRoom.width + 2 > room.x &&
|
||||||
@@ -95,26 +107,26 @@ class DungeonFactory {
|
|||||||
carveRoom(room) {
|
carveRoom(room) {
|
||||||
for (let y = room.y; y < room.y + room.height; y++) {
|
for (let y = room.y; y < room.y + room.height; y++) {
|
||||||
for (let x = room.x; x < room.x + room.width; x++) {
|
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() {
|
connectRooms() {
|
||||||
if (this.rooms.length < 2) return;
|
if (this.#rooms.length < 2) return;
|
||||||
|
|
||||||
// Connect each room to at least one other room
|
// Connect each room to at least one other room
|
||||||
for (let i = 1; i < this.rooms.length >> 1; i++) {
|
for (let i = 1; i < this.#rooms.length >> 1; i++) {
|
||||||
const roomA = this.rooms[i - 1];
|
const roomA = this.#rooms[i - 1];
|
||||||
const roomB = this.rooms[i];
|
const roomB = this.#rooms[i];
|
||||||
this.createCorridor(roomA, roomB);
|
this.createCorridor(roomA, roomB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add some extra connections for more interesting layouts
|
// 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++) {
|
for (let i = 0; i < extraConnections; i++) {
|
||||||
const roomA = 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)];
|
const roomB = this.#rooms[this.random(0, this.#rooms.length - 1)];
|
||||||
if (roomA !== roomB) {
|
if (roomA !== roomB) {
|
||||||
this.createCorridor(roomA, roomB);
|
this.createCorridor(roomA, roomB);
|
||||||
}
|
}
|
||||||
@@ -138,7 +150,7 @@ class DungeonFactory {
|
|||||||
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
for (let x = 0; x < this.width; x++) {
|
||||||
//
|
//
|
||||||
if (this.map.get(x, y).isWall()) {
|
if (this.#map.get(x, y).isWall()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +192,7 @@ class DungeonFactory {
|
|||||||
row.push(Tile.createWall()); // Initial wall tile on this row
|
row.push(Tile.createWall()); // Initial wall tile on this row
|
||||||
for (let x = dungeonStartX; x <= dungeonEndX; x++) {
|
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);
|
||||||
}
|
}
|
||||||
row.push(Tile.createWall()); // Final wall tile on this row
|
row.push(Tile.createWall()); // Final wall tile on this row
|
||||||
@@ -190,7 +202,7 @@ class DungeonFactory {
|
|||||||
// Final row is all walls
|
// Final row is all walls
|
||||||
newTiles.push(new Array(newWidth).fill(Tile.createWall()));
|
newTiles.push(new Array(newWidth).fill(Tile.createWall()));
|
||||||
|
|
||||||
this.map = new TileMap(newTiles);
|
this.#map = new TileMap(newTiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
createCorridor(roomA, roomB) {
|
createCorridor(roomA, roomB) {
|
||||||
@@ -220,7 +232,7 @@ class DungeonFactory {
|
|||||||
|
|
||||||
while (x !== x2 || y !== y2) {
|
while (x !== x2 || y !== y2) {
|
||||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
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;
|
if (x !== x2) x += dx;
|
||||||
@@ -229,7 +241,7 @@ class DungeonFactory {
|
|||||||
|
|
||||||
// Ensure endpoint is carved
|
// Ensure endpoint is carved
|
||||||
if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) {
|
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 y = 1; y < this.height - 1; y++) {
|
||||||
//
|
//
|
||||||
for (let x = 1; x < this.width - 1; x++) {
|
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) {
|
if (!cell) {
|
||||||
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.map.get(x, y).isFloor()) {
|
if (this.#map.get(x, y).isFloor()) {
|
||||||
walkabilityCache.push([x, y]);
|
walkabilityCache.push([x, y]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,7 +275,7 @@ class DungeonFactory {
|
|||||||
|
|
||||||
for (let [x, y] of walkabilityCache) {
|
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 =
|
const surroundingFloorCount =
|
||||||
0 +
|
0 +
|
||||||
@@ -283,7 +295,7 @@ class DungeonFactory {
|
|||||||
|
|
||||||
if (surroundingFloorCount >= 7) {
|
if (surroundingFloorCount >= 7) {
|
||||||
// MAGIC NUMBER 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 y = 1; y < this.height - 1; y++) {
|
||||||
//
|
//
|
||||||
for (let x = 1; x < this.width - 1; x++) {
|
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) {
|
if (!cell) {
|
||||||
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.map.isFloorLike(x, y)) {
|
if (this.#map.isFloorLike(x, y)) {
|
||||||
walkabilityCache.push([x, y]);
|
walkabilityCache.push([x, y]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +321,7 @@ class DungeonFactory {
|
|||||||
const idx = this.random(0, walkabilityCache.length - 1);
|
const idx = this.random(0, walkabilityCache.length - 1);
|
||||||
const [x, y] = walkabilityCache[idx];
|
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?
|
// When spawning in, which direction should the player be oriented?
|
||||||
@@ -324,23 +336,23 @@ class DungeonFactory {
|
|||||||
// they don't face a wall upon spawning.
|
// they don't face a wall upon spawning.
|
||||||
const dirIdx = this.random(0, directions.length - 1);
|
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
|
// Add portals to isolated areas
|
||||||
addPortals() {
|
addPortals() {
|
||||||
let traversableTileCount = this.map.getFloorlikeTileCount();
|
let traversableTileCount = this.#map.getFloorlikeTileCount();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Find the player's start point, and let this be the
|
// Find the player's start point, and let this be the
|
||||||
// bases of area 0
|
// 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) {
|
if (tile.typeId === TileChars.PLAYER_START_POINT) {
|
||||||
return [x, y];
|
return [x, y];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = this.map.getAllTraversableTilesConnectedTo(x, y);
|
const result = this.#map.getAllTraversableTilesConnectedTo(x, y);
|
||||||
|
|
||||||
if (result.size === traversableTileCount) {
|
if (result.size === traversableTileCount) {
|
||||||
// There are no isolated areas, return
|
// There are no isolated areas, return
|
||||||
@@ -385,7 +397,7 @@ class DungeonFactory {
|
|||||||
const floorTiles = [];
|
const floorTiles = [];
|
||||||
for (let y = 0; y < this.height; y++) {
|
for (let y = 0; y < this.height; y++) {
|
||||||
for (let x = 0; x < this.width; x++) {
|
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 });
|
floorTiles.push({ x, y });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,11 +417,11 @@ class DungeonFactory {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Add monsters
|
// 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++) {
|
for (let i = 0; i < encouterCount; i++) {
|
||||||
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
||||||
if (this.map.tiles[pos.y][pos.x].isFloor()) {
|
if (this.#map.tiles[pos.y][pos.x].isFloor()) {
|
||||||
this.map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint("PLACEHOLDER_ENCOUNTER_ID");
|
this.#map.tiles[pos.y][pos.x] = Tile.createEncounterStartPoint("PLACEHOLDER_ENCOUNTER_ID");
|
||||||
// TODO: Add encounter to the dungeon's "roaming entities" array.
|
// TODO: Add encounter to the dungeon's "roaming entities" array.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,6 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.faint {
|
.faint {
|
||||||
opacity: 0.42;
|
opacity: 0.6;
|
||||||
color: #44f;
|
color: #44f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,13 @@ export class Character {
|
|||||||
itemSlots;
|
itemSlots;
|
||||||
|
|
||||||
/** @type {Set<string>} Things the character is particularly proficient at. */
|
/** @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. */
|
/** @type {Set<Item} Things the character is particularly proficient at. */
|
||||||
items = new Map();
|
items = new Set();
|
||||||
|
|
||||||
|
/** @type {string[]} */
|
||||||
|
freeSlots = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} name The name of the character
|
* @param {string} name The name of the character
|
||||||
@@ -72,26 +75,18 @@ export class Character {
|
|||||||
|
|
||||||
/** Add an item to the equipment list
|
/** Add an item to the equipment list
|
||||||
* @param {Item} item
|
* @param {Item} item
|
||||||
* @param {number} count
|
|
||||||
*
|
*
|
||||||
* Maybe return the accumulated ItemSlots used?
|
* Maybe return the accumulated ItemSlots used?
|
||||||
*/
|
*/
|
||||||
addItem(item, count = 1) {
|
addItem(item) {
|
||||||
if (!Number.isInteger(count)) {
|
|
||||||
throw new Error("Number must be an integer");
|
|
||||||
}
|
|
||||||
if (!(item instanceof Item)) {
|
if (!(item instanceof Item)) {
|
||||||
console.debug("bad item", item);
|
console.debug("bad item", item);
|
||||||
throw new Error("item must be an instance of 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)
|
// todo removeItem(item, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { isIdSane, miniUid } from "../utils/id.js";
|
import { isIdSane, miniUid } from "../utils/id.js";
|
||||||
import { Xorshift32 } from "../utils/random.js";
|
import { Xorshift32 } from "../utils/random.js";
|
||||||
|
import { Security } from "../utils/security.js";
|
||||||
import { ItemBlueprint } from "./item.js";
|
import { ItemBlueprint } from "./item.js";
|
||||||
import { Player } from "./player.js";
|
import { Player } from "./player.js";
|
||||||
|
|
||||||
@@ -17,13 +18,22 @@ import { Player } from "./player.js";
|
|||||||
/** @typedef {import("./item.js").ItemBlueprint} ItemBlueprint */
|
/** @typedef {import("./item.js").ItemBlueprint} ItemBlueprint */
|
||||||
|
|
||||||
export class Game {
|
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 */
|
/** @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 */
|
/** @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.
|
* The characters in the game.
|
||||||
@@ -31,34 +41,40 @@ export class Game {
|
|||||||
* @protected
|
* @protected
|
||||||
* @type {Map<string,Character>}
|
* @type {Map<string,Character>}
|
||||||
*/
|
*/
|
||||||
_characters = new Map();
|
#characters = new Map();
|
||||||
|
get characters() {
|
||||||
|
return this.#characters;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @protected
|
* @protected
|
||||||
* @type {Map<string,Player>} Map of users in the game username->Player
|
* @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} */
|
/** @protected @type {Xorshift32} */
|
||||||
_random;
|
#random;
|
||||||
|
|
||||||
/** @type {Xorshift32} */
|
/** @type {Xorshift32} */
|
||||||
get random() {
|
get random() {
|
||||||
return this._random;
|
return this.#random;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {number} rngSeed Seed number used for randomization */
|
/** @param {number} rngSeed Seed number used for randomization */
|
||||||
constructor() {
|
constructor(rngSeed) {
|
||||||
this.rngSeed = Date.now();
|
this.seedRNG(rngSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
set rngSeed(rngSeed) {
|
seedRNG(rngSeed) {
|
||||||
this._random = new Xorshift32(rngSeed);
|
this.#random = new Xorshift32(rngSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerByUsername(username) {
|
getPlayerByUsername(username) {
|
||||||
console.log("GETTING PLAYER: `%s`", 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?} passwordHash
|
||||||
* @param {string?} salt
|
* @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) {
|
createPlayer(username, passwordHash = undefined, salt = undefined) {
|
||||||
if (this._players.has(username)) {
|
if (this.#players.has(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = new Player(
|
passwordHash ??= "";
|
||||||
username,
|
salt ??= Security.generateHash(miniUid());
|
||||||
typeof passwordHash === "string" ? passwordHash : "",
|
|
||||||
typeof salt === "string" && salt.length > 0 ? salt : miniUid(),
|
|
||||||
);
|
|
||||||
|
|
||||||
this._players.set(username, player);
|
const player = new Player(username, passwordHash, salt);
|
||||||
|
|
||||||
|
this.#players.set(username, player);
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
@@ -99,7 +114,7 @@ export class Game {
|
|||||||
throw new Error("Invalid blueprintId!");
|
throw new Error("Invalid blueprintId!");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = this._itemBlueprints.get(blueprintId);
|
const existing = this.#itemBlueprints.get(blueprintId);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
console.warn("we tried to create the same item blueprint more than once", blueprintId, attributes);
|
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);
|
const result = new ItemBlueprint(attributes);
|
||||||
|
|
||||||
this._itemBlueprints.set(blueprintId, result);
|
this.#itemBlueprints.set(blueprintId, result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -123,6 +138,6 @@ export class Game {
|
|||||||
if (!isIdSane(blueprintId)) {
|
if (!isIdSane(blueprintId)) {
|
||||||
throw new Error(`blueprintId >>${blueprintId}<< is not a valid id`);
|
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";
|
import { Game } from "./game.js";
|
||||||
|
|
||||||
/** @constant @readonly @type {Game} Global instance of Game */
|
/** @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 */
|
/** @constant @readonly @type {string[]} Type of ammo that this item is, or that this item uses */
|
||||||
skills = [];
|
skills = [];
|
||||||
|
|
||||||
|
/** @constant @readonly @type {boolean} Can a person wearing this armor be stealthy? */
|
||||||
|
sneak;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +56,7 @@ export class ItemBlueprint extends ItemAttributes {
|
|||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param {object} o Object whose attributes we copy
|
* @param {ItemAttributes} o Object whose attributes we copy
|
||||||
*/
|
*/
|
||||||
constructor(o) {
|
constructor(o) {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -9,28 +9,28 @@
|
|||||||
* or magical portals to distant locations.
|
* or magical portals to distant locations.
|
||||||
*/
|
*/
|
||||||
export class Location {
|
export class Location {
|
||||||
/** @protected @type {string} */
|
/** @type {string} */
|
||||||
_id;
|
#id;
|
||||||
get id() {
|
get id() {
|
||||||
return this._id;
|
return this.#id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected @type {string} */
|
/** @type {string} */
|
||||||
_name;
|
#name;
|
||||||
get name() {
|
get name() {
|
||||||
return this._name;
|
return this.#name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected @type {string} */
|
/** @type {string} */
|
||||||
_description;
|
#description;
|
||||||
get description() {
|
get description() {
|
||||||
return this._description;
|
return this.#description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected @type {Map<string,Portal>} */
|
/** @type {Map<string,Portal>} */
|
||||||
_portals = new Map();
|
#portals = new Map();
|
||||||
get portals() {
|
get portals() {
|
||||||
return this._portals;
|
return this.#portals;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,8 +39,8 @@ export class Location {
|
|||||||
* @param {string} description
|
* @param {string} description
|
||||||
*/
|
*/
|
||||||
constructor(id, name, description) {
|
constructor(id, name, description) {
|
||||||
this._id = id;
|
this.#id = id;
|
||||||
this._name = name;
|
this.#name = name;
|
||||||
this._description = description;
|
this.#description = description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import WebSocket from "ws";
|
|
||||||
import { Character } from "./character.js";
|
|
||||||
import { Config } from "./../config.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.
|
* Player Account.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Player } from "./player.js";
|
import { Player } from "./player.js";
|
||||||
import { mustBeString, mustBe } from "../utils/mustbe.js";
|
import { mustBeString, mustBe } from "../utils/mustbe.js";
|
||||||
import { Scene } from "../scenes/scene.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 */
|
/** @typedef {import("ws").WebSocket} WebSocket */
|
||||||
|
|
||||||
@@ -42,7 +42,8 @@ export class Session {
|
|||||||
* @param {Scene} scene
|
* @param {Scene} scene
|
||||||
*/
|
*/
|
||||||
setScene(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)) {
|
if (!(scene instanceof Scene)) {
|
||||||
throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`);
|
throw new Error(`Expected instance of Scene, got a ${typeof scene}: >>${scene}<<`);
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,7 @@ export class Session {
|
|||||||
console.error("Trying to send a message without a valid websocket", messageType, args);
|
console.error("Trying to send a message without a valid websocket", messageType, args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._websocket.send(formatMessage(messageType, ...args));
|
this._websocket.send(Messages.formatMessage(messageType, ...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +113,7 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.send(
|
this.send(
|
||||||
MessageType.PROMPT, // message type
|
Messages.PROMPT, // message type
|
||||||
text, // TODO: prompt text must be string or an array of strings
|
text, // TODO: prompt text must be string or an array of strings
|
||||||
mustBe(options, "object"),
|
mustBe(options, "object"),
|
||||||
);
|
);
|
||||||
@@ -125,12 +126,17 @@ export class Session {
|
|||||||
* @param {object?} options message options for the client.
|
* @param {object?} options message options for the client.
|
||||||
*/
|
*/
|
||||||
sendText(text, options = {}) {
|
sendText(text, options = {}) {
|
||||||
this.send(MessageType.TEXT, text, options);
|
this.send(Messages.TEXT, text, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string|string[]} errorMessage */
|
/** @param {string|string[]} errorMessage */
|
||||||
sendError(errorMessage, options = { verbatim: true, error: true }) {
|
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
|
// The client should know not to format calamaties anyway, but we add “preformatted” anyway
|
||||||
console.info("CALAMITY", errorMessage);
|
console.info("CALAMITY", errorMessage);
|
||||||
this.send(MessageType.CALAMITY, errorMessage, { verbatim: true, calamity: true });
|
this.send(Messages.CALAMITY, errorMessage, { verbatim: true, calamity: true });
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +156,6 @@ export class Session {
|
|||||||
* @param {any?} value
|
* @param {any?} value
|
||||||
*/
|
*/
|
||||||
sendSystemMessage(systemMessageType, value = undefined) {
|
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 { Security } from "../../utils/security.js";
|
||||||
import { Config } from "../../config.js";
|
import { Config } from "../../config.js";
|
||||||
import { GameScene } from "../gameLoop/gameScene.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 { Prompt } from "../prompt.js";
|
||||||
import { Scene } from "../scene.js";
|
import { Scene } from "../scene.js";
|
||||||
import { gGame } from "../../models/globals.js";
|
import { gGame } from "../../models/globals.js";
|
||||||
|
|
||||||
/** @typedef {import("../../models/player.js").Player} Player */
|
/** @typedef {import("../../models/player.js").Player} Player */
|
||||||
|
|
||||||
/** @property {Session} session */
|
|
||||||
export class AuthenticationScene extends Scene {
|
export class AuthenticationScene extends Scene {
|
||||||
introText = [
|
|
||||||
"= Welcome!", //
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @type {Player} */
|
/** @type {Player} */
|
||||||
player;
|
player;
|
||||||
|
|
||||||
onReady() {
|
onReady() {
|
||||||
|
this.session.sendText("= Welcome");
|
||||||
this.show(UsernamePrompt);
|
this.show(UsernamePrompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,25 +53,24 @@ export class AuthenticationScene extends Scene {
|
|||||||
// | __/| | | (_) | | | | | | |_) | |_
|
// | __/| | | (_) | | | | | | |_) | |_
|
||||||
// |_| |_| \___/|_| |_| |_| .__/ \__|
|
// |_| |_| \___/|_| |_| |_| .__/ \__|
|
||||||
// |_|
|
// |_|
|
||||||
|
/** @property {AuthenticationScene} scene */
|
||||||
class UsernamePrompt extends Prompt {
|
class UsernamePrompt extends Prompt {
|
||||||
//
|
//
|
||||||
promptText = [
|
message = [
|
||||||
"Please enter your username:", //
|
"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
|
// When player types :help
|
||||||
helpText = [
|
help = [
|
||||||
"This is where you log in.",
|
"Enter your username to proceed with loggin in",
|
||||||
"If you don't already have a player profile on this server, you can type *:create* to create one",
|
"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 };
|
||||||
|
|
||||||
//
|
/** @returns {AuthenticationScene} workaround for proper type hinting */
|
||||||
// Let the client know that we're asking for a username
|
|
||||||
promptOptions = { username: true };
|
|
||||||
|
|
||||||
/** @returns {AuthenticationScene} */
|
|
||||||
get scene() {
|
get scene() {
|
||||||
return this._scene;
|
return this._scene;
|
||||||
}
|
}
|
||||||
@@ -126,12 +121,8 @@ class UsernamePrompt extends Prompt {
|
|||||||
// |_|
|
// |_|
|
||||||
class PasswordPrompt extends Prompt {
|
class PasswordPrompt extends Prompt {
|
||||||
//
|
//
|
||||||
promptText = "Please enter your password";
|
message = "Please enter your password";
|
||||||
|
options = { password: true };
|
||||||
//
|
|
||||||
// Let the client know that we're asking for a password
|
|
||||||
// so it can set <input type="password">
|
|
||||||
promptOptions = { password: true };
|
|
||||||
|
|
||||||
get player() {
|
get player() {
|
||||||
return this.scene.player;
|
return this.scene.player;
|
||||||
@@ -156,9 +147,11 @@ class PasswordPrompt extends Prompt {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const player = this.scene.player;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Block users who enter bad passwords too many times.
|
// 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.blockedUntil = Date.now() + Config.accountLockoutSeconds * 1000;
|
||||||
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
this.calamity("You have been locked out for too many failed password attempts, come back later");
|
||||||
return;
|
return;
|
||||||
@@ -167,7 +160,7 @@ class PasswordPrompt extends Prompt {
|
|||||||
//
|
//
|
||||||
// Handle blocked users.
|
// Handle blocked users.
|
||||||
// They don't even get to have their password verified.
|
// 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.
|
// Try to re-login too soon, and your lockout lasts longer.
|
||||||
this.blockedUntil += Config.accountLockoutSeconds * 1000;
|
this.blockedUntil += Config.accountLockoutSeconds * 1000;
|
||||||
@@ -177,23 +170,23 @@ class PasswordPrompt extends Prompt {
|
|||||||
|
|
||||||
//
|
//
|
||||||
// Verify the password against the hash we've stored.
|
// 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.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();
|
this.execute();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.player.lastSucessfulLoginAt = new Date();
|
player.lastSucessfulLoginAt = new Date();
|
||||||
this.player.failedPasswordsSinceLastLogin = 0;
|
player.failedPasswordsSinceLastLogin = 0;
|
||||||
|
|
||||||
//
|
//
|
||||||
// We do not allow a player to be logged in more than once!
|
// We do not allow a player to be logged in more than once!
|
||||||
if (this.player.loggedIn) {
|
if (player.loggedIn) {
|
||||||
this.calamity("This player is already logged in");
|
this.calamity("player is already logged in");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Prompt } from "../prompt.js";
|
||||||
import { Scene } from "../scene.js";
|
import { Scene } from "../scene.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,7 +7,38 @@ import { Scene } from "../scene.js";
|
|||||||
* It's here we listen for player commands.
|
* It's here we listen for player commands.
|
||||||
*/
|
*/
|
||||||
export class GameScene extends Scene {
|
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
|
= 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 {
|
// export class PartyCreationState extends State {
|
||||||
/**
|
// /**
|
||||||
* @proteted
|
// * @proteted
|
||||||
* @type {(msg: WebsocketMessage) => }
|
// * @type {(msg: WebsocketMessage) => }
|
||||||
*
|
// *
|
||||||
* NOTE: Should this be a stack?
|
// * NOTE: Should this be a stack?
|
||||||
*/
|
// */
|
||||||
_dynamicMessageHandler;
|
// _dynamicMessageHandler;
|
||||||
|
//
|
||||||
/** @param {Session} session */
|
// /** @param {Session} session */
|
||||||
constructor(session) {
|
// constructor(session) {
|
||||||
super();
|
// super();
|
||||||
this.session = session;
|
// this.session = session;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/** We attach (and execute) the next state */
|
// /** We attach (and execute) the next state */
|
||||||
onAttach() {
|
// onAttach() {
|
||||||
const charCount = this.session.player.characters.size;
|
// const charCount = this.session.player.characters.size;
|
||||||
|
//
|
||||||
//NOTE: could use async to optimize performance
|
// //NOTE: could use async to optimize performance
|
||||||
const createPartyLogo = frameText(figlet.textSync("Create Your Party"), {
|
// const createPartyLogo = frameText(figlet.textSync("Create Your Party"), {
|
||||||
vPadding: 0,
|
// vPadding: 0,
|
||||||
frameChars: "§=§§§§§§",
|
// frameChars: "§=§§§§§§",
|
||||||
});
|
// });
|
||||||
|
//
|
||||||
this.sendText(createPartyLogo, { preformatted: true });
|
// this.sendText(createPartyLogo, { preformatted: true });
|
||||||
|
//
|
||||||
this.session.sendText(["", `Current party size: ${charCount}`, `Max party size: ${Config.maxPartySize}`]);
|
// this.session.sendText(["", `Current party size: ${charCount}`, `Max party size: ${Config.maxPartySize}`]);
|
||||||
const min = 1;
|
// const min = 1;
|
||||||
const max = Config.maxPartySize - charCount;
|
// const max = Config.maxPartySize - charCount;
|
||||||
const prompt = [
|
// const prompt = [
|
||||||
`Please enter an integer between ${min} - ${max}`,
|
// `Please enter an integer between ${min} - ${max}`,
|
||||||
"((type *:help* to get more info about party size))",
|
// "((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?`);
|
// this.sendText(`You can create a party with ${min} - ${max} characters, how big should your party be?`);
|
||||||
|
//
|
||||||
/** @param {WebsocketMessage} m */
|
// /** @param {WebsocketMessage} m */
|
||||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/** @param {WebsocketMessage} m */
|
// /** @param {WebsocketMessage} m */
|
||||||
receiveCharacterCount(m) {
|
// receiveCharacterCount(m) {
|
||||||
if (m.isHelpRequest()) {
|
// if (m.isHelpRequest()) {
|
||||||
return this.partySizeHelp();
|
// return this.partySizeHelp();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (!m.isInteger()) {
|
// if (!m.isInteger()) {
|
||||||
this.sendError("You didn't enter an integer");
|
// this.sendError("You didn't enter an integer");
|
||||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const numCharactersToCreate = Number(m.text);
|
// const numCharactersToCreate = Number(m.text);
|
||||||
if (numCharactersToCreate > Config.maxPartySize) {
|
// if (numCharactersToCreate > Config.maxPartySize) {
|
||||||
this.sendError("Number too high");
|
// this.sendError("Number too high");
|
||||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (numCharactersToCreate < 1) {
|
// if (numCharactersToCreate < 1) {
|
||||||
this.sendError("Number too low");
|
// this.sendError("Number too low");
|
||||||
this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
// this.sendPrompt(prompt, (m) => this.receiveCharacterCount(m));
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
this.sendText(`Let's create ${numCharactersToCreate} character(s) for you :)`);
|
// this.sendText(`Let's create ${numCharactersToCreate} character(s) for you :)`);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
partySizeHelp() {
|
// partySizeHelp() {
|
||||||
this.sendText([
|
// this.sendText([
|
||||||
`Your party can consist of 1 to ${Config.maxPartySize} characters.`,
|
// `Your party can consist of 1 to ${Config.maxPartySize} characters.`,
|
||||||
"",
|
// "",
|
||||||
"* Large parties tend live longer",
|
// "* Large parties tend live longer",
|
||||||
`* If you have fewer than ${Config.maxPartySize} characters, you can`,
|
// `* If you have fewer than ${Config.maxPartySize} characters, you can`,
|
||||||
" hire extra characters in your local inn.",
|
// " hire extra characters in your local inn.",
|
||||||
"* large parties level slower because there are more",
|
// "* large parties level slower because there are more",
|
||||||
" characters to share the Experience Points",
|
// " characters to share the Experience Points",
|
||||||
"* The individual members of small parties get better",
|
// "* The individual members of small parties get better",
|
||||||
" loot because they don't have to share, but it",
|
// " loot because they don't have to share, but it",
|
||||||
" a lot of skill to accumulate loot as fast a larger",
|
// " a lot of skill to accumulate loot as fast a larger",
|
||||||
" party can",
|
// " party can",
|
||||||
]);
|
// ]);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (Math.PI < 0 && Session && WebsocketMessage) {
|
// if (Math.PI < 0 && Session && WebsocketMessage) {
|
||||||
("STFU Linda");
|
// ("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 { Config } from "../../config.js";
|
||||||
import { gGame } from "../../models/globals.js";
|
import { Prompt } from "../prompt.js";
|
||||||
import { Security } from "../../utils/security.js";
|
|
||||||
import { Scene } from "../scene.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 {
|
export class PlayerCreationScene extends Scene {
|
||||||
introText = "= Create Player";
|
intro = "= Create Player";
|
||||||
|
|
||||||
/** @protected @type {Player} */
|
/** @protected @type {Player} */
|
||||||
player;
|
player;
|
||||||
@@ -16,11 +19,11 @@ export class PlayerCreationScene extends Scene {
|
|||||||
onReady() {
|
onReady() {
|
||||||
//
|
//
|
||||||
// If there are too many players, stop allowing new players in.
|
// 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.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.player = player;
|
||||||
|
|
||||||
this.session.sendSystemMessage("salt", player.salt);
|
this.session.sendSystemMessage("salt", player.salt);
|
||||||
this.session.sendText(`Username _*${username}*_ is available, and I've reserved it for you :)`);
|
|
||||||
|
|
||||||
//
|
this.session.sendText(`Username _*${username}*_ has been reserved for you`);
|
||||||
this.session.sendError("TODO: create a createPasswordPrompt and display it.");
|
|
||||||
|
this.show(PasswordPrompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +50,153 @@ export class PlayerCreationScene extends Scene {
|
|||||||
*/
|
*/
|
||||||
passwordAccepted(password) {
|
passwordAccepted(password) {
|
||||||
this.password = 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.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("../models/session.js").Session} Session */
|
||||||
/** @typedef {import("../utils/message.js").MessageType} MessageType */
|
|
||||||
/** @typedef {import("../utils/message.js").WebsocketMessage} WebsocketMessage */
|
/** @typedef {import("../utils/message.js").WebsocketMessage} WebsocketMessage */
|
||||||
/** @typedef {import("./scene.js").Scene} Scene */
|
/** @typedef {import("./scene.js").Scene} Scene */
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@
|
|||||||
* - onColon(...)
|
* - onColon(...)
|
||||||
*/
|
*/
|
||||||
export class Prompt {
|
export class Prompt {
|
||||||
/** @type {Scene} */
|
/** @protected @type {Scene} */
|
||||||
_scene;
|
_scene;
|
||||||
|
|
||||||
/** @type {Scene} */
|
/** @type {Scene} */
|
||||||
@@ -30,20 +29,25 @@ export class Prompt {
|
|||||||
* Values: string containing the help text
|
* Values: string containing the help text
|
||||||
*
|
*
|
||||||
* If you want truly custom help texts, you must override the onHelpFallback function,
|
* 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.
|
* dictionary entirely.
|
||||||
*
|
*
|
||||||
* @constant
|
* @constant
|
||||||
* @readonly
|
* @readonly
|
||||||
* @type {Record<string, string|string[]>}
|
* @type {Record<string, string|string[]>}
|
||||||
*/
|
*/
|
||||||
helpText = {
|
help = {};
|
||||||
"": "Sorry, no help available. Figure it out yourself, adventurer", // default help text
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {string|string[]} Default prompt text to send if we don't want to send something in the execute() call. */
|
/**
|
||||||
promptText = [
|
* Default prompt text to send if we don't want to send something in the execute() call.
|
||||||
"Please enter some very important info", // Stupid placeholder text
|
*
|
||||||
|
* 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
|
"((or type :quit to run away))", // strings in double parentheses is rendered shaded/faintly
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -51,28 +55,21 @@ export class Prompt {
|
|||||||
/* @example
|
/* @example
|
||||||
*
|
*
|
||||||
* // if the prompt expects a username
|
* // if the prompt expects a username
|
||||||
* promptOptions = { username : true };
|
* options = { username : true };
|
||||||
*
|
*
|
||||||
* // if the prompt expects a password
|
* // if the prompt expects a password
|
||||||
* promptOptions = { password : true };
|
* options = { password : true };
|
||||||
*/
|
*/
|
||||||
promptOptions = {};
|
options = {};
|
||||||
|
|
||||||
/** @type {Session} */
|
/** @type {Session} */
|
||||||
get session() {
|
get session() {
|
||||||
return this.scene.session;
|
return this._scene.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Scene} scene */
|
/** @param {Scene} scene */
|
||||||
constructor(scene) {
|
constructor(scene) {
|
||||||
this._scene = 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
|
* It's here you want to send the prompt text via the sendPrompt() method
|
||||||
*/
|
*/
|
||||||
execute() {
|
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]` */
|
/** Triggered when user types `:help [some optional topic]` */
|
||||||
onHelp(topic) {
|
onHelp(topic) {
|
||||||
if (this.helpText[topic]) {
|
if (!this.help) {
|
||||||
this.sendText(this.helpText[topic]);
|
this.sendText("No help available at this moment - figure it out yourself, adventurer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.help[topic]) {
|
||||||
|
this.sendText(this.help[topic]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
|
import { Prompt } from "./prompt.js";
|
||||||
|
|
||||||
/** @typedef {import("../utils/messages.js").WebsocketMessage} WebsocketMessage */
|
/** @typedef {import("../utils/messages.js").WebsocketMessage} WebsocketMessage */
|
||||||
/** @typedef {import("../models/session.js").Session} Session */
|
/** @typedef {import("../models/session.js").Session} Session */
|
||||||
/** @typedef {import("./prompt.js").Prompt } Prompt */
|
/** @typedef {import("./prompt.js").Prompt } Prompt */
|
||||||
|
/** @typedef {new (scene: Scene) => Prompt} PromptClassReference */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scene - a class for showing one or more prompts in a row.
|
* Scene - a class for showing one or more prompts in a row.
|
||||||
@@ -16,13 +18,8 @@ import { sprintf } from "sprintf-js";
|
|||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
export class Scene {
|
export class Scene {
|
||||||
/**
|
/** @constant @readonly @type {string|string[]|PromptClassReference} Text or prompt to show when this scene begins */
|
||||||
* @type {string|string[]} This text is shown when the scene begins
|
intro;
|
||||||
*/
|
|
||||||
introText = "";
|
|
||||||
|
|
||||||
/** @constant @readonly @type {Prompt?} */
|
|
||||||
introPrompt;
|
|
||||||
|
|
||||||
/** @readonly @constant @protected @type {Session} */
|
/** @readonly @constant @protected @type {Session} */
|
||||||
#session;
|
#session;
|
||||||
@@ -47,21 +44,15 @@ export class Scene {
|
|||||||
/** @param {Session} session */
|
/** @param {Session} session */
|
||||||
execute(session) {
|
execute(session) {
|
||||||
this.#session = session;
|
this.#session = session;
|
||||||
|
|
||||||
if (this.introText) {
|
|
||||||
this.session.sendText(this.introText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.introPrompt) {
|
|
||||||
this.showPrompt(this.introPrompt);
|
|
||||||
} else {
|
|
||||||
this.onReady();
|
this.onReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onReady() {
|
||||||
|
if (!this.intro) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @abstract */
|
this.show(this.intro);
|
||||||
onReady() {
|
|
||||||
throw new Error("Abstract method must be implemented by subclass");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Prompt} prompt */
|
/** @param {Prompt} prompt */
|
||||||
@@ -70,9 +61,33 @@ export class Scene {
|
|||||||
prompt.execute();
|
prompt.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {new (scene: Scene) => Prompt} promptClassReference */
|
/** @param {string|string[]} text */
|
||||||
show(promptClassReference) {
|
showText(text) {
|
||||||
this.showPrompt(new promptClassReference(this));
|
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,
|
message,
|
||||||
type: typeof 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);
|
this.currentPrompt.onReply(message.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +127,7 @@ export class Scene {
|
|||||||
* - call this method directly
|
* - call this method directly
|
||||||
*/
|
*/
|
||||||
onQuit() {
|
onQuit() {
|
||||||
this.currentPrompt.onQuit();
|
this.currentPrompt?.onQuit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +147,7 @@ export class Scene {
|
|||||||
* @param {WebsocketMessage} message
|
* @param {WebsocketMessage} message
|
||||||
*/
|
*/
|
||||||
onHelp(message) {
|
onHelp(message) {
|
||||||
this.currentPrompt.onHelp(message.text);
|
this.currentPrompt?.onHelp(message.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,7 +205,7 @@ export class Scene {
|
|||||||
* @param {WebsocketMessage} message
|
* @param {WebsocketMessage} message
|
||||||
*/
|
*/
|
||||||
onColon(message) {
|
onColon(message) {
|
||||||
const handledByPrompt = this.currentPrompt.onColon(message.command, message.args);
|
const handledByPrompt = this.currentPrompt?.onColon(message.command, message.args);
|
||||||
|
|
||||||
if (!handledByPrompt) {
|
if (!handledByPrompt) {
|
||||||
this.onColonFallback(message.command, message.args);
|
this.onColonFallback(message.command, message.args);
|
||||||
@@ -203,7 +223,7 @@ export class Scene {
|
|||||||
const n = Number(args[0]);
|
const n = Number(args[0]);
|
||||||
|
|
||||||
this.session.sendText(
|
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 {
|
export class CharacterSeeder {
|
||||||
constructor() {
|
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.!!
|
// Which we easily could have.!!
|
||||||
roll = {
|
roll.d = (max, min = 1) => gGame.random.within(min, max)
|
||||||
d: (max, min = 1) => {
|
roll.d6 = () => roll.d(6)
|
||||||
return gGame.random.within(min, max);
|
roll.d8 = () => roll.d(8)
|
||||||
},
|
|
||||||
d6: () => {
|
|
||||||
return gGame.random.within(1, 6);
|
|
||||||
},
|
|
||||||
d8: () => {
|
|
||||||
return gGame.random.within(1, 8);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,14 +44,14 @@ export class CharacterSeeder {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Character} character
|
* @param {Character} character
|
||||||
* @param {...string} skills
|
* @param {...string} proficiencies
|
||||||
*/
|
*/
|
||||||
addSkillsToCharacter(character, ...skills) {
|
addProficienciesToCharacter(character, ...proficiencies) {
|
||||||
for (const skill of skills) {
|
for (const prof of proficiencies) {
|
||||||
if (!isIdSane(skill)) {
|
if (!isIdSane(prof)) {
|
||||||
throw new Error(`Skill id >>${skill}<< is insane!`);
|
throw new Error(`Proficiency id >>${prof}<< is insane!`);
|
||||||
}
|
}
|
||||||
character.skills.add(skill);
|
character.proficiencies.add(prof);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,17 +64,24 @@ export class CharacterSeeder {
|
|||||||
|
|
||||||
createCharacter() {
|
createCharacter() {
|
||||||
const c = new Character();
|
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 =
|
c.name =
|
||||||
gGame.random.oneOf("sir ", "madam ", "mister ", "miss ", "", "", "") + // prefix
|
gGame.random.oneOf("sir ", "madam ", "mister ", "miss ", "", "", "") +
|
||||||
"random " + // name
|
"random " + // name
|
||||||
gGame.random.get().toString(); // suffix
|
gGame.random.next().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
rollSkills(c) {
|
||||||
c.awareness = roll.d6() + 2;
|
c.awareness = roll.d6() + 2;
|
||||||
c.grit = roll.d6() + 2;
|
c.grit = roll.d6() + 2;
|
||||||
c.knowledge = roll.d6() + 2;
|
c.knowledge = roll.d6() + 2;
|
||||||
@@ -90,11 +89,6 @@ export class CharacterSeeder {
|
|||||||
c.meleeCombat = roll.d6() + 2;
|
c.meleeCombat = roll.d6() + 2;
|
||||||
c.rangedCombat = roll.d6() + 2;
|
c.rangedCombat = roll.d6() + 2;
|
||||||
c.skulduggery = roll.d6() + 2;
|
c.skulduggery = roll.d6() + 2;
|
||||||
|
|
||||||
this.applyAncestry(c);
|
|
||||||
this.applyFoundation(c);
|
|
||||||
|
|
||||||
return c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyAncestry(c) {
|
applyAncestry(c) {
|
||||||
@@ -168,44 +162,61 @@ export class CharacterSeeder {
|
|||||||
* @param {string|number} Foundation to add to character
|
* @param {string|number} Foundation to add to character
|
||||||
*/
|
*/
|
||||||
applyFoundation(c, foundation = ":random") {
|
applyFoundation(c, foundation = ":random") {
|
||||||
switch (foundation) {
|
if (foundation == ":random") {
|
||||||
case ":random":
|
return this.applyFoundation(c, roll.d(20)); // according to the rulebook, roll a d20 and reroll any invalid results.
|
||||||
return this.applyFoundation(c, roll.d(3));
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Brawler
|
// Brawler
|
||||||
// ------
|
// ------
|
||||||
case 1:
|
if (foundation === 1 || foundation === ":brawler") {
|
||||||
case ":brawler":
|
|
||||||
c.foundation = "Brawler";
|
c.foundation = "Brawler";
|
||||||
c.skills.add(":armor.light");
|
|
||||||
c.silver = 40;
|
|
||||||
c.maxHitPoints = c.currentHitPoints = 15;
|
c.maxHitPoints = c.currentHitPoints = 15;
|
||||||
c.itemSlots = 7;
|
c.itemSlots = 7;
|
||||||
|
c.silver = 40;
|
||||||
|
|
||||||
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
c.meleeCombat = Math.max(c.meleeCombat, 10);
|
||||||
c.knowledge = Math.min(c.knowledge, 10);
|
c.knowledge = Math.min(c.knowledge, 10);
|
||||||
|
|
||||||
|
this.addProficienciesToCharacter(
|
||||||
|
c,
|
||||||
|
":armor.light",
|
||||||
|
":weapon.weird.spiked_gauntlets"
|
||||||
|
);
|
||||||
|
|
||||||
this.addItemsToCharacter(
|
this.addItemsToCharacter(
|
||||||
c, //
|
c, //
|
||||||
":armor.light.studded_leather",
|
":armor.light.studded_leather",
|
||||||
":weapon.weird.spiked_gauntlets",
|
":weapon.weird.spiked_gauntlets",
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addSkillsToCharacter(c, ":weapon.weird.spiked_gauntlets");
|
return;
|
||||||
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// DRUID
|
// DRUID
|
||||||
// ------
|
// ------
|
||||||
case 2:
|
if (foundation === 2 || foundation === ":druid") {
|
||||||
case ":druid":
|
|
||||||
c.foundation = "Druid";
|
c.foundation = "Druid";
|
||||||
c.silver = 40;
|
|
||||||
c.maxHitPoints = this.currentHitPoints = 15;
|
c.silver = 10;
|
||||||
c.itemSlots = 7;
|
c.itemSlots = 5;
|
||||||
c.meleeCombat = Math.max(this.meleeCombat, 10);
|
c.maxHitPoints = this.currentHitPoints = 10;
|
||||||
c.knowledge = Math.min(this.knowledge, 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(
|
this.addItemsToCharacter(
|
||||||
c, //
|
c, //
|
||||||
":armor.light.leather",
|
":armor.light.leather",
|
||||||
@@ -213,385 +224,350 @@ export class CharacterSeeder {
|
|||||||
":kit.poisoners_kit",
|
":kit.poisoners_kit",
|
||||||
":kit.healers_kit",
|
":kit.healers_kit",
|
||||||
);
|
);
|
||||||
this.addSkillsToCharacter(
|
|
||||||
c, //
|
return;
|
||||||
":armor.light.sleather",
|
}
|
||||||
":armor.light.hide",
|
|
||||||
":weapon.light.sickle",
|
//
|
||||||
);
|
// FENCER
|
||||||
break;
|
// -------
|
||||||
case 3:
|
if (foundation === 3 || foundation === ":fencer") {
|
||||||
case ":fencer":
|
|
||||||
c.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.silver = 40;
|
||||||
c.itemSlots = 5;
|
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(
|
this.addItemsToCharacter(
|
||||||
c, //
|
c, //
|
||||||
":armor.light.leather",
|
":armor.light.leather",
|
||||||
|
":weapon.basic.dagger",
|
||||||
":weapon.light.rapier",
|
":weapon.light.rapier",
|
||||||
":weapon.light.dagger",
|
|
||||||
);
|
);
|
||||||
break;
|
}
|
||||||
case 4:
|
|
||||||
case ":guard":
|
if (foundation === 4 || foundation === ":guard") {
|
||||||
|
|
||||||
c.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.silver = 50;
|
||||||
c.itemSlots = 5;
|
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(
|
this.addItemsToCharacter(
|
||||||
c, //
|
c, //
|
||||||
":armor.medium.breastplate",
|
":armor.medium.breastplate",
|
||||||
":weapon.weird.halberd",
|
|
||||||
":lighting.bulls_eye_lantern",
|
":lighting.bulls_eye_lantern",
|
||||||
|
":map.city.hovedstad",
|
||||||
|
":misc.lamp_oil",
|
||||||
":misc.signal_whistle",
|
":misc.signal_whistle",
|
||||||
":maps.area.hvedstad",
|
":weapon.specialist.halberd",
|
||||||
);
|
);
|
||||||
break;
|
return
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
//---------------------------------------------------------------------------------------
|
|
||||||
//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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.PI < 0 && Player) {
|
if (foundation === 5 || foundation === ":magician") {
|
||||||
("STFU Linda");
|
|
||||||
|
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",
|
name: "Dagger",
|
||||||
description: "Small shady blady",
|
description: "Basic small shady blady",
|
||||||
itemSlots: 0.5,
|
itemSlots: 1,
|
||||||
damage: 3,
|
damage: 3,
|
||||||
melee: true,
|
melee: true,
|
||||||
ranged: true,
|
ranged: true, //
|
||||||
|
count: 3, // basic daggers always come in a bundle of three
|
||||||
|
maxCount: 3,
|
||||||
specialEffect: ":effect.weapon.fast",
|
specialEffect: ":effect.weapon.fast",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,6 +62,31 @@ export class ItemSeeder {
|
|||||||
specialEffect: "TBD",
|
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",
|
description: "Padded and hardened leather with metal stud reinforcement",
|
||||||
itemSlots: 3,
|
itemSlots: 3,
|
||||||
specialEffect: "TBD",
|
specialEffect: "TBD",
|
||||||
|
sneak: false,
|
||||||
armorHitPoints: 10,
|
armorHitPoints: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
gGame.addItemBlueprint(":armor.light.leather", {
|
gGame.addItemBlueprint(":armor.light.leather", {
|
||||||
name: "Leather Armor",
|
name: "Leather Armor",
|
||||||
description: "Padded and hardened leather",
|
description: "Padded and hardened leather",
|
||||||
@@ -72,6 +109,29 @@ export class ItemSeeder {
|
|||||||
specialEffect: "TBD",
|
specialEffect: "TBD",
|
||||||
armorHitPoints: 6,
|
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,
|
count: 20,
|
||||||
maxCount: 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 { AuthenticationScene } from "./scenes/authentication/authenticationScene.js";
|
||||||
import { MessageType, WebsocketMessage, formatMessage } from "./utils/messages.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.
|
* The order of the elements of this array matters.
|
||||||
*/
|
*/
|
||||||
const opcodes = [
|
const opcodes = [
|
||||||
["(^|\\n)=", "($|\\n)", "$1<h1>$2</h1>$3"],
|
["(^|\\n)=", "($|\\n)", "$1<h1>$2</h1>$3"], // lines with large headline begins with =
|
||||||
["(^|\\n)==", "($|\\n)", "$1<h2>$2</h2>$3"],
|
["(^|\\n)==", "($|\\n)", "$1<h2>$2</h2>$3"], // lines with sub-headline begins with ==
|
||||||
["---", "---", "<span class='strike'>$1</span>"],
|
["---", "---", "<span class='strike'>$1</span>"], // ---trike through---
|
||||||
["___", "___", "<span class='underline'>$1</span>"],
|
["___", "___", "<span class='underline'>$1</span>"], // ___underline___
|
||||||
["(?:[,]{3})", "(?:[,]{3})", "<span class='undercurl'>$1</span>"],
|
["(?:[,]{3})", "(?:[,]{3})", "<span class='undercurl'>$1</span>"], // ,,,undercurl,,,
|
||||||
["(?:[(]{2})", "(?:[)]{2})", "<span class='faint'>$1</span>"],
|
["(?:[(]{2})", "(?:[)]{2})", "<span class='faint'>$1</span>"], // ((faint text))
|
||||||
["_", "_", "<span class='italic'>$1</span>"],
|
["(?:_\\*)", "(?:\\*_)", "<span class='bold italic'>$1</span>"], // _*bold and italic*_
|
||||||
["\\*", "\\*", "<span class='bold'>$1</span>"],
|
["(?:\\*_)", "(?:_\\*)", "<span class='bold italic'>$1</span>"], // *_bold and italic_*
|
||||||
["\\[\\[([a-zA-Z0-9_ ]+)\\[\\[", "\\]\\]", "<span class='$1'>$2</span>"],
|
["_", "_", "<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>} */
|
/** @type{Array.Array.<Regexp,string>} */
|
||||||
const regexes = [];
|
const regexes = [];
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as regex from "./regex.js";
|
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."
|
"\.uid\.", // Mini-uids always begin with ".uid."
|
||||||
"[a-z0-9]{6,}$", // Terminated by 6 or more random numbers and lowercase letters.
|
"[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
|
"^:", // All ids start with a colon
|
||||||
"([a-z0-9]+\.)*?", // Middle -optional- part :myid.gogle.thing.thang.thong
|
"([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.
|
"[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.
|
* 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
|
* @property {any[]} args
|
||||||
*/
|
*/
|
||||||
export class WebsocketMessage {
|
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;
|
_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;
|
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.
|
* @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 " +
|
"Could not create client message. Attempting to parse json, but data was not even a string, it was a " +
|
||||||
typeof msgData,
|
typeof msgData,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(msgData);
|
data = JSON.parse(msgData);
|
||||||
} catch (_) {
|
} catch {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
|
`Could not create client message. Attempting to parse json, but data was invalid json: >>> ${msgData} <<<`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
* using the xorshift32 method.
|
* using the xorshift32 method.
|
||||||
*/
|
*/
|
||||||
export class Xorshift32 {
|
export class Xorshift32 {
|
||||||
/* @type {number} */
|
|
||||||
initialSeed;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State holds a single uint32.
|
* State holds a single uint32.
|
||||||
* It's useful for staying within modulo 2**32.
|
* It's useful for staying within modulo 2**32.
|
||||||
*
|
*
|
||||||
* @type {Uint32Array}
|
* @type {Uint32Array}
|
||||||
*/
|
*/
|
||||||
state;
|
#state;
|
||||||
|
get state() {
|
||||||
|
return this.#state;
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {number} seed */
|
/** @param {number} seed */
|
||||||
constructor(seed) {
|
constructor(seed) {
|
||||||
@@ -21,41 +21,44 @@ export class Xorshift32 {
|
|||||||
seed = Math.floor(Math.random() * (maxInt32 - 1)) + 1;
|
seed = Math.floor(Math.random() * (maxInt32 - 1)) + 1;
|
||||||
}
|
}
|
||||||
seed = seed | 0;
|
seed = seed | 0;
|
||||||
this.state = Uint32Array.of(seed);
|
this.#state = Uint32Array.of(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected Shuffle the internal state. */
|
/** @protected Shuffle the internal state. */
|
||||||
shuffle() {
|
shuffle() {
|
||||||
this.state[0] ^= this.state[0] << 13;
|
this.#state[0] ^= this.#state[0] << 13;
|
||||||
this.state[0] ^= this.state[0] >>> 17;
|
this.#state[0] ^= this.#state[0] >>> 17;
|
||||||
this.state[0] ^= this.state[0] << 5;
|
this.#state[0] ^= this.#state[0] << 5;
|
||||||
|
|
||||||
// We could also do something like this:
|
// We could also do something like this:
|
||||||
// x ^= x << 13;
|
// x ^= x << 13;
|
||||||
// x ^= x >> 17;
|
// x ^= x >>> 17;
|
||||||
// x ^= x << 5;
|
// x ^= x << 5;
|
||||||
// return x;
|
// return x;
|
||||||
// But we'd have to xor the x with 2^32 after every op,
|
// And even though bitwise operations coerce numbers
|
||||||
// we get that "for free" by using the uint32array
|
// 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.
|
* Get a random number and shuffle the internal state.
|
||||||
* @returns {number} a pseudo-random positive integer.
|
* @returns {number} a pseudo-random positive integer.
|
||||||
*/
|
*/
|
||||||
get() {
|
next() {
|
||||||
this.shuffle();
|
this.shuffle();
|
||||||
return this.state[0];
|
return this.#state[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {number} x @returns {number} a positive integer lower than x */
|
/** @param {number} x @returns {number} a positive integer lower than x */
|
||||||
lowerThan(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 */
|
/** @param {number} x @returns {number} a positive integer lower than or equal to x */
|
||||||
lowerThanOrEqual(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
|
* @method
|
||||||
* @returns {<T>}
|
* @template T
|
||||||
|
* @param {...T} args pick random function argument
|
||||||
|
* @returns {T}
|
||||||
*/
|
*/
|
||||||
oneOf(...args) {
|
oneOf(...args) {
|
||||||
const idx = this.lowerThan(args.length);
|
return this.randomElement(args)
|
||||||
|
|
||||||
return args[idx];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @param {...string} args
|
* @param {...string} args
|
||||||
* @returns {Regexp}
|
* @returns {Regexp}
|
||||||
*/
|
*/
|
||||||
export function pretty(...args) {
|
export function compileMultilineRegex(...args) {
|
||||||
const regexprStr = args.join("");
|
const regexprStr = args.join("");
|
||||||
return new RegExp(regexprStr);
|
return new RegExp(regexprStr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class FramingOptions {
|
|||||||
frameChars = FrameType.values.Double;
|
frameChars = FrameType.values.Double;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} o
|
* @param {FramingOptions} o
|
||||||
* @returns {FramingOptions}
|
* @returns {FramingOptions}
|
||||||
*/
|
*/
|
||||||
static fromObject(o) {
|
static fromObject(o) {
|
||||||
|
|||||||
Reference in New Issue
Block a user