This commit is contained in:
Kim Ravn Hansen
2025-09-04 16:54:03 +02:00
parent fc28f4ef55
commit 0acd46fb6b
16 changed files with 629 additions and 21 deletions

222
server/models/character.js Executable file
View File

@@ -0,0 +1,222 @@
import * as roll from "../utils/dice.js";
import * as id from "../utils/id.js";
/**
* A playable character.
*
* @class
*/
export class Character {
/** @type {string} character's name */
name;
/**
* Alive?
*
* @protected
* @type {boolean}
*/
_alive = true;
get alive() {
return _alive;
}
/**
* @protected
* @type {number} The number of XP the character has.
*/
_xp = 0;
get xp() {
return this._xp;
}
/**
* @protected
* @type {number} The character's level.
*/
_level = 1;
get level() {
return this._level;
}
/**
* @protected
* @type {string} unique name used for chats when there's a name clash and also other things that require a unique character id
*/
_id;
get id() {
return this._id;
}
/**
* @protected
* @type {string} username of the player that owns this character.
*/
_username;
get username() {
return this._username;
}
/** @type {string} Bloodline background */
ancestry;
/** @type {string} Foundational background */
foundation;
/** @type {string} Money */
silver;
/** @type {number} Current number of hit points */
currentHitPoints;
/** @type {number} Number of hit points when fully healed */
maxHitPoints;
/** @type {number} Number items you can carry */
itemSlots;
/** @type {Set<string>} Things the character is particularly proficient at. */
proficiencies = new Set();
/** @type {Map<string,number} Things the character is particularly proficient at. */
equipment = new Map();
/**
* @param {string} playerUname The name of player who owns this character. Note that the game can own a character - somehow.
* @param {string} name The name of the character
* @param {boolean} initialize Should we initialize the character
*/
constructor(playerUname, name, initialize) {
this.name = name;
// Initialize the unique name if this character.
//
// things to to hell if two characters with the same name are created at exactly the same time with the same random seed.
this._id = id.fromName(playerUname, name);
// should we skip initialization of this object
if (initialize !== true) {
return;
}
//
// Initializing
//
// Rolling skills
/** @type {number} Awareness Skill */
this.awareness = roll.d6() + 2;
/** @type {number} Grit Skill */
this.grit = roll.d6() + 2;
/** @type {number} Knowledge Skill */
this.knowledge = roll.d6() + 2;
/** @type {number} Magic Skill */
this.magic = roll.d6() + 2;
/** @type {number} Melee Attack Skill */
this.meleeCombat = roll.d6() + 2;
/** @type {number} Ranged Attack Skill */
this.rangedCombat = roll.d6() + 2;
/** @type {number} Skulduggery Skill */
this.skulduggery = roll.d6() + 2;
switch (roll.d8()) {
case 1:
this.ancestry = "human";
// Humans get +1 to all skills
this.awareness++;
this.grit++;
this.knowledge++;
this.magic++;
this.meleeCombat++;
this.rangedCombat++;
this.skulduggery++;
break;
case 2:
this.ancestry = "dwarven";
this.meleeCombat = Math.max(this.meleeCombat, 10);
break;
case 3:
this.ancestry = "elven";
this.rangedCombat = Math.max(this.rangedCombat, 10);
break;
case 4:
this.ancestry = "giant";
this.meleeCombat = Math.max(this.grit, 10);
break;
case 5:
this.ancestry = "Gnomish";
this.meleeCombat = Math.max(this.awareness, 10);
break;
case 6:
this.ancestry = "primordial";
this.meleeCombat = Math.max(this.magic, 10);
break;
case 7:
this.ancestry = "draconic";
this.meleeCombat = Math.max(this.knowledge, 10);
break;
case 8:
this.ancestry = "demonic";
this.meleeCombat = Math.max(this.skulduggery, 10);
break;
default:
throw new Error('Logic error, ancestry d8() roll was out of scope');
}
//
// Determine the character's Foundation
//
//
/** @type {string} Foundational background */
this.foundation = "";
const foundationRoll = roll.withSides(15);
switch (foundationRoll) {
case 1:
this.foundation = "brawler";
this.proficiencies.add("light_armor");
this.equipment.set("studded_leather", 1);
this.equipment.set("spiked_gauntlets", 1);
this.silver = 40;
this.maxHitPoints = this.currentHitPoints = 15;
this.itemSlots = 7;
this.meleeCombat = Math.max(this.meleeCombat, 10);
this.knowledge = Math.min(this.knowledge, 10);
break;
case 2:
this.foundation = "druid";
this.proficiencies.add("armor/natural");
this.equipment
.set("sickle", 1)
.set("poisoner's kit", 1)
.set("healer's kit", 1)
default:
this.foundation = "debug";
this.proficiencies.add("heavy_armor");
this.proficiencies.add("heavy_weapons");
this.equipment.set("debug_armor", 1);
this.equipment.set("longsword", 1);
this.silver = 666;
this.itemSlots = 10;
this.maxHitPoints = 20;
this.currentHitPoints = 20;
// default:
// throw new Error(`Logic error, foundation d15 roll of ${foundationRoll} roll was out of scope`);
}
}
}
const c = new Character("username", "test", true);
console.log(c);

34
server/models/game.js Normal file
View File

@@ -0,0 +1,34 @@
/*
* The game object holds everything.
* All the locations, players, characters, items, npcs, quests, loot, etc.
*
* It is a pseudo-singleton in that you should only ever create one.
*
* Serializing this object effectively saves the game.
*/
import { Character } from "./character";
import { ItemTemplate } from "./item";
class Game{
/** @type {Map<string,ItemTemplate>} List of all item templates in the game */
_itemTemplates = new Map();
/** @type {Map<string,Location>} The list of locations in the game */
_locations = new Map();
/**
* The characters in the game.
*
* @protected
* @type {Map<string,Character>}
*/
_characters = new Map();
/**
* @protected
* @type {Map<string,Player>} The list of users in the game
*/
_players = new Map();
}

125
server/models/item.js Executable file
View File

@@ -0,0 +1,125 @@
import { cleanIdentifier } from "../utils/helpers";
/**
* Item templates are the built-in basic items of the game.
* A character cannot directly own one of these items,
* they can only own CharacterItems, and ItemTemplates can be used to
* generate these CharacterItems.
*/
export class ItemTemplate {
_id;
_name;
_description;
_itemSlots;
/** @type {string} Item's machine-friendly name */
get id() {
return this._id;
}
/** @type {string} Item's human-friendly name */
get name() {
return this._name;
}
/** @type {string} Item's Description */
get description() {
return this._description;
}
/** @type {number} Number of Item Slots taken up by this item. */
get itemSlots() {
return this._itemSlots;
}
/**
* Constructor
*
* @param {string} name. The Item's Name.
* @param {number} itemSlots number of item slots the item takes up in a character's inventory.
* @param {string} description Item's detailed description.
* @param {string=} id Item's machine-friendly name.
*/
constructor(name, itemSlots, description, id) {
if (typeof name !== "string") {
throw new Error("Name must be a string, but " + typeof name + " given.");
}
if (typeof description === "undefined") {
description = "";
}
if (typeof description !== "string") {
throw new Error("Name must be a string, but " + typeof name + " given.");
}
if (!Number.isFinite(itemSlots)) {
throw new Error("itemSlots must be a finite number!");
}
if (typeof id === "undefined") {
id = cleanIdentifier(name);
}
if (typeof id !== "string") {
throw new Error("id must be a string!");
}
this._name = name;
this._id = id;
this._itemSlots = Number(itemSlots);
this._description = "";
}
createItem() {
return new ChracterItem(this._id, this._name, this._description, this._itemSlots);
}
}
/**
* Characters can only own CharacterItems.
*
* If two characters have a short sword, each character has a CharacterItem
* with the name of Shortsword and with the same properties as the orignial Shortsword ItemTemplate.
*
* If a character picks up a Pickaxe in the dungeon, a new CharacterItem is spawned and injected into
* the character's Equipment Map. If the item is dropped/destroyed/sold, the CharacterItem is removed from
* the character's Equipment Map, and then deleted from memory.
*
* If a ChracterItem is traded away to another character, The other character inserts a clone of this item
* into their equipment map, and the item is then deleted from the previous owner's equipment list.
* This is done so we do not have mulltiple characters with pointers to the same item - we would rather risk
* dupes than wonky references.
*
* An added bonus is that the character can alter the name and description of the item.
*
* Another bonus is, that the game can spawn custom items that arent even in the ItemTemplate Set.
*/
export class CharacterItem {
/** @type {string?} The unique name if the ItemTemplate this item is based on. May be null. */
templateItemId; // We use the id instead of a pointer, could make garbage collection better.
/** @type {string} The player's name for this item. */
name;
/** @type {string} The player's description for this item. */
description;
/** @type {number} Number of item slots taken up by this item. */
itemSlots;
constructor(templateItemId, name, description, itemSlots) {
this.templateItemId = templateItemId;
this.name = name;
this.description = description;
this.itemSlots = itemSlots;
}
}
const i = new ItemTemplate("knife", 10000);
const ci = new CharacterItem();
console.log(ci);

45
server/models/location.js Normal file
View File

@@ -0,0 +1,45 @@
import { Portal } from "./portal";
/**
* Location in the world.
*
* Can contain characters, quests, monsters, loot, NPCs and more.
*
* Can contain mundane portals (such as doors or pathways) to adjacent rooms/locations,
* or magical portals to distant locations.
*/
export class Location {
/** @protected @type string */
_id;
get id() {
return this._id;
}
/** @protected @type string */
_name;
get name() {
return this._name;
}
/** @protected @type string */
_description;
get description() {
return this._description;
}
/** @protected @type {Map<string,Portal>} */
_portals = new Map();
get portals() {
return this._portals;
}
/**
*/
constructor(id, name, description) {
this._id = id;
this._name = name;
this._description = description;
}
}
const l = new Location("foo", "bar", "baz");

5
server/models/player.js Normal file
View File

@@ -0,0 +1,5 @@
export class Player{
_username;
_passwordHash;
alias;
}

27
server/models/portal.js Normal file
View File

@@ -0,0 +1,27 @@
/**
* Connects two location ONE WAY.
*
* Example: two adjacent rooms connected by a door:
* Room A has a portal to Room B, and
* Room B has a portal to Room A.
*
* @todo Add encounters to portals
*/
export class Portal {
/**
* Target Location.
*/
_targetLocationId;
/**
* Description shown to the player when they inspect the portal from the source location.
*/
_description;
/**
* Description shown to the player when they traverse the portal.
*/
_traversalDescription;
}